From 5c42507b12efc4d92f5677b1a9ceff057dd59524 Mon Sep 17 00:00:00 2001 From: Amonoman Date: Mon, 9 Feb 2026 20:29:12 +0100 Subject: [PATCH 001/145] Delete assets/images/1.jpg --- assets/images/1.jpg | Bin 129551 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 assets/images/1.jpg diff --git a/assets/images/1.jpg b/assets/images/1.jpg deleted file mode 100644 index d1ddaf508821a0f459733144408eb20c6ac57646..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129551 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 Date: Mon, 9 Feb 2026 20:29:21 +0100 Subject: [PATCH 002/145] Delete assets/images/2.jpg --- assets/images/2.jpg | Bin 125993 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 assets/images/2.jpg diff --git a/assets/images/2.jpg b/assets/images/2.jpg deleted file mode 100644 index da279a982d678a72a159482375924e0f607bb34b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 125993 zcmeEu1y~)+vhIRF2p%A~1rHwFEeUQ3?(Xhx2?Uqm!7aEu3k~ib+=4p1b~2m0EmDO!0i$wmgviuny+P~ zg+wI;!3h8Wpq>K&<`#A~GQuyODk-Zxh4~7A03Y|Ht!r!bo%^R}FzR&tJ9d=z zpCbM|5>`*&Ru>HU2L9>UfRzIP5OQGtiQ#wN=#JO<&b!_5j&@ddV9bX*-bPO5C7AyN z=ARkvTP?wdA-HV%V4Fq3_UZ!Yz#JpsIe-DcdOHJn34nU= z;O-CV0TeV86!b$ln1^6OgoS$qhm836F)|`D5(*mD6BJZTR3v0{JakNKY+PL2$4~GH z@o@;TaBy+%gg`(+LqCLmhyVkFfP;dJg7dfOwgrIl2o3<1`v8I%0Eq(e00rW<4O{?l zb%egFufNhm2(XOz>O#gUR|5E4cv`9e~+FN!g&Dz5!Wn7^rNCnR~h+-K4in#*2iH;d)yq{ZOQ4- zn%Sqh7nH}INJ2jQeh=KCoi3JAR9?y+ZRehRa{C0zc>kf?z?Ax!2FOR&n7;h%Em+*o z(GRMJjG2}VCV1J(DH6GAzAa%N-vTgp+u##6bvHm^gG{@k7x0@>D09P47n2Ovn9q3Q zj>3l9FyrUeH$c^*hGr?T9>Euv+f1shpkf{}+da*(!KljeNz|XS1l-e#4dnm;u=Jyv zjMvP`^gWzS&t9mb#1e{p+gompjGnr&0KK{}lpJ@q;iR|m>4`iFv%YBEI6Uj~lyd8k z@Cd!=+*n@UKmb98Ww1^sm3df#%2()fdadKw1`G|Xm8S>;75snJ7HH}hsQ>`!g7{TH zj9s#+=sF)wQIlW8^;9eryI1*Gf`%z1Qjz zp^$l_Lrv0?GIHUuI^+mFN{|dw%krAgGq0aDK&FD~XQ6+^^ber# zx9$&-KNtLW3yKNvQA?U4vc|RR<06A>wE#5+M{M#QHtz{}&g=NJ7ynPPO4jvXro)a! zX*yBu_Usdv;kp2T`{9?3jI)yc<|N|fMo06{x$jq0l${4r_$P~uhK%I!@I69qM>mCPv*s`1YYmPr>^s(}WAoq=yRO>njVANM$!*$(_VUQ_0alVq%bnLVQ(CE)o52Gq!ZsI z{1*a_lJ!pP@Pp$KayyX56-jM^DTS7kG-JbgVDy%)9y!n{H%^%=0Xjb4O*D`a`fAt0 zA%@npM_wTA1)7)xPnWppO8Zu8Nrax##UwE)K4bTS{eod{6m9Dfi66sEP3*25+DU5j zAh~N-FFg37P$VYSo`TtWc6YNTSe)n~0j9*7mA!^MPn5&UNLc#yp0tM;^ireoa~drg zFE}x2H%M_w<~z`&9G|ZpPHlt8bAnAzIXC`GDFA@8uMXK{4`NToBkm-Xrr?E0-sI)*lpnXcTeu~KY{yo63U&E6^Zua**9*RGEo zrBwNwjQB4a6Nh^Ju|P2S3D`S8O`TQTcN`AK=j#M|ZzjPQNm})aSAaq)2VS-*_Stvn zJ69=LrW!(wCHJ*M6!+N9!+hpEv{WF<3vxFt&KH~od^60AN*~9(`dsi`3k37~I|gl1 zgHZr{{Ug!YBPqnH*)UAd+Pn=9>%J>kDDutNr_(hVxc99F zg69)v`K|`d>}F}nA7%Yj7*-*yWD^HT)1zHL`Mwi_NbuqEq#HX4$?sI>OIl}tSN2yT zeoSvvn(qBH-F_o(sty?7|3|+_#&knh@1Rb|JF5&l3frhJqo)b#B|hwG5+}a?l{@VE zHU9q!!^p&<^y-r2!xEZ^K6V#B0Pyej#6LRX=R*Grha3oElApa@Z3;xvWFYWss;NDa**L{dR^+tmQ?SRPj<^8#0xy|YGBfnzd z!E;p}^&?=UArrp2YfaE|rpk?^Qw@R1W1C0P!&eqIi$vqUlXb_cSN*z?p;mPlK^GI9 z%IaHi$NollbqM0wULs$e!$lmnit0gUX;G|xOhR-YEz8$)j)~(L;gn_bQGyvlwyq_09k&1=AsYq$tBa{Qj$Cwm$+Y&- z+~Jme@x#7({iZEH1c!M#l^t%^3WAe^T^u{Q*al%tM~zp;m*j(1$uI>|+bHEpvtF2> z0ZdwH%@s!%>@NIHsgNg31-58PdpAg!!`9?}D8D-Cekur+UwJ^cx_N%upiwu|98lg6 z-dm+Wd#<2jt6nc7n=PMT>y{h9%9ny=s4nr2Vcnq@#7DewX)D6PCbP;>^V!AA9lO`C zo`?&nzs2<(yQkwKeqX31N35(AH>UtL>yuA!@LiFaWIkqXF^~y$CfPlM$h6b2U{5U&p*!yNdJBfPT=QJ5@_$;e~DcVQK#qs&34_%*YX@+>eYb>Hv zzH$E6#wz}{YJudQYuw120F zzN-J36hLiTt*Q7UUTn?5OUw#uj~<~49$Y|su&6G~8{ggwDu*M<#Z5c~~h2~^8R@pv4SAJS1b8V4)@0H%^aYy*-7 zFR)wh2Q64+jb(L;rnWgnY0Lb9NY@~0V?&myHk)JaokJG1F$=|_xd9AE9NKw~a%!;k)($E z(AlP8+6W<3xatk4dpX!_=eQrZa$|FXSG!>vQnj|TAZW31=|t3utOl0y6ZC*9N5C#3 zq6_F296CykahK!1n~?DEy=dAf;y#UeUhZNujdH1r58p99R{K)zZ#1(Tha`5=5j7p3 zorg;8dahQg+VfImc9#s*tooZDYi;`8vO7*pUc%0#AZr|MoUnbQ(rfV6?w`^9%NcpokDgT zx)9Ws5&@4B;}TB98o^Bim&#ySm(c`GrK#1Av5fgI?zc&-u{`b;I#WiKbd=zuVXR+kA!BX zmMYdmgMCx;j8jhNzHH^HqkvFXWE540`h<=QW8cYhM(!Df2fC?C+nsRh*9#Hm++GeK zt-sLmL-PHGCbs+X`k;L079SsDN%rXRg>N~b^zzl9vOB%w8jBrBW53Ug{&>81Rm@a6 zul}mYZ#3CMPtYS`u{7URjqS@1^8O<5Ke)m07nc3g;dpcpAv?RZ<@~VB4vZFAAM~M13tMbAF_e2=3=&`lO=YML` zzli+anMn_&aGz;3(OV~ZM>nUGt#aBHu57bxUK|nfsk`y4Dfe`mveCWat(=-(a-Zbz zRIdNFF*sBoKWX)0=U>SE=h63Tj3`Gsve`?wzP>rO0w8O}$j)Zgm@T}d6FIqJgZ;Rf z)fo>=VWG3_m+8KfwXhAs-q);25bQkMS zYd-iBg?Mq16CpIn^;YF_;C|J8<7;76h;H5ny;%l5Iq?i!|#l^z)0xn#06yq*vSAZEahw%2ZBB4+oWk2a1=Ili(vo z^*U+o!^pE>MuFkR6UBIzX`8Etl)t&>z9pd+&9u3OWv(a#UG^=JFnabB>3}uPDBSx! zY{Qg-pdEeVa&z@v${=5J3lP)V=^#a7e!68ZaAi(2e4Sg5hoAT3dkc1A$0XNxXUtXu z`z>5R^a$IFcs;xcHWjndqK!9S%IvMbY&nJ-GqQ3oryq?DB&*m_Z>qi?s>_pHZ`Bib9+m_yG5z9!Q zhyMJz@%w4^CL`|l{q6oQ29j8uKIEo-vT=-O4eT1_h6TdbONSRa&S4VWUifV99eGhh z7y4WriKRt`juDL?CpA+TJ~C$&3C>b{Bm&+VsDKu`bLID1Orf)s#MIO_e>pt8bEF@u zJwfSQOf%WSxPMaLF+Qjn$66-TF+M6@-%0C{E{V0W`9b06 zb#~L+HVd&M(adZKVdwFT^H^|jh|{!|3NDWp>2NstkP_nj2Sd<=`9OK=sY~lPh?j27 z)K{oe0=MwloQe7SULC=-D%^&r{Vj+yBJ?|Ui@;HY1ki!4J3hhMhX4WMwZ<#N##D2A z{uz^3*=>4e)}w9Ylv?HZAjE7!H#Q@4{ctzyd@CO~RB~1k zrZI!9(4?EcG9kUKS+l+!^^(rG?rQS&>gne@NqgIy8QYn^^w!buR(J5#}|En__ zgL+XU&(gA_S0OnD8LlYxqt1XRxlY3`6XmC*?zNv8{@R-Fi2Ip;^@f#2`=ErzTlCfSDs6RFNY_uvavNQE*)|c12CL7eB56^NGW`UgaW!&CcO==U*)rsMknBla2)0)24l{$hc$TZ<&R-fQc$E* z**`f7H?(G0{T+q>=0Ns$RQ-niAKQq6mZe^QY&Y*%o5?@H-B*?E4BO!3U@jQq8IzNq$|HxdgK(3T@nb)I zR}*75MVZpIDdlajT?gYHhRJVm;n;ExCR*)!Gb3rh#J7_=6zb@e|v) z*pSV>`C5+tNPLbgyBdLfI|<^)Q<6wFo7^kk;#y%|uU?-A3ecXu^e(dVj>~i=rS&ip zz$JKRGk!!G!pREp$Yo=#ih7nEm8 zW>nWw9G!xbX?>yV79SDw*$@+rj`2OV5Kh(hQ zjww4}$RcP?{Nx`>;#>T3Zvnh$k{GF$6>?|`kKdF|2}5NIgP%@=TZtZL6on9-9f2L$ z0X?H__P1QKR|6?WL^Zvkh`|w0sE2@GH>}KF&uqa+d7Hc+@E-8VqZJsY9`Ln`z(Hfz znl=#bSTgPN9i$cTX=K}c==Y+xlZSeGL3_Ik|i})xK-80q|Li5 ztI0PGQ-baUXow6sF1d@u!x&zFWes?>e-F7I0u-G;ZSt_c&{G8p3-fV*-k9o3OPG3y zr$FD3pl5q>K;_`HtRY<)H?KmUGrv{mmV`k*Fe-_`I-%;Pp4K{EXRT=1=&cH6SY-WEDT*`18$wD z#*r(Vy~K<79@G8e6m(nrhnzpD;XCZ!<$o*3FW~zc`b*$%UH|7^hv8Yd#wi|%0@;jZ ztOZ^(SzY)|j9yq>D5S`r^D4Saf4wk2zTkTl?LLvbAB-mmDo| zg^-pQMW~B__)R6f9#QQKVGx8UonB61N@rx;OTh>gVE ze_giw>iuxQ9b&VdLvbKTvo}R(R2Gi*d2gWk>4<9IdVRHY`shVW>*hj&U)_}Ia{8zd zuZPkxX#3!70gk02V3hsyv4h!(ciPeC&3GfNh;HW`;H0yq1jkJ_mV?ME5mEib8gsJe zL|nmJe&W*y2c;7*;_H(cBpJ3SUpO+;Mu7c|zS16^v9ClmYy3B__8;3LY2XXN=imnk z3_>jkWaA=bb_$EGeg0=95Uq&bl38Ii+if1SHU%$2zdy=?sM}TxE4OXm2<@N`b?t}p zR_u(PlJF2MN+_b3sksG6H$4mLUc#H-@!UuzoZ@PffzPS54CyxL)@wxzX^buO4^9Z$g9SJ{m2a~Wi+ z1{Q>Bn^8~HjLrd5x^%>uN0_^~MXWhpo(?yr4W=iaJIp??)f;ECXEqP5xl;&H%F)HTTn+b(7Ud~Xm@7*yjZr=;MAE=d#&?1uADmA?id+vj`{IqT?YKPd@JfL;N#S@ z=JGNkv}YD0SXGo88_eSx(Yw6v84n8sY+>B-$YvW2;UIss>7RwZubV(;&oMa^*4kRxL(dZ7A_6|+<(q(VQ>K>bqa0vFZmgeAd zF3-Oi3_4}jr*3FsYX7FX@` z6aD5}F$d<@1uE%#w6k$V@i)ClV<-AA>N^W;K&EZboDciP>uCBeQs~onk=P`zQ2jD` zDqwWjPq+xZ!#-U?K6d3M2&0$hrr!7lv2mng_l^mFORa**<)Up{>d;De>g%5x_T2-c z8=gsx;fdB)mmO8k1UvHqK$GMwM)#Q$9M|&wDO;q|M&gqOiyNH*;k@=D8-nU?;@RiS zkY}b#Nd=2@3Bji_^zd=PX{=U`~~^1wt_toT5;w9W9j z?I-?ki!l6gYF`o^dq=ofnAjr>Mho1rHI#`%GuTS3Z(6Jg&YHQV2FYe;(z4;fuWOol z{h9gS_pS_&{RRjHr8Kk>B;2y$P4O4TM|Op3=?gjAYkbwy<=2s?Eo^W6T6M@!u6G_J zILocSKUFVFIh1PXvRF(@#dO8QLIQSm7-}xP6Bw=W-*PL#w~Rn)UH%ZVF0(|e&$iTB z*rENx6wVw8IKZii@C>Ossokg3zW?DZfFJzMl$wM8IwzX4ApyQ_=W92pKc~6k@(uw>()@61Nu>?ot$UkkZ11 zN#^Uu>;7HT1pbtx`Srv_6QS~<8`4IU0kfuGo7R#d)?;B!rRL#BG74v6o%(Qg6Ei` zjSqBjgi18Nq9Twe-K)fXpfR%rVOQ~eLG!PyfE36q!|liF?zjWoHw7lrQiNLB`Reu{ z8Nq$r4%_^*$i0JcP%&MSp|g1*osQ1zp)qpD>G4cIl`iny<@(#aaq0D(ma6ge%B4AUYi955aouw>1`hm9E0jN4{VThE z7k_WnGoVGy#iUN}vw<=5BHP`ix)1_e;FnQ2n51%cJ^OAyJ9aPR5M;wp( zoyvd3-tW}GpINhBjM6jmIbwxxT`$X*bcN2xjL#RdYM5P|u>dQJ>x)YcZ2IlHr-W?^ zB@?B|DGL@u?*{LmrhhC;6uclS0P+Iz&~iVW^E>cv-v8gD|FLllLZdkN(9yFP{l?QB z!0YJc zV?A%^Cl!D1U@eaa`xYdwI>n-$IfvyaRTTK2LC8}Jl8KE(-svH9aN~Aw!a{Q1*Tq7eXMtZbSEGprET|S<;O{iPM|C1WOcZ{A{yoMm%RgkIJX!m#pfQn4e2wLn( zJe(v4q1uZ3)VG7vD=m9?c3I{XVWLq;ro9zPLID$IuBe&*2{WmukN;8^bg#{>Pb%-V zy={2e!Ba5&`#89;nG)9+ci|C_O!3-FtU@mJNg=sevJG8bSDkSi%&av8aCFFJ zLAc!r7uV?}HT_RC0SF%N6K4zdt6n@W|2xa>*UM}m-8FD2H9w8))h9uc0-}wuIIO#& z+V@$qSc~Il3NFDLkm0AJ{>U{;fQGN%z}di^tU4_Zh!#_vpOK-ZTAbY2r*7rjpY*$s z%GqN+<4+h5?v#Fis@610$aKwcc+E{wdo?qT0-UqT)j%7z0a{o%Y@DpQ?yRYWwZUqv zM@-Krr96DluwKzZgqNAb7jaS~iPqXVb5O>xWk+jY7ZGgffKm=A8fi8>_rZnb!1&9P z!dam5gT&_IR`5TI3}iANd+hu(n>8xv>-f-$I0zSQ$4oWc!1OJB3n@?RR zmSLtg+_Y;z(-nq?7L`YXNyS`*#`mM(U#a;u{l1%>-2M#>s@fL@-QTpewl5IA5^#}0D{n+fn z`y;z(GrB(hj01kf+;`aX?y5(8qwJdf`I9wsYsvY%?hSCV?dfMtyawFoCl2Ln#jtg* zXvD*gK8_lqh3sDQUccu53Fdo;#+T_~3VI1YQ`Olik-}=3Z?RU(6sRA9eZ(B0-(E_~ zH0$)F=w#M4Ge~2wITTU-tPS6L(}|5+K*CK>%MI;hWk|`fD-8jTN`J?c zsn}d+o#_^K?R=fybe)*sw#MNo1u!hy$i}0*hX!2cz>#d*u=eFoomsYSq?i5vbHW^<8?c}!+ktZ@h_#?HJgYl6bB0!7LekR4qP%;}>BC0qUuwGtK)Zy|ZbnSLG<=N8 z`V<@k_JD#?n|`p6Qq(o>q5975i2-5n7FU+zpoQr*?)th|dt+G31t?yz;qoi<3}0H2 z+wRLsvl^VRY*tOm2~kjMl#Qv)MA`HNN(=+G?*HUTM^cwRhF*GIgn_YxghuDLlPsfO z>V4V&{^{45BTTVORSj(syy?5?uXl{ov!_2V?H}`30HD#i`*;MBADk1y6qhqvi%zw` z=dZ-*e+;QAIQ#|m%VX-Z1cQ^WG=K*Snk?asM<$xjDIPlk00dSJSWznIKa31K(&fvC zak5d$WYz@XxkTx|NczVWzN$92m$SnlP=-L;GZinQ(|Tp?BmeirUmnCGEO_v2LOjLX zF~E;79^}OLExL&9=G;f|A=~Z)KmscDPAr7Of;_zq?`8Wcm5+!c4Bz%uSYe{q zKV<%bgBa)F9#YgZ>KBKklb9b0LlCeabXfSl6f-zS54hdq*pDBl4>e&X{B zg^({AJ5N)+zC(T)Y7h$ElqfZQg1V2+V#bj8eaQgeeYY9qiGO7Ip7HJsK^=$`H*hfW z=pRwP-f(0M!L3O#(C5Yy`U3cENYLk|wV}daP`~B>*W9la@IiKB^L`#)nxl)b#@UK} zO^wl5I7++G4>?&4N*M~$3U|AN6U<(s3~L!MIp)w-jj_N_3H`7;2sLt*8bDrzb86SM zzB~JIz1za8*}`71YIG_`a|%c6Ugj_U006e!%Cj&&x#{K*Fi$vik9MW0wulE5p24>6D$8~D-u#NfmHGrUP2!} zgFm_P2>c}u2x!P#z||+%{ej){LWR?G>-+01KzN9lbGasnO@Sry)YK@ndXKn&I7EA4 zc={Glu(p439Y0vGwtI97C|^98Z`$UlyYVzA2VdN7-fV}JHRlSTys3={Fp=b-1|3 zzXh!I8XPLGMYYS8YdP`^05>M)!{VC?4Hma9@?BU4GbgwrT4`z1@(h)slk_m`KPZ$Xu?hVCS7OVWC`Y#S4Ee?|!O5*8d{zx>v?V3Bp<><}Ie0hzt@e z8K=9DQ~u3<5z8$=c`5Gu9mA7lvrOst)Yl77&xi~MG9QHY2WcIV_1g{?HxROTs`R&x z9Vr}}rik}&Wq*;tUQWVErz0k@89z4Z>P~8xKB-eH(8T2BRqUOf$j59S42wQ2);jft z3o28ldaSofugAI;hF1+EO~C)kTXemOT|g$5T)<3gxZB3AU?XCtpj z^O`zCzK4hMlb%a1UE7yfPT~Kb4on%fNI4=@7ZPD%hCa>d($MhFrbsR{l~KDCrroS0 zDM2j??T78cGx;M&@M*#EvCYGNN|36~@hAXheFxtnK5jg7`=uYen88Dy0yEQt>l z#Et`YU%LjM=35sLu?cTa=%iO;gV*oPU3WyDrRBVO29$^dJ;YwYKZ&uLX||TznrfVW zmc$)w#(8Z_TA$mLx*R@KFS^)R{9MsTczWyP!d)jT_L4mnc8oV0&oP?OB=Q@ff`Wv81$z1PFl+N11*_7Re$^dQ zpNdpO>{rw~C(W0aJO!aP?(rZ-WWh1z&!F7xiCq@yH}!BfyP7X-an|zjPqpNohI!t4 zCsw*c^qEd>%Ix5XBH61HhV5@TNabz0qNxqn^soeix4!MlrQ@lWDK_dR*eH zOv{g0qN$8j_LM#_qgsukb;?}#5cAX}SmQF+b%swTkq^iIq&&?9g^Dxr7Qo__^F)Vt z5Nv+%=8HgFo5hGzfk0odV35;|sE!+QAm+eL5?_pw23Kp*#}zT1FGkCD#ga}ZWBgj`wSA#|H!k z3rQ~1;)si<7bIeZ6_C=u!;*KVFy8u_OWL%VnP?w6hV3QAl*vtI znQI~U&dMM+ANRMbr*UGD+Q%NlpZ5HxGuB{sPFQaNSt>{9J0^!DG?v763eNej*72>5 zBYLZN1>IEIm^$^vc+7YQa<^C=h@E0hm!sapKeHKbUfc%m#+aVhfk1U+&;@~WdQ%xo ztl4`lyFEl-%#5N3%4tWYCY!a8s&2Nb#9yq$?9NlgmQa5r!~Q}X^WN{d%(nENmwTFJ zRJC&9!6Wah@j!bf}w*Sp-kGEGVmct9CZbY9M?c{J)FcrnP-|?a9J{^cONT zkEn=l0g{2v5X8mkm$aH+sx;+JSqX(t={-34%D+rpzm$y{)WJ^VZurWD0GoUD`k6Ot z8?0B#WIiyaQk?3g!KuCUn`vQN#8Bb3%Ui&q{FpTThZFozYy)enHyy*<_!e%(lNa@M zidU7F+p=xjQ?zD70Ro%Y+8|SsSGKz*Zgp5^LfeguBc}a|<13zdX6yPLGz4U?UX(L7(%mm7*N_NilEVC}^coBMOSxkr6Hl=B% zeWqHzzR~hw-qcvn_6Oo#uBcMf_yNw-;ss4@TDx9Tr88v}Q5{gP$U7Lgia?Xl&&BFb zD2)|Sr&PrCuC2GxU1?2|t77&`-*h>ZbFQG4To$7|Wc_B`{>T@>zBHJ7I!MIJ4`Tfw zK~!x;@k6tCDKTRZM#m#lVjD(r+cZw;<*f#Bcvj|ojF3fxeih7;pnZi){9o_J z?nAosxiXD6yaWDWh=x^&;IeB!DSE#WO=NQ&le@)5pXV{2)dC@K`8=fXAu5G6lP4wa zI7MrvVD}jDx?E6_44r6YOGvUI3fts1z0lOmu!M6Gm6TmB$H=F3vB(7C45~}}w{@dc z@3=;9`tu6y604u&C4w7i-3-2O$7$o>mIF_pjV4~e7Z}6W_(}W@_7IbA%CYvQ{ZWf@ z;$|go0mg+&XJ}^84Q;o8p$hp%_QK@WXx_0Rg+lGp+4uUIWOm?q9ay4rNzy0iqhgyY zm)c?UjBD!^U1k+yY9x0v>*VFWphrF0iFp7Gt_;2*jqa zcPP7fp8tGvk!DIV_Ebv~74@YZ#7?9ppdwbrgRVfq4(L3xx-K4@M5pl|_P_6kfPD1u zEt?gUNnPDtYEvAK4gq%X=LcpwUX>Im8omlBBZ&LbX()@IB;jDsF!dkyzwZaS9-Ece zY|#ib<^|CEv{&|pN%it8VP7rN;#qtZ1Zezoo8O?zf9u83C5n)cQz*8kcbpaP=~|$g z8tDX3?}3ub(mu6g$*pmu=8V??4iL3>hsK0lrUJeOPeA(=ObGf!Qb;L-y9t46~5v?{&jkRr`zEXS9n)f-BD&EThib{lfk!q=9?&L`2RWDV1bs3LM5$HuH zZh--6!+>JlmMP(Z>2Oym?*jwQksEa4jqFTbby(b%CzFC0Z4Pm=3MIk<2CmYPk2I+> z8VrNr<@#0EyD!2fU%NE67(Z5Oy0M8>4ks=tuVzLKsbcq{5M)z%*r>afW#|R0ta=V9 zR?Y1tbWJI|mB5y}K-`$I`ZEwjSkbln)Xs2QY4 z_5Q$5MJ))0I2y5wv&^PW;o{Ik_Y{EsHK>fMPc*;MT{nv)@#CrSn;48EB{KJdlU}-h z#`1bcZZ4Ys(3Umb8T^(`1r%O{1d{sC;jEOx1j1(#?vhVN_%~_$qQkp3K9S~%e{dy! zZ?@7So?7Ip`x>*O}M>_|mE)vX9m6?V4ERc(Xm(9)1D}Mqyx?p55%} z+pO)gsFVSYj45f9D>bhok#{dn|DY3_nOYh(%M zxj}26v+Zy>T1BAOnLq1O3Dv$5g{GtH|#9aex|EzD5%o&&SgB+kuptC7tKI=g3+nyj)zk0;OqIzrTHG9SC0Y zL}@lrwY7Y2z)Y`Rl?$_$Y;&MUP{Uz~2c3QCl-h$wagL8scQ}I0!&HgzKqBB!CmZ^@ zeA&&@q$`D-9_UI`oNV0`I?*5>5G>dh%}&#}cHqE9q4{m_B6P<)sFX-;MT064z0^8Z zRr>JMGKF?W;(&S!lwM75)={Q0NT|4?X;Jh9&cO^Fiq9a!N2GB^2%tG=CjYGik6itzYHzj3VFyqlv z#kd;OBDRElCWOi>gw3WJ#Z+#z9^z-jMFM*cSWVka2NfO$U7V$mVTb zW^C;wD&uFs!h6us_|`1uRMq%{)Jp?sJn#~FaYnp$N^oROx>#H(^`=7tpHiA)d>Pp> z$yq5WM4B&`jAvWTF`-SW)5)8tjAvD%KE?T{KS>GmEnXMr2#K{QaU0+b%r1g8Lu7R6 zci_Z+K}iGl$;5FXtfYjp6m?XRIvmjjbz>REWtMyuLnH_if{XF2Q?} zS5Q$<9;sDqZXent4&xID%89EIa~MNadoyFIQsY52YD$)`&!MM|q3Jaa6Jk<@LS|@B zZcIq00n|GT%3^+0(ZRA7Z!Rw|4gq8PMgeXz-df(}(n$YNj1tJ#_+>^wX`Bd`XaFia z0D7od)Z8ai&t`;yX`RI3)5}rMASJ(GV zpjT}6?_dDSE5zQKggg5#nhuuejDCO?WYthBywR><;otw z`fdA>T}tv~gNgU?KtgN3Lo%;p#*KVfjzRQ=%#A@L@w0Ih8W_2@^2xo!Y`TK_3X99* zur6G`Hx^f}{nF#$)r6w-a8tACeEvXwn613d%DO8q-K)fq*kYxk&UMxLCCD;TFL>`)2X3yP@Z zrK?-Og;o}FjCEk}SafyD%gG7w=frX)gm)TCnX2nHk2^;5Lm}&Sgmy&N`iB>_S}|+A zQE`5gHKwQ`V#UIurO&X*!r((GXo_j=9X3wA8guj2sbyp~1Z?zz<%`p-aut(_cq{o! zv}emu2xTV-V?|oNNU=rx4z4){zM-Q%{ft?)d!kY>YQ+edSiKMelW7@vaO=~c#_7}A zY*xccBm4G6Yaj={Zt)z_w+ZVk&)+eL zxv;{XJR^@c7kZRFk{H6Kt-;1jZ(=HMNNc=4F|mU!^ckX;Fs1)_RPfB3FRD1MZ1PyI zCdI;aOZNq)ys`@)7R)t`4exG%CW)WUfW9_U?d1SI z$D}t@Xbdg(X}HgM2T{uKgoZ4Xe{v*?^_L&Gu%_r3Or{djQKr&w?om5fc~1{xZ4(4z zi$lfHCq*UU)^7+LZQ#)IbZR>@*r?Y!WiPfw-&tx%boICe7>3~qF76z0al}@K>GO!q z^xJYXAI~K!mWLq?P)~KT7nk-m)Vn$rS!;lU?XJ8x;LFQmmd)}h=A%pY%SU-}ec(4a zmRrjMPp#FK8qz-;JZgByWpxw=Rv{?26Q=g$fNFCwP1U=;qc!k4NiaWOzcMd)<2{nh zx@+get7OA)`kk$YPRF=;ojQ(arZzr`6d$8@mtgu<`n7j_oIUlpn9z2ntpthPBplXx z5ieB~5k$+)Jh}OrLKWMJTpB#xm&k-X`Xjq>d7C2|S=>g%Pp1#`gyarVtS)2NuD16fP9+UA0&mFo zpK6T`F4PG4**w8v;O5fn^jT?6cM(P4?lt`;QNu?))vWD43d{iI<;{)7aiF5n${vTItGpu%-d|u@Mpc_|SK`y)ETh14LWX<+*aJW|b+js~u1N*PW6M)U z(3BKeAvII`v*3;CM(#I8DClM-_;}e&K*9w1G z%7*dU7u&*Ylh7DcYU-H+b!ESnYI;}lY)RTUJ9@2UPc(q7tdUoxMh^791-wYV;IVdA zXz-k$#0)9S&d}$XR%nsv5r93LQ5HcHuZ-%-!)yzn2g%8|IHr%(5Ms-2JA|&6G+kjy zU)a|ta=dpYK_FLlq%Qu4acKeM<$;alx}716?f$cP@9n7asYjl4$ItJvFHooDXvpOm zB2(GE;g2hf$9^*q(AQ0R?wU;{ptebZKx$(KS>kZ|?);;CfCqC=(mQ9$#8bZ-Q@-1b+@<$a*mr*KK5)RSpV^LU!6P}vvwke(QIVY zkDrM3P8mMW%t$j)gAnlw2T@p^0&LqPlZnj^&N1JYgqhaN9-(!AzRC8mETVe|;N8Zt;@L8had z|Av7Y&6{7CgW2e@q3M8YX24Jr2rd*O+$aUZ=9Icd50yvZ1?F3eC8$128XDmx*VtOA z)y5OoH#OrKrQ5yHBVEc9~+oSTCyEu)NKui@0ni2vQx{j9(2w4b_9|JpB)}9 zF`_it)mpJ62BFff=N`_H2}!|&5{72 zO-P5t+LiCpcz&H8`UPa56I<8FQ_s}Oeff|?`C<=}QSX#J;UXqjN6?Y$?7kmIERiEC z34S6ME+#gMrGqVJEtW2&_=}2(&lb!pK#t2rcr!4W>=EUwAb-39NG|UJFGBg-U6FWLA;AniM`qcpG z3GxUv2Je@9r(VjzE_eA)oE8eYya7+J3ayU(vZ-;Y%QWAjY~^sphC#Kef7ZMd7<)6E z|M*!Wor2#0!fu21q$%YzJXe`3$LDlZpccUeRJI)0PKi>&G<*;>>k& z0r<~|#I?k8jPqp{R0C01;y;^BTJ;ba$Us4()6&Eoai+h+re|4~2Tu-o!WoCgAO))` znD_}`BH5(mCj?%qJr6S6!mnz|6hM;gwnyYKHLLWmCwsj^)oEBrP!Xb2V9J5~_Jh)3 zuFv`DV}4kXv3FLwZ-(PzD)$N)gEZ~aS(Cl|X1+A0=EFUYi3ol;Ik`jZr()k8n;0?Y zQd{B7QX-AsufW9|7RwQO5I80=PAyzUB?gx&QZ??&FNnYTVb>w63J30>#T-0Ai=uW? z!#N6@kaUVkNG;T6W+qk|!xvP6b|{q~^AEFxg&J$e5;)n)Fr0kn+t~gkEem)>4%`}y zRu4#8{~Xl%RG8|ZrD3`E+X{92`@>J##TX61zFF#hwQ)l5HuwFU_pz&~3Fio26-Sk@ zrr5?wf#xtsP2{LBZ##rFzc;UQ+==XFog}9BbwF`tZ6RQp3S0g4^uVhrN5kTv5zGbS zEuONZ)h}jqx@%MU@k+I?M9L<=D10>XP=j`;HGEfDo3~$uIMfvCeXXu9FE8X6{N=bf zX)f`6jilw-q3anuF;53gYWBnX!Gr4KdYhfrAh=fIb9yXxJ7Y0;1m1@q*c<|P} z4VrH@%AFC#OYu2A;XbrOJZ9&Aw@@(4O!BmpUs*6`{}sMjx4%Dw6|#A>+)<$`VHs*` z6jwVt1y-f~Juc2l56;SvdsqvdXAo6TB?{I##>Li|sG1X~78F&P(CH;K%yP-4wgFPw zdDzjxc}zn1>9i8aAlmQ&Ytgq zlzF81lUDbC{#5bvDG$%4;;Oidk=yj+?WiGaBnJz!QnJleQd_26tq@eH1ZFNwTH|LU zcd|qatm)46nf)QGTyAP;k24vZlCOTyZ-1MmlT#I#Yt$|GfV!`$+*+d;ChQqvy90CF zwSx}+Y#Nvv6mg{$>mAL^_A0fq&vhF|RV+sj-8Qg6>n{km`F3QsqSSw5_l!iFuXc$J zV3EnGao4zBU1iBEeVNr+ze zGaJ5Z(=x~3i=DyvTJq0&xN0Ioht7W{%~}Uyf?t#Y2bEzi%I2SfJ-s*+u7&!|ofhTO zwBF$)yFMW|FiX%4=l`x11O_V=)DPM7C|gHXE>Uj9HYoY3j1K3XiZ#phOziFI>bnGcr~PeF=vz zh+~;!3)kc@ccu_i;81JwfeixGI^M~;<>B3@@V9KwjqsBs)ey9p_#|z7{qSxJAm zHiZo>8=q4GB?%4#V)hfdC)$*}MT3mYCCwuiv5a9uFR@w@KA1bc(faOuQ`hjPPvw)Z zpudB28ljm5ZR|!vxf2IuV7w0f)+*eWI(zJ!Uh-gF6T>`j8%+qotND9qy!3@k5VrI7 zx|T&^6_`$Tj0C$9TT0w=^4eb%#2xP!j6ft7y7()EwalOCbtW@DASC|U@NkhuA8PKt z!(FF_dd+OaLfr>0NSBUQEjQJtpbaJAEF6;f%x-vd27YJJ;FifMO{9#e#vkKD)g@Y9 z8vB8MNk`Uvd!{ZpprC{2%6l%abtQeQ$^j9$ z#kJ8rt3Wv#oWoW9-9s@WLjrln=bG9RnmR70u0PBaLy3y7O7l1h;rlwLXd*<&UGutgnMi=@-r{3bm#KiMTc4mm*r< zpMloeizzYlOs!;7^xOT&*3+gr(QKT#UB_w1yBdS=O#KY#5?WtcG(EpyD;ZQ3)&OD8 z(Li$SdOc*b9bE8}JgcRCrGD8=-}6a69GIJXW9Jik-*!z7N(tm0!2&q*md&WtIfZS~ z4fL>fvep{CZsozbN~*xlP3`D~3O+q)scR<3#D@0#ybj=0O?VS(3YOwcJDG^N{0sMw zpCpr{N?}$fw{A~V?M=4aWl!Qg1d^Ei_~2LhbxHqr@>rZhgxMVZ=JY{#k5e#$s>4&b zJ=S2B{1*s%|Cu7EPHMrgy?-*0aq8RszF-X5v7v>M|DU(#@#$L5^1f#)DyMQT*27vmKY?K5q~xUj?xgma zS~`Z67$Wr(+L^Yg^su#k6k{O4`LT@4xPt!9V)w7Or<)ZSBze0}(gcm1TMG7>Xfy@p zcnW4u>2x%stNvhUAlo%C;NWXMHBs4U>t8s1ARYAv61FJnLbT+rF&6f`04OF@P2^+O zbI`SbaS(?a&ePnz7NAF#lo_tlz9NL4h$b0Gj3Dh5KztF%%YBp*aH=F>6exI1H3Lm& zjZ9%JBUR|k;TKJN=^!#s`T0WSE|1;|pm}l#Oa}O~;*#XfcnrnqJmJ%Gp4pm}2SF6Y zk0RWKGDR|J*q;P$NVG7SXoB;g>QsN>$i!?gClWtBonh#6u_wl<44b9^o3vY>n$lk{ zz5J5-iIZOG3sh?i0WndzYdfnGUIU6Std4nB>?_%J_zhN-Nr1?F#VTu-24vYTTvHlk zv}bv3Bf@Y#i-RzWP9-R8Tz+5Ce=#Uqk{nJ@f`iB~ptWpQ4~ky0zvI*n`B_T4>@6=# z;+VE`Xx^FL_5iX`6x+vA#_r*~X#|&2Sk-27WwtVx%T^?(7fjN{C^N}cc_af&B(xaX zY=8sR8p@!5`!w;qau?UV<`%^*ATxM^!3+;&=;h2z)z~OOZ-z*oAogC;?2<@I|B-EUL(;Osb2tivgtnb6UVyVwE$e&{WAsu&@Fl`5n2!1e7#346^WX-~9%STX zRh#wcpSIbR^lq_!EXi$t1Ui8Ua~wbBzPsskHqi--+X3tkrms*H8i2V zNK9j#qsH$$+J*n+Z%2L))KQ)AQ=M=FwRF4j>kpMQh@&==WDV+hO9=d02(6j4K{96rdGcj7m&i z#hJnUm()XOV|m)39Z~Ipq`@osD5U-lfyDX`l9~fmF!kA)?C@9aw{(m$T#QmI`B7mFtaxesPRl{d*HEaSV4ehBO1Ow&&ZV`zm3X^4`#x zy1Doio%ohA1KZs9+B0MJl15^dj3@Y8TLXanbbLwUj6LraMN`dec^26uC2?Aw`Xc|+ zo+DiHV(t)K?ye;*(fHpzsn+%{pBc&!8Y<;N{iXW%BE^c+7h}82s4z&4c|l(cMx{2$a;k3Gb>oo5rs)}~nus|>|qvxWG#f>8*oR;CGtoP;TjGREjBZ}Q_ znmJ>v83xR`p1@4__#-hBX0U-H4f0|SHC^GR1ufC9hg%8^^CCZ3q0)fhWVKo^+Puy@@v$&@kZt+@t^IU-Y*&k z1bW~+7gcMPw!0((`z+b-kKTLOq|ZAx(x3jktw)MIDeR+0Y(#92vC^ARbbs{t7XGz@ zCBl!xk7Bhb0>!cZ9MVKLaqL&%{PZOWIA+E=5CuMdr^D3fVaP<8@&@P_4x8~W$%0n6 zy_}A(h_BX@AY4DDM{;E>gu(MNlOM-B`m$>nyyW{USN&K9l9u4^Nbbx^r?Zh}IeW*j z^2K=oe^Za1A&sWp;LB~uZhY_42fJ|luwaeN_Kv|VH(jyZqgKc%;V=)B4KfCZ}b{!N8g7=&F$`vRoerzS7yI-@`5werHrYUklmn1+Wo`h_O!W zcL#}QJB^{eKbxH%f|qTaLiV3Fc;sBYiX!d_SBTy5k7kn6OiUOCS79r$k)uN|3G%yu z@4}S2AhScNEP@dgDpvP%zjZul2%*ONkd)4=+IC3b!oIFrrZnni2$n)?4Q|=z=I|a} z6dfFVrQ;;UZVml;fRQ-lr8*ie+OCo6nk8tpxvEXl`a#i7zXPjcV1~XkAnN6;R`>D2 z3>Tn%Q<_N&-gkdR1(%mrfy|QF%*-7!jn8G~TwB58<<%Egvcv^m^Jt+x9H))-Z~0E`1TS_7FNC6qiav<4Vq(aF#df|V2jcr(YTkLD`Drc2T(R1g zum?*!(Agdhj`I!?swUYmFJOJgU#Ptc+YqLj2t6l1;2z%7m@+<;oFC8bVlR`=z-}O? zeQXszsuqrYTlsdgt_!(M6)}XX2B*!sxA=2U_{bZ2(>_Fnb9SIkaD>kydp~caCt@C% z>{u_4tB7Tj<_OWbuOAkSuYdP5$a}BikB0C@W(zUD;?j#;Mmra^j2d`R6#g}Y9lJ&J zmRzff$@?-QRmUyUJHQawImXwI3}yIwaiws-Y@+~1K=bWepJFam6X3z(Qlh<7@(N-E z-km2efwkOB_sJ}@c!d0)wRd-p$GB43DRafv64;{&$1lksKJ0`vmx}SZT0mN}Im0*wo=&*fbI9m;wwGCMw-R8d$L=Shb%pB{6}m{b9LV(ab)MNe?M& zlL>wqr04s1JJg_up+~#~%^K16gx&=+8vV0{Ft-sPHH2mhEXC1r;#Xj))G~C%e>f3; zXP$d(6i9zfkGbEUfvtZAvAmgdma?Mri4bZC1ax28&+{K` zMol40c>_6nl5#O)&n5Ol=!LNOH;1PX|E0vqMU6c-w+hV@BK&wy6Z7~%Y&Ku4ngq45 zf69}ZgIYn#S=rN{e{|Ig^Uex)K!5Ler;lbgtvRg}p8S{YMnwUHbM4#2r2S6LetUuy z4;$vzH;zduXr%afz&Oj0A=S1%FAtTrgwZdMYa5O+UkHs8FNzzhh!uBLqZc_(TuXW) ztGpYg4^|nEwl+THPeTO-=BBi-pu%M4EBT?A4B`7dQLsOsTT96`9_JEixIT$FNPiD1 z3&J8J5`L~sJG0&aC`X%rg{-`7@6fxs12aj=MQ_>oJR@lW;xUor~od!J||2G4@Q;CM9XGb@GB$ zt4T`{uwzLg6mT_7PBJU29fCDFVsWn@=N68Z_~|$$-eoIWooMj|JgxCN_kdpQyJ{3B zV?y>w`~~goM+|WUKQwfFm16YoWB4i{JjxZ2D&0uUsJ^tJj7HOYSl7mDROj!4DDfLh z${VuzAL<4Zo~>nV?NK+{cSHaL>MS&rB$&H~g*R%!5zO8e3Liy*TAK`e zFp@k=2$$sr%U^eirO^pXq52NV413F7gWi%&Fd*YU!(TomCTT2>_Re|Z+S~4Dq#(u9 zmT&Ckd;ih-6az111+9z~$B^?>(4~W(VJ#g_(+c+I4CNb# zn)$>Uq{>8WVXxmT7g<&s{d|&R z0eCw>V9}%*8i|11A#?bO23>iS=}k69 z)v z+Oc)X6~lWoPVU9^ zmoV;mg5m)aQ^kYiqq+J9^;E&F89z@d#MUn_Ngn9$ z+uDE037QZn8YAwd?w01SOL*l=8r~ngw)^xKj*0X<5;JEHj~tV-BU6z}qHGo@Q5qG4 zc=YbmplcS?HfXN?A?3K@DdD5T0o<1-T|!%mNaL0()4<4&nTXjh4zcXqu&#f7EoA6P zF2j{v=+EMivP-O8rTINEO$-U7JBz6`kUEg!r|%WpAoIxhe5c7bSxpnxelr?-#qA}7 zHSfqfgR7P$(VTA3(a$6fPPx8q8Xw8*+cZ}%Y5ogl#fAt*I&LctoLruvizd+s$g}fb zxRiHf3Y2~=Xel8YA;n+CcESnWI+l`*27>)*-xcgryZ%0XEYB^QS6f-B)gEM0g=Ntj znPGdAac$J2?5W0Zl2ElNW2|+E-E&3~Zhdh@^>MY%W4|0M=*WaT8zgegMa$}OzBqvm*Pd_@>M3! z9_aGt+2dj*e)?SJc1&qK=;_TUL@@h9FfrWq+`Q|Q5TiYs*b67sZHtM^)&%*oA(JFQ z%6khXx`NnvbJy%5QSR=u7WFSrW>r@@rCo*2$o7Oqf)^q`oJklMpce zS0RPcQZmNy@A%k-*uG1}wBKi^97#BaQm<_+(AZ0%oEJ+Jcvmtketo20bo(R`&9xWk zF=9TZ+l2@r=^aZ(;9k`F5NE`66~6WUF#R+e%j@=z>Pima*2fz|ak?(pJ$lQ*-qKoJ zwTC*n8z;ihdG&rEHoC7pvrhUBun^L|ZBX{D_I7X}T^<`S^a+Z()p)(z@_wP()ZNol zc2%ADLS-5V9(jWWNoHW8Ir`Vbak~E1UnP$~X`Dhvz|MUQkc4S3i zf7MYH&@f1%V;jgD?htx;*oqF{9GG|P1j3KS)d@3-;qo^f|@K% zGr5@p4P!2^H|wJ1J$(%+fa-~|esQVgB}Eq$tnqTsyfcYcD+JRGP7>1EyRWF;=sv1fX0K>sNXNE}#$0HXi0EAbWfwoSj>m^{9yJF0=F? zb^MRQI${t5S=d*F6|H?$_?*v1F+ql{BE7f{?9JJtuddXlqvsyek*1*V9@}0|fv+MM z5&%^@g#J>j#bqqF;a*!G8NSEhhitWX0$*9l6zF^?T9$)dbyXHqrXx9-gFSp;4$u|< zNh{xuI)A{poi{%})`c7MTU&Zo0gdfSEz|f@=7bE|%#S%&->T2B_!sbw`XU@+x5%&Y zYmSt*dby@vAum@?N!|Dy9UOtDyFSZ#=p$n>RMlQezKu~sOm6u7AXWNK zVcHfu8@`p;-rb3{baS5SCXGHDh>MA#)=Nm6?3dh*>Enm2Kb!H_dsn(VKVQFAowaE`U3|TLR?yeq{JMX+Kllvwwesxni)TvWZn<`P?}aHbDAC$k zpX#`L*?|?iM(uV|MW{*nWQO@(<~d|qW2zlORB^2OJBm9<3uVGjff4T-e$b(K!T=S; zimAqws@k=NOpBGR9SeMBmZn~>gjc6+v_Uj;E}Fg8rOu4=7cOd_x+`-oz=@wTJI$wM zpT}*xeLeFpoa10|zyZ`ysHL($CZ0Ra$@1xPz27Li*;mPiJcafgBxz3>>!A47D2vEr z)4l#9MaB(jYGj;+hU0gA^vigBVYRgp)+x`;waVW-qpqGV2)XkOdbSht^Y+JfhvOv; zRw}0mUif(WOdic_oORU=a^RjOmv?!V#SZq5jqP!ceSvrFZyq{Vc7c7@5jA>v(Syg+ z3mHMAHYNdV6}}5wN*D=#+WVbJ1$f}R+YR6I6MfKDSn{ym+r-Nq!+}Y@=%udbW(dLF z$$CnbG?e%Zs60a?{m!ih@Pk#jfx+~cD)m<+hN+NH9hkU{RBwS-RAV;a26%Dik>K&J zY`fgfssB`6IPFH!t$afI!6yTvmX_41c1|pGk0k|n! zHv3+G#<=dPIBg%Xl*weCWXK*8?5J|Rs+f)qnM5n>KY6+`qh9?uJUz~%5*=zztB*H> zMZb}~-{NG-HnAKXq1*9WCgP*KA+3ypFr%{Np91ud^qV0cZigTX(0xA=R0G_sl`nxL z7KIeI3QBjeyA#onxM@hoUNr#o5mm;5H*AuUS1iA(4U_d+#Wft4wBsgHzyjeJ#;sRs zTub?3+06wf5aH3ci#)+Y%2I$^!98tWgXZ`qpTn~8^vg{ttsoIn9~9I4MrZb70a*GG zFmMWV(@{_9(~1>iEsq8iJs-CSQ;Swwr&%YasH|Y2HLnM#kp)Wozpc%o74d1M;&?Vf z+3y4R{9e!A9}C=Tk>DG;H!y{0fnP|qtZ41$K@h#4r2d16vg~%fV}RlmSjNQzYgA6C zBs%k-LsCFeJx|Zr86<@Jlk$1}_%4t`e*~{mV(2ODaG;3FyDs36t<%I3wQ}%W^X!`t z@0{;-5qq!duO2~Yve7bVv^FaUTYvW+o3>XmjQ_g^V-tIOP9mE(hYfb<7$7e!w`Xmy z01#+9+9P~b!W1p|@qqb_DT<(Zr0>>NS2-e~^0#D^g)rnj7rDAB@G=UwZg)M_b4o}& zZGl9reM&#r+<8=`gk4HIj~pen`X^TZ2Vx(5&HSR4E(RdhbQR~?1~GGsS|_uuMo4q`4xmn>_kZIC>;oVJLA*=d_eST=W$vSSYY|CHyvpM1JGjpH|AZwh$d2 zyYatp=Ar{}O5Rl!D`WcWe`F04#7R%RFs;b2z2bf1K`D!5U+X>^_`nDL3@#$#57rJj zj;9~pRRTSJ)6pdCaB{69rB*`&B@Brs^ZH#f;`1JDV53BJL#^8MWKsx{TG@8x?}W9| z0g~p9wf>o+5ELfGAS|{lVi6U)qgB5pZ7e10Q}O5$Ovd91e|#j(C269s>I78Lzhyjh z@Ltm96!`dTKXVdmlPJ|9{lqe*k0jX;Luek5YBWoF*VNMiDv+`vS8wzI%@jpcFK&S{tDoP9+fnu?+Skbonq|!xVb@8o@n}BN&?9%^Rl4z+3WB>&Pa^22Xe@I; zRCihw(U0yTP6T#+Q@40K) z9?Umxiy9g7>^?8dkqF{SyQ1lxk7GP^Fe}(j;E=;BDzKM$z4B|vHfZe!ji-p_%*bE3 z7C>r&3PM_R6yP#0d16298?zis!WPLwMf|p&pKXFf7bIa7F%66nUuBQ88UVfL z-;d13`oDFD`9kDt(2;GRlqn4the2~Sj*$%*0p&eZ**=DO7(BxV9b0Fkf>7a011KfkL+J!)G~ZS2*}n~x z5yfl^B0u0V1YHQ-S8wlelg4gl(lePQ0U3RqgC>~RLKp=bWchPQM%MZOK!SQYKU0mg z(tWIvaI(r?dNuNww?j)iD?|11F0g@Q8$Dj-3^Z3H5<`H5`M|mUNAKkSR-|A)2(HgZ z=ykVFi$G)G{0k>$XU0{Lx8$V4E&Bb2+;9XP%-e840ybx)&wM*PvPAWgs%bhdio=icRjXHv8egzsvDv_No#->YnzAfS%LthCbRZdjp0;7PA#;Pkz5Hoao9HK7=uF zF|hVheRugfV9x!XG%ZyPDA2~3E7hn%<61xrP|x)B9dRY1cRy2FG8VYfKze$upAGQ3 z)!P_*!fmKs1XEHX3~7IY8|VRfdbF@q@tMfv_&fF`PMEgcaCuf_{MKHU=K{ib@hWe~ z4PMe=)eQca*zHKqG!FM>CpOu>&C{)JrmKN#?1Xsv@rlbj1Rm0NcHqGv+*dDFrH^OF z%`k)!!$AGz>^#f;%I{nW1eH~=)Bu)(NNo95divA&SPqZKQ^Z6zn;(>1i90^XPJ6%R zg!|uh{dC8_Y@&;+=8B7?q}hjOumpY}fR;YoU{H36@|`J6xDTGH3dd)qia%LO&R1 zmRl2dQ4QoB?aU=J)R)i#Y$c0KA&c+q_}dN&NHaMbmB6e#M*A0T_kNpv@2Wi+E4PNU zM!JMN$YG(w@d6?qGajl?&uY}F_a^Aw-Q`OygeGh8u)@;iLK}{6CJk^PZ$?NoLa&^; z{t@w~44w_je&WaK<77sv(_##)`r9jr~bvkQ@W*R(6HZo z{~k|m8SE4jGO>t7ztFSDpglWXWTJOMou)gOgK-S1#g05k>_Z>k$NVUoTQzo zI4Xei4Qw_-!TrEI*ssH@tZhHTtqQ_JOXH{)F2ln998H~dc#KNhMP+DcvH1WCX2(Ni z^<%?EzB_pzS7n|-oVH-?r@`l&5R@H57T*PcP{G5n9jtj% z=#50UJ`sB%3yS|H?``6(aj0~V{xhYR0-Qp0fAlU^|MHjjN)8quzZntk2Q4k3e0?3B z4>6{ThNMCoDL?=xNdnv9i}Zq<`%OYq#SQ)|>DlhD6q>TMoW={vmjzVv{;tRo#?v) z7ge{62p9dmepkU}52Qq#7o-Z`8PlC^0xL{Og`IDt1AN=<5j4?5V7A)Kv9k?P`bP}V zO?N9nqruoWlIP91p~T^oA&9{q%U)EIY^#Ir!1B({Jr2Rlk2h}L@`y@AICn-CqiTh9Lnli+_>*cBo}PGX zGES7cEnbAh8vOavNJ7D+R7y)g>l<*sOI_-_N&|RGU5}kq*73ZnOIf zhnw`QXbMPMok+-zX1yB<cLj43P6Q%aUW(?9 zXQDXjrxW|TPwHn&LCQA3M%30KZU<`YPaXm4^UdU-607+6Pe1tgPB$2hR6tp>#Q8z< zzBN@BBN>!IK$%lkKdcHI{`Js&CKs(HjcxI(nt@(Aeqx+fb2n9s?UYOMkkWVGbevParHH0X4kzgz9&6ba1w5K`|3Tywupgv}!N7K~C*l zfmoNLUFW-oC!N_-V=tgP3s9x!hq%v3{tE=}Yg2BsYO0_k<4Hl`^sZOKr~D|Zf8pYE z#T$v=ki}r&;KpAhiJivzONqvilYnnfcEUTe7sg(+lvR2PLZEuGEagT9Y%p#FJq1tf zNpIu(Iztk_emhg&R+4YAN}h$Kw^V`z*|7K!rrHH4G7ZwWHp#o5+3DXsr-Avtv!x{< zblRK6qCxtTI<*OTy={e7cZT4D8*H(#@B4galyjW9Aq!iqkK`O(b*B7x?RDnHST2>m zdqU7(xS0f|e_>Vr1F`a-m+uce>*J|aMb@-q!0Y6{Vy%u>*Nuuz@J%_#Fb9+{o;lUQ zFOeImkITXtxOJM{$9Q}t=cV9X6kWI9JsNLwwKPn%e~l6a5t1mUje_SE%(USuo3992 zerPM(1BJVLhwl6wY&&PO9^Wp}IHYB0u%ZR_O5DM4$6|)VCJh~lB?E|sOm0$t?1qHz zZc1so2Yw-jVD{O)Q*U~A^(L{rjIs`S6iXSic^Tn5!83K>nJq(}Q0lA#d}nzSw0)sH zuQuggCUafBR(=bH1$n*{6+Ip4hFXiplsCuj87xR)z3COEptApv!SdoJk>0{l@usQ% ziZCclxqX?WEZno!2H8_&SY{30Ic3St-d^*aYv=lGp4YkTscsM(cX2;gQl1c}qO{3J zoJ^@1tYddaf|mfV`!+Z_A3f@%V%_+rw3{2mVa;hyAICa=IMb>lfcALv= z$ulzy%T7jMUls*(icB8ErxI)2O%RF4cpc7lIRlcuDD}$PQA4Un)*_2OGMVpo>qC|} z4GU4-6v2xO>RAEqrP#Vx=1{}@uP833PXZ#!C8)c{L&vGkEADPtQG&!3H+2t*UEfozy(oE?)_g5&@5Dktd0p*-;OUpO35 z$pM@_|5QyZsd>O^gC-hRqts^(uDwViDp=Tms2!Gie4e%;T=y8!cy+e3x1f}?m!Peq z_{~Pj8@K$sl2E8TsaRc|%m&ViuKKoIU20_iJ(Pj5@AX|UgVo?M!yzjL5#u0Y$a&&E zmJO5OypN6ys8f`Nn=P{dlV(tsR5rQs*xiY&AwR1zyov8eV&sIHIp!;Y%GO%TL{M}B zk)oEK?OQ#Nr&XpwY)?&7I|A~z7{-+ZBX$OO)MbNf#aG^7rI@h+3GLKY!d^nAZzDT{jK1DV4 z5zNTk=+VIEG;<(f3D~+wfN+xLU@|_Cz~Dd_phSde?<8+^6l#X?-bJo zVzsGC>l#u;UIUCAcUF&ItqbD&|3}0i2uokk%N>l;j2jCM0shS!1VlunH;8ZkMUw`j zG{d6A<}k()L;L_#rJ!OrK~ixE{_-htnws-_Eu`oCUuod)o1t$*H#L6>EE_(y8*mTo)g#w!5_lo1oseYH%|14h{vFt0-1iafTqp7u&cNvg|VEAc2m@B24 zM|sdMbnV3w(Vn_PpOQkA?z&IfI<@dqWi+za zGVR=WT8ijkoF$6&v};is$+^*j=WIA0>h~1O+}$n?EwQ9=;Sh9Pi+?Ql=M`g7v9Z+; z)oq3id%;8}-*LR~jP_@T`K(p9)1~3da8pdvMB?~|_7JAie{4kRr5czAozbb@4-a){ z668@_TTI_YYf8Yax=N)dP<3X*@A8Q61*i0y3sIjAi*v*aq7%>~A#>%qrmB{dev{KM zV1kYH+npI{mcouRQh$S6XKmsfx>t2JL5|Q$>ngm?2`9OM!Cl&8zEV%h{1yDL=ov+mgg#lAxxU%5B57u) zC<+oWt++`VLGr8GKY;LWnHM*Yb@RDq1N$j{cjTS4saY@3co5E6feJme|1>aFWn~f? zeNo1os~B^gsKjcM{7s#}aQ3r2>A^HbN_#67OOZ+o;-1^D`L5q<6XXTkoxOxzx_xfv z^&+dYnoc&rvrG$74n$ISwOK4;ls&v-Uv68}O5rY?y4HQjyrjS6#Fk*rL&yZMLM;nu zsWPOLo)&YX#z0kpgG{`D93<088p~wL^cFk?PB%+NU6~4)qr4er-$Xhiu`DC^Kxw&v zkfoUK?9}?(1|8p8MUf4H%C&er&2JFBl)CKT%(%XneaA(+>a5&)Sq-)FXl0t$gjg

Rb%kFMJKJFu^C!>ov=NdV`TQnnV@MlBzm20&Cd}^?RL2eEwEn!Q7Gpp9IcE2Yw>j}?BQfwz+v_YSk z>UeNj!Xg)ir_v9rbE+>N=B|o6Fx0hc(OQmiMfqkRfRp5vuxH@Jvfu-RDx%c2X;x{s%BIVZ1o5&_&jp`wG=CzXf)be2b>rq!V!=#v3hb$omDCq=r*hF zj4yg3m59%W%oz@WXd&q3{^;fH*gbVrb#e!F+>QRGgPvla`{SU!S{SG!{G51Rk9LTh^2^`Eg)=9U>O#7L>fwYkJ zyuOO&upc3JaBRTiu)FewrK89lP-s$ssu|Fcc)O{(cq)%My8?_|)RLL+$X!JjBps}d z<6&010~%Cv*he-abjIm$zr^R?a=*kjd0x&M{)M{`_=m_73l9DbJQ5-bG721w$rJX1 zheN=6gH6eyYJx*8W{imRf!*ax5*`JWiW&qL$jKG_qvw-zV(r$yx;}*e==y%S8DF&e zzX36i$OF~JQ|oE}-wNks=wnJq_(`kla&5V#$v*Hk)EawHNvrRJmF&pG&AyjJiJW(Y zt6goa@qWmOei-QtRrbx>LtR&PUlsSH@7}sJBs*e5Sub%5;l(*MJNwr`CGR~lxSo2q z%guKFqLl`jtv|MKNd48c#{Oy0irDDSAv?nvY5oJlZ98(H?kSlpmuR%#{V}>gnL_6OK zY1KhQfcruYHo*6#3DEaV7A11t zzV_t^1Aw(MUNZ$iiGF~8_Xo#{7`U@!K+CPruo$zdv~*p(Os1Qom=*}(7r*b42mgN`p0uH3iM8$=@-0i z$}nhqF^QLM&Klt$Y`<=r6TH0i{6oH+c$6#Nx^Y~=uXiP}3`Fx~Sn_*S?1gv~hAFK` zsJ>(CJFqnFGS)yK$s6!9!#8zCic{>LVS053e0%~QtS7?w; z>h(d3MNXnoJUqV&iO5CYBA{lv0s866*t_E4oDY_*k))(EfXb{9{Wzd(^Z>Pdg`gf@Jk_9R-(C4mDlH#qu*3s$Cw;0uAAk z#PV90E$!gY@vuougujRN)w^Kl>dP>Ujn<-fe1tMJzInTc!=zR!K?pPE7#_7!bgX*H zvEBemh#(DI6@oIQ)>}6GI)yR`cmzu9Ups*STf*Qp$0~KO2YpR^ZgiS0(tS)C)4(7< z+W0#?>~LI$=uXiBgg_irci#fA15W>s>0!Q_-pm1DqRA^S_xFmo0CW723ezPF;}74_ z11RQ<>1*MK6|m<_r;OF$yKbUYHuHR=l%GoyOJz}`nKrYI&Xht_LR3sB*1>!wuj<1Z zY$h<|eDA%YG1ZMRTrefaa>-9#x=gBFtvAC47!N%Lsa1^~LV80_2 zVivVa5lU3-net2~S3SnT0x222a!MXXE-q(18}^(F+&``$S-9s7veUTD ziZE(qocs^Ttm?kW>Xzzxme7aLVU2a&nQ)np0As34RUh=#3Q6DzC=n#(PjiXK5bh-D zc#PM&S(uCQCL~%Mp+TN_1ny)~$1l)vzyC2n3A?~85fmPn2u~7j$npRH+reY26c1=%4H!z~Q*sQnPA7Yb2yQnJlaXy0d ze&{1xwv3Dz`^7cax_T?(^FW>5X2 z`m5>=GRLZC?EmJSuKawV97;~4zG>`eW|g->V6fTsYla84_G;b zXP#zWAy7e3!X7Vf6CACt+Jnj@7d@bl8}`%#r3?}NT54#!S*(egD^8lhaDr}PG;AV# zuIkQ5b)YT2{mqyThiR4Ua|_~~tXAyE)#9`O6*VyR?>)b zbw@P_l-fs2e7lelUPfOd;lf7tF4sqsf9Qq~07Y?OB-;q&Gv!M|Ua zMuhMp$ateO1yZm z)RuOuyv0ib-N4#&*u11}|4Bjawt5qEy0m{Q6h5C9?EaAZ$}cDC{9slj#PfZnOt+)d%hVR@_77Z7P|%EG)}` z*s~wWIbIS?Z}JCVHDX0OaRQ}AOEuc%Wi>qTvdNg93n3qUGZUulDAC8+6!-U~6AG8@ z54C2y0ivBBI|_yE$to35L>qXM9vOF=EJX5#9Wln-J5q+6`WTU0*+DRkm{60fYxFFN|ugMMjPe>K+q{r}C$cyR}{;mxKfGja09fu615A z{ycL3ZXdFG9#I4Ed9;J($S6wGe5HQneM5&X#++s-#c-wQ1dnGEcwXldzx|3}az`(`Xq~+>SeHz0TiTVhl3`TiUkDYf8>fw-73%&~=A|e(9-FJ&y zef8T`N*+<6LUsbF>G65gtIoHf_`q5fTQ`rg(I??jgH>Su8k;Yy5dI+O63M|2vUqHZ zcu%`tG#f^3m(RyZ7lCa0tqV_$gXbXMrl9AN@<2`Er1`68k)Vj}*q_(Zy$olf>b%Y} zTz{OUD)(-=XkOAcxKsoD+%B8t=5*^0xatuEOUn9vHVyDN-iJ-mB$rUebt>zjiF|Js zg$Yh>ofR+fqz--&u{uH%=N!; zf8nxNXbc0mbSXD?4DccL#M-BoT{VBIa;GH76UL-96!XakbYHR}M1__bq;Y|m_5)hw z`&gw_NbG#^_RDB1qwqohR3n)&8^9CrukYK0A`^4a2*V(Z2jCDOzr`PV^#!n1JXToM zPotssDOOFPUWo9~x!(B`0EcHSk&IfBbd-@bZ3_ zt<@!4vP9uvxwUYnreX!t`?T6{1c|5nczv>sS6>HKRQxaM=!&wkUn>o;{^5_o9wg`7 zApZHV82uy{hxd=}G~hP9|4W<5^OCSf)<5lPvR=iHZSEQ=YhBBn1aV_j+jPt$lYi14 zVb#Evc~K{Fd)neA2xNcjOYtWtJ>hmmLKNKS$2%RriqggOk|o8X-KdE)kU zW9FjM`RT&jFuv7BP=1g&!!Y~j>C}OKLr#$n+E|SuMZ?EtNs!>~%V<-yoKo`)ES`$` zP65pCfc#0PDIyucnP+<}N2W17)aN}@S42G8FnkM;BKem&ZMID(R-)*SdHm#O7Y#w# z-_s#w!bX4LXyek$7sHpjvypaa;I)@+UMjvlWBmW+|Fi`#lzTh$zsuF&?Y)9N6OeUR z7{q;x`Star$faXmkJrhU!$Y1~G@6;l_>aXTnk%%`Rlk*0KZ8|1gLW)=cjW&|ihaaZ zy%+oL+1{x?70Cu=&RqK@Ox;<3pg%Va^UBu3o4hl`JDi$-56lgBCayS zeh47ZF>541c#N-PCuc~J#^Hvw^^|YBFv^PW3eG8Cc*XsM%d+(p^d_~<&>Xve$GSAi zq$$IIwDgk9HCk*3)Fe*gxEEvHNxtMoBtT11h1(H_CKE zF$()7Y0J5%7Rq!K>#2ky*WPgG4|pHNO}s1xefDSE+Kgn_oM`WRc{&Qh>)>m`gHjSE zJ;WMLYKZ3E^LM2y!zf~Sq}MN6kLxIe5KV9F94D*{Jy zu^3fM7jA_*Ws(#3CEFiIUI!A2wl=in)FX4{f)quXbP?&jHxUH`(mNrcN$*vfR0Tl^y?2xj38B}7 zBGRM;2oRbOkQzdjDhRwc@Au8UbMGH;f4DPuCNnwvv^nSOv(H}7TFf?KaN zZLS-Y?%_25PV#Br6zwIOs`JNB7Jqk#B4vDeSBB5v2xvYx<84ps>?e#8I0s8}e-k-v z5#mL**M~;^TR?zwdw@90TshhNk1tVz3oTv%qmSQLtTLAF6FhzNp>~Mn(HijWwe3cd zB!~6UjI-=7@Ru&1qv7mk4Vlh4`F2>~f`DupX5@w3^Eed!%#D+AliKQ+&h7dF%a_hX zFk{4s({pp2D|60uQ*3FBy7~P6JQ_zx^mFsG;uJ&}k3AyZnpYd44N%Ye!zXDd8A3LP zij%bNYdy5~HO~!2LwiGhG&W!_>HYw)qCrBQa zo{1jYn?A=4*5^I!T>NRoc!a<#knU8$zcLRskie17ozvAVy%cHo?xoH)Gdeu4nyV$P zOf}+N1Xna?tyL6;ckf5f3`3^8c&b`%HzQU%XNr8c>VpQ82Mr;BDaM-&4sycy#IYbQ}0OmA-q1KDg;8Rp?%oqNufZE36k=o zhn`s+!(4IG&sS!f%;>i{u2&?OB(qZyfuH!BZ4C55U;i@IEtk{;CWlF6(X{ni@!&Or zSOYhPm%aUtOJ*##P{JodrD4Q*<1Acrkt?oyuUNz0G zZ>w~eips0moG;ug^Q#z3_wb2^zXqmZX7Wy{>q(2qrCNNIriDey-}K9GK69(XQ!~q= z1n!W>)Mdg-tJ$9ick|tglE#!vqjz2^ebQfGNgTPRV0G}zgS_Jv>_71hVZ3M%HR*!8 zL2Lu$WxX$7roSA+=%H8}yl7|l48*=gl}w)L9L_rUg=b z+bwh+1yyMS`6$v!hHr1&B2!#5^|}Mz`>1nK9^3FaLb=q|ecq&A_I|9v;bm!}{r826 zad?WHBCVx+*NOO@i5%aN3=8N6P@on}HU5pi92?N3si7wsd@VR%wwM+O5A4%f9e>`9 zlU`dd>7O$D6`^r0`Cw(YkV1!D&6PI1>f3o}>h?_}rG5edpmO`kZhtyf;O7obA$;z2 zCB7T;0QK&yE&^t-(_SgEC->H=X0FM9)Qc28-CoZULFIl=UQ1LHVkAsCRE#m^!8EqNdseX7+wbN8iSOXi#KC}8zQAjW^S z!s#@}>rQo#BZU|!a+KjsBT2T<|BHIVcG7I z-6yPX)55rk69>xGeKK3CXBh%PIlE!SyBUCwN#i|Rs7@Xhz|uH$#0_8l?9wxA4(zkz zM!TA5OAbg-km}8pIu6C&lsH{Q^v|?Vw0cf48jgD;d(j%k7v8LOZdo4KzW4X}g^0w` z;MYAimdh#MvYY7L_ls0A@Bx3;JS}X7`H7s>y^y`5luoGe1r*z?9Tj>bTe$PPpORLM zifO~YK`lU&3f#`e6TNUT*~ZI@x;dVIC1#5vk0BN{H3bP1_W}DGipf4nB7O*+u}~wO z+!x(Us1SV3#O5m>kOlT3fRYhklI2v;U+e@^Ghd>Xz8jHd({}$YN*xLc!-V2TgXE4O zW7RDyxFSI(=rZ)n{0)KX_uo6h%sGm&z@r{^%#CxPN& zR4oMcxuS63w%o!jaE?a7c0l!K@FT49)0&bQP~V$pg8m!^5vh%*6{k!f&$Xb9;#*)5^Q5}B(0OOypDVS8fiYs}r>cRWB{`}C%c`!9Bp9&(Q5 z2ZMoGQ!^Di{}A0Y0XXhqrh2)PoiM+Erg2jGO|s^<96T6NIo&48jn~Fq$ZIwEIWLQq z<}pUGGMWkhNDEpNt!sS^D=k&=gWP;ev4Yz&M}X(B2ci8_@ifFDS0L+-^JAz@(<@q| zGK*k!j$Hm-1w`YlYgan0;VG8F;@8`I22#9_hI=y%7`3jqI%(P%i!9}b@w9h~C@=+X z!X%-WCUcIQ?9st*-$rP*Q}%I9B=Tk~-Vd(Y9S!HcDq!5*@NUp`JbluS`76K3&DqaA zn-(#dzq{rAg?RlB@*kqDH{aw^-V7BkgY1ukBsRadSJt=-=0Y}$6TS}%;c{1(%;S7K zh5O$N_iI(!XAVCNG_Z{Q#j&BsbaQ{|`c5O(b&5VQpWeuTEtr7`qqLtu_x&uMfq&X& zTl*Pb^J7e)fih*pSAtwQaO7K$-PfIoq~b18_FHM2KWerBLWAqDr_nh~5ifq-9HhEG zW9AB3r1{nFP{N({KA2UD*OZ2`HfugyhzyZqa2NsGi%T*O6OFt5yxcfcYQ&l7x4|YM zcVF-IGWP^5M3;gt>@@ z`=6gAnaH-IdqgR}RA~Q0)TdHvUaMJkj{(V&w&bP$p)#l&V^MAe)SL4@WgO9x!;QZ@ zeX?V+da0zqWYVNlKDd(=#cKLyhHawft;(K65S$a1(#H3+l`P)ibK0-mk3uwU7NG&o zwH7Q3`MAhiJ&n9Ldl~OWsRkn#Hdy;_dokF<<{yqKYz*qzHLF} zJWUsfpqzEeD&>RN9vHib?yw>m{e^dJkrYU6F7J{txZlBrEU>b0X>kJcLk``g86s88 zy+H-ixg(p^5XnO`lU!$7coGn2&B=n!E1UYpAj4-)F*<}ST3qBB%|Xze_s7{mvaV&i zd_($i0QZK0Z|>A8C%Bd3#!s0z&VzvDrH0k=CbK8g$X}0PdQA~TO39pYkKJ`T4nRo~Yoov84Ldb6a<;`~ zq9}8LT|nkxPit1NbV5nZp1G7k|3`-=K4j67kexfAKstZdU7QvjJAZ5bg2~tO6Bzdm zY)UaF%YY);tWF8K(`W%xDum1!vdFy33gHNxkxFYbgUzLmZKWeQm}K=CEk`o z34mH^$#L`0U%WTjY#O)PzuI<6s3qTgoHqRrQ3id@2<@#ptW$9!>Gv})+{-(lT-AUf z9p$&Z#_N04)X}bI{m1BMccNG)TJr{f2K+Akfa7q|xDigzTX2ZpTLWSixu zrp{QE07q5{bRLW_!thKi;D;3&(OhDS_qHc6wL4C5(Vdwq)%I1r~71o zh{Bz4+|wi?`K1BqeMh);WYb^u!za0;=k|}Hn0b8Dbr2#F>^Lzo?Qi`i?#C?=CUOha zjz;20mPL1KY$A>r~aFhi-q$E*-qGVch}=l-+tIa!j<5?8a5c^rpr-T!F$ovpS<> z+O*bE3;!dx`)C+qj&(Bt|3gILa+XwLNk?k%ZgkKNCMbLvz4nW1M;d|ZLS4(;Q zkm-doyL&TPOc^Ef;n3NgaENXd%?Z8$EE*xKG(10}3~dgvy}&h3Nrky$7#Vo{o#Pvs z*g)@}otc`ot!g^Cv*oA3oT`$cFFSEOHF54FZiaaYYlic#S z;*-FMp}a^oYZ5h+P$OA3Dk}x|uJ)kEquK{ArYIzv1EdV$mYU>^tg)Ts@COTEd24A& zJ<-rHkC)}H%)dT_CNBm5DK}WS!_wYbMBL--Twa_GPYWDMM(2Qnq)(}4oqoGw-pw}{ zVQ}Lju1xN!Glmfg%~MqQWp6qk*!>YexmSuD*3oj@MW#v2Fo-jdj&!-76~&JP=Ns2| z6#RA(DroeeS$O&nQ4EW9WTx%taOG~x*sI^RIKN|nyUmB+NaLlkeYa^}Ue9QyEyp=i zgk4LSQ-Bll31mWUDKt3!?VyV@Dov}T4=7;4`@l4<;C(huL?@w=M&v}JNUHT$n{R8cj zD=C2V&)IuT+Xf>J`EOEk(~amd%QID1hBd4y);Ef$-p#k5hnCYjOw__I%u7t1J9e>x zr-VCdzT{^b%GrLB7&_mYHxW6-R#u{5i#L}}bxi)rL6~XCJypBbf9I9kFQl4(+)le7V@DBd=HRtz5N4oAX8}NrknG zOi3^kW94v$D?Y-6(loW5hC!r=f;Hpe@<5Y;mVIVJ_pxaeoJ%$dX}Pb&&g6P)Y{%qe z@x9kWADNO?3|Dw}X8z3m5_+5mBZ+~@@u4ke&pZXLA^2crt3rwCnwo`MJV0ihBuB(rgaDy`{}kL)eIPEeF8M{4H16BW5b6wk&XTm?(08z|LG%ao;b$5>sz zCnCUpj;Ds+%r>X83M^dV1j4!HKm{)!pME(o>nFl29|2W9xja>ufA)cEPvT%~%g(~W z95RSci3$z}^VRBQsg&Fikdx>p!W3Ag%goH+ooq}RwM>k zc)Q|*a8soLcG_6|PAE#%DsjGyx=t6FSWza?f zf5cD!$mG4<6!ue?X85+0FTSIHqx=-RG~MsoO#IZrlZ&Lhcs6KcN4*bGmGEdYg{45Fxf}e#Ufv7*_#TrE&Jq=NmuWDp08bd|cO`1#9uJ6T*n0eeVt&vx znj)XTGkHg2{`glCd_S?Siv9Si@LTPI%GZ z*F)$5afyl$=uV&}$T4?cwFhK1+K1k$>->@q??Q@fhbSY_w;Ym&984yndkkieYE^;- z?g)2GHH(`ImU%P`8RFrXcMnjJai%7)?hiBzZZJj|T0`{i5cv==;ZVAEVOlanOvhOO zf7+Td;!J*PXZ=D`?4!T8+BB(-{r>w55fBzMbR5VD0)7~JQ}t@kg63Y2(u{FAg>da1QgfBz?R%9HOXhOU zkzH^9A$swvwqm1%ZhQEsYE{djs=|rh0@O*7m~XiIql&JfV3n0TVBY4qviXaHX%!dxB(9#wspDIe3Et+pc0pIZ)3OD&zCGB(W((!Dz@Ms6iMn+ z3yOG}%sqY+xjXu1*n}0T8gXI&p0IRnCi@%nnuheI$a0I?!TAzvW z+}@{NljS>8cBdmf)L*A=0ai8kQ!@7H^a;htlI;~Hwz)T!vpB3_o#y+IkHF=xl_3ky zY=WH&=NDuBQyNVMntK2wWwmPBe}EH(8tc>ZRSlxt;ydb&`+@Oy1xVN{DUb{a4Zpxo zn$8(49iHXRK*^k3KV+~;HKMRH1l7Yszpce~t7D%|6IhyKw7UL?U1o#g#c56QnNJam zTsxb0F+|>aAPqwH*rWI=`I8@ z3vVy}^8#8){4c6w%LCz(L5HqfXDyIZxY9lS=D6g~CiTV2s|!my|k2vL&41c)b=2F9RLn$S}W$;hIx>9N+HSlRf^>5_hDcG}&Ydyz!b3T=sRC z890`^>C2iDP&6;ga1?4YFsJ!jxKn_^FD}Rq*jyv5j+XmKJ zmu96)s9hhVy!N!hS{E;&vDx9VRp9z_U7Cgz7;}Sb)7yf2??h1oVt9SqHPcJpqE8Vb$wP48G zy7AgbSEC8I)m&FFZ&TOV4u7d^CH8BpDj(AqTa`BLule5Zm_HVRUSi$)M$Zehf_)U< zBwdXuY1%d4NgQdLt61&i$&PAIu#rG{M@){8ac}t$!2>|I$vp6el~FQ$Qt<^T9FoT; za-l+1a-AXG_2v;*DrSix{-`~y#hCCqJV__tX>uLei~(>S;ATGL}^2F7yrwhoQ62Is5yUoPLJ5uZW5AY35E_$ zjpAh_=Z!Go^Jj7ls2NB>hWrc3VB=tVy7(LWFHpvar{wbw7;qGlg8GL&n2H7E_v}CD zOaIiN!LP3_1&sl1B4f)Cvfjr{NZC$Mr@CpaNk+*+WwzzQ@1&7e+H+QIW7Q`Hf2CQ~ zMMFLSV#~$%VUlsPp(X_@x$@ffN1AO0dbU|C-MpsrC%VtT0KP7F-Rffb9rexXgn^pU08~DjM#&OWvTy?#%VL{?xW=BVIs9Ok!{1t7NdX4I3(NPz*^Dv_i4=h;?YB2 zck?nWUOz_`MY5IZ{D5h86u6tjnPE+UeC(wRZ^@LlKB4Bvi=4!j`#MHuwOCmOQu~*R zOAyDhU^ce$9=`!#$C{M@XsCPkHh-!qlM^{tRN<(m2B9QfO$Dp5|x*M(~Z(kvIcw06A?Q%L@ep~kS(oYwfWGWI^ z2b_K6)U@0D#L)kBi9CBs<`7TwoQ39J4{75f@y1aWpSY%M zq{!8NY=Wvkf;K%gJ>+rOj&f$4dh_*zQTajP1Jp~UkMpCx;J+ZU48A90aq|$bH$Q}a zBPp^N4dxf;PV;I?($|?D6=w6a&_(_+J(&1aK^YFC@xRLOn`t(Km-z*%tCBf(3S4*z zG2A=qSAFb*l9mD~1s?j-YHu*+m3=LA;>(LkJe_@BoQ^EsWr(7|S>qk~6{fVD6jNpC ztD9MV_?7t?j>|{+iJsEZw5l-K2$st$*fqaCqJ0zB$sKgySg0DDW4j-t_^R)XyyW%+ z3JtM1gADd*&6Dy?OVQ$8nJ5~QCQU>C3JN|q@z9d3y@caq$E=;@{-12pIMA4lM6 zr{y;83?srtZH=HJZ)1~{oifmN%+&P`;fj<=mILr^2nCu!4+dxZr4!+>JlDHdU9d+n zL06fAkcE)1Eip%n#oKpvo<&1!;J4D0+)PY|jH%&+)7de=Gkx!lWfEZWiuQ>^ zVce;%LmqCBWCG6<{^c$eB@IQ6GAX#8@$GPI-@>-+{cin~H`Ai29i(HGiW@!h44y45 zUtZo%u6zVlI1!EqS^Jd7gPzAr4bHBh+%sApbD$`HAH7Z3{4Ykud|gd){JWXY!+6_Z z%wu(Z$uM8XG#2W7dM9`V>mOEpB?zhZF(8lzeJ7|%&PU}Op?x@AZFO!x4i|mNRV@>i z*)q&TKW1+g_=W0DB3Vh<9vz&v6aOzH7VCBT9A6eP2+(?^`w%p8?*hs}u%v$?^4xPi zR!;ldHhtko>{qjSBCB>I{M}`aY@i>Tqk+$S)NXq|VUiTt8V<^9i3nXIc!Mtlew=4M zl3=ujdXo1^Y)_m^8naqj3DJYja!ib)J>ImM8iz6etce@iF~H2@ubz>;U;5}4hmdI; zUVGr4Fbjs+*BlPZS{)bJinocFoAf>%SRYl`>emutX#0@glAX?=$Zwsziih?uBZ^9W z3u_*r&^k7GpuiyO?=+zW69C7a%FpIhN`e9rDh`e2Vh}xkE&9*RpSX;^_qY$Ahw-no zQZ?$aEp!)@kR+W5dPRIY5#T98OMkC~oBu=PD;OOEA?QCNJNZOph%2Gld-wg6_@hS! zCt7`gMY6q-Uw>Td=C5}gOnh+=laz#YtKD6;osB`p72dyYrr*xBbO_CLIoNx%gbY3? z!PAToHugiZ82T&8uT6ZX>1uT6m8VcO$=|nf%RXCGljz-oFP!oC*dhZf@G43oU!CKV z%;*;m?KyLb+fJ6CTTuiii3V?2+#>USkWt4ZYcRsIs+iS`YH&|+i9E*CX~zQ2G}8zk zuppspu)TQ=n@P;YP^PR+=Zrk{l#}+eFTm(2m)yPW6H=5)K(8G(?f#YajVL%jebyy;!Wxc$CJr{NLEPvpdcBpL{t8(wK%+k#&O?B%&F@xJB5;KqiV9{PEcimw% z&U9NCyjuop)PDOzCLsKNTt&`jr`SdxLf8h|UL9;2TMSW?+sABhmhh%B#e}pimC@eM z06%VGE#YFIM)kgyQ*6YqsrYES>uehg2^wlb-o#tP*Eu=_qvcet={jnMG@cyyzqWI6 z{>%}uVbJ$34jp(klSvgfE5Z{q%ow-c0yn5Xu>^W>MXl?kl_Zp5ESYEaj3W&xeaH>s z^Fj{6J8RE$@>)sM`lncriK%aN1mIjsteRP`gZ^g>i9ped$B#M%(7U226y@ugrMo zy>0Wyqs>W=qfMMryJAd8M8UU#>?;@PpXD6+`%g`~)|13AyA1Zm{YjE-leSP= zPx|8}`yEf1jTKKzok+EMaiv54DIenL-8$-JgvqX{J0&pqHcMMKlk2^+0B?XLuXL&#q_mh z1Sn!eT9?fgOd(kn^6Q|hMh#Y3yZUWRtMmP$T|$GWNCm{`?VA9k)xG~B-Fu*f`uG52 zRA{c;aO7}^bnQT$P4gxqhT|e2t~<9eI1z3I6I5@05n$V9zbbu28#p>ByjV-G|BQI` z)pu!!2X5p8+T_sa%a>$xl~X_HVJAgRQmF9euntUau!p#X;k@2MQt#`Gau?=Ycv8O} zj|ceP#|_u_dGdS|?G5#J>;jNK)^f*#c_Ty_?47h)3!6L`zF2D1a4?0q-L}3oHw;-u zr8W^*`#S7RHEJgePKJTK%ONpy=pUJz!Q(R(*ajm9mC61W3ZZj6nMt4-aY_x0)CT{0 zeh-uj>f>@<^@7B7k{s#m=8)FoB&0F54H}Z)b&_dU;v+%*(Xb{@jH#1sm76#;Y998V zkov@sbL=+?-pSjy*TI#C_TL%a3H(De+${6yXB*d_s;{!;&AA6FWqf4?D}#%D8eN;otUmKBb4v z8eTT=)pkC@3wGJw6_XjrXVZJS@+<7jl@QWDp~(nYah?C0SDl!UZ$n%19Cn`}EHL)}9F?Q}(AHSlu9o;=KLdiXIHu+hVPr_x6_`qh=m-tHxvY;} zmK-WGDj#2g^GtA)41@(W!U7u0oPzY-!u-MmS~ zvQ);RN}T27c$_vs_nt!$s`#RLD`Dr=!+Z}mboo846sfFBgK@1=z&nyc0~(q^Y6ZKv z&JX(()NYO_7G*HJln{w((0*~Q(csrBMvTn@Hpca+Oaq+0B}rJE@| zEd{PicP18@SU<36ysLs_PN@2?2czlZZ64a%x?#3W+-Zr6C5>XaQeQAJEyO=#(hzJG z?ew#5C{Qqus!>r~5(K#@oXD!!-Yt@)>*XV!Ud68``HD)Jl1J*3S8XWpKfNUrP;!@4 zU9p*U`1HXG?|64na3hX_U*&PHPPsqZS3p8%;x5lBy+ilQY{*{ zM6%R!&3vx5BTG}-gPuSWsk6C>a(>2DJxwRoniD&u7pj@=(zOPsDq8dHy>RfWPdL|7 z4?S0P6nG<&)VYLJ*qz1y5lAb}U#OOA5Y_A@f$-d2AOwvC8sQx$mXv9d+Yxp4@|?5c zRJ2q}7F_Gs;c_a&{X zz+n~6b;V*S2NJTrJ=UK0s;Obx2G%`GijK8@qMF-(7iUHSzEM9JKQ^4FAPrl%(A>6T z+0Vkja+e&@z#;O%9;uQ~MNDbU!U2F$fvymfbjU>zn*-E_cZ)vz6T z569<%EQ!rBVR?$78u?a;WF#lJ$vuEw1OMZxAQPxx%Mb-y;q=_5C^3-z*txvXx1$-R|BGoPw=3U|C0XwfGtRA!-fC*$~|Lddhv%3m{sm+tdDLBo@2JH(08E5;pJwTL~ z1cK$%T#hk4KL`JDA6Al!(;b=mbr($jgm1n^<~Lf)rCIwv_b?F!<~1Q_$Cu3?_kytc zPD`HrpM|CyXMGPyf*Fu0=$FqxV3JNe%8s&#G8hvzC$e?gjfd!NeJ+5W&1y-cvTv(g z9z5Ph)5&(|5A2WD>H-Vu(D3g!HLW{?2hp7yfi~?u^yS;H&-W9y^aS^m2Ift3@tg5la+XA zaf|tVVucBVr4Dl;gTK5N4OISeOSPI|{=Zn}@-peH_ZmnGemAfk^k@lQysBpJ2~;My z!;EDLw_XE;1HJ_bYMom)tS=rzIfsU29f3}F?}iu9!KMykpj>5FY*~aPz60^KNuE14#y@rYYOrsMyM?MBi*5*s+YHT9e4sZ_=U<&$sp3Q?Rp>AoR{i{5 zJz&VF@jPyVNeyId4d~7O5zLxgPn%*tTc9oRsIwd$jSkIwv7?>1V*tJ2jE^rAUt(T| z{i^|+O-wG`@z)PlAe=$@Sb3TENw!@&d-Zs~2G(eR)}-Z29KVXZ-9tOZz-`mQGqJb6 z%pu7ielT~cO$8gd*0^R%)Y8!chEb6Lk~Aln*Rmy@AtNtjBU^tXI*i{dhM4F*rvK#+ z060HJRjUO)uQyJakBs?^_dGB7g0s@%M%2?ITInt5MbMsb45E$@tY3IP zriZimSdj7Ki-RxkyhS`=X=wgGD>VN5_kXqsD`U@qPs0A)Kj<6hju-B!cZ}nS_5(G{ zaT}s{+ZW{A{#cyi(BJHWU)&4FN`PkQqzR`L+j<^zx$0ZC)A|;{oz|D6ciE3k#Kd{} z-7^?QOfakr6G)+n%~Nw>-6sF6D%1G=4(DcCDUp_;CEGfO!Vfne1`PMtjtq4I5-b>o z7l3IjiWGvWFTp-~;SYgUgo4_|oIGwYFbd!eiE~Ya-Y!@N0y-?;YMTNpicC<}RP~46EITBi}OSxXpiW3o_9nE24pwG?gxe$2RDv=d2KY0<+&w@)H`=} zhE$kz)KuJcRd5`RZ%uAouaEaUee!6>-}PI-k$%pQ(Q`7;iA)XL7$qW_+p8+^{!^@S z6s7P6gOaAz)^Ch0Hd=_cg9HF=h&~?6064kHKP$)Z%QMOM&zTH7iprqjCrJ+f$FMe z;<)K*ZFBm5MU0$wnfpK-NIE-P;VoKA2*7noe^K4nJ3043EmW?>df}3N zhQ@SU6WcL-WonDu?s@h$YQUpsHVhvM_&HlEqmfe|s1i`wY^$fXU@x&?+sIH_$_z&SCrSSN`87E6q_&f)14&nIE+J>U3b5oAK!=LgV5J2^0$4E6l?RFNKiL(JL$#Qa&rv1db~$mWYJ!C$6c zC@H1MkA+-E`TVc!X!W1E?Jf}CW#lb}{9qYu{99G+>qVt{_Ui5j3iQMeyP#JZxX8bW zRl`|))l-cgrVuyQE*}C=dMi$`i+wF!XWv_Cnp~9Q{ygiK$m#d%c|U_ zzoHlo#|8k41=e@9tO?ANuj*QuZZO9w_E`*$+TbNtwmAt>u4TOEd?Jxv77{8k)*P%HtCut`T}iKZF`cU-2IX8%#Ugl)5Z>K!IX+*V~Z( z8^&Jm?f!d(K2^2V^3A;J2?Ft#9kEFe-CmaTb{*#4D|jE8J9w)ej z1h5X`;9C8d1T`ZRy(bd}iu{QX-zgr3fo$vlESrAbdx*|QNNLn`*UGL4{7dYs+@K`8 zS6$-lwet;%p)-~2u4&UjM&1t1h888F!VkM-b0}htpWlma6fVkkIYw9KB8r*mX<6wp zWN2t2i{&*wdd6@Vlw+bnRepV*vV7}A=6yqd1b9?FNyX*sm3f)C`EH>k6xOl(KuZ6) zt6j4d;-y-N;48H0uMBXd;|H9|r#~5)V*K45~O|2*GoC%dETrCTV5NgDaguk1iygAz5O82Lgw$jMjUiUdGSN$!y<`aY^BOi z*Id{ve7dGiYL52(nX_qIF?PXLAjjw#u70IOdKOkjFw!^O@UrsLWc^BPa}t zQc=APfHwVpw0eKs3md)8glG1@)tIvMiu6cQCwc|noUB0P2V#~@;xmt>H+-;7dXJ?^ z6uYGL-epqO8=msMU=v8l#uoI*( zs<>YjU56Mf@;uv8q5n;cnn$_050V!FH1QHl2|+8$m9Rfu%B8MzVjb$oS;UFcK z;;6{@FR|TGQWqMEizE&$GUq(5|MfCw>!NA}f79bCSOe*+oE9;-1%_fx)&dbAYErhKmf>n|c8H#3}NTDvqa>a0ZsA>a@vNw|)y_LP`nLkU-f=b)^bA4rDT_T?dl--=WEv8Af zxG+P55HF0?vs_~vU&TVLMU?t7XTY4gM^mWIy`kfnOYE#k>R_8ve~2FOu)Io@bK&b&qX` zbnj<7zrVIiOzh!$AyA%0FCeeqrISMQ|6A71a97<86yvy{C2cxUkzjK~Wm0Jr5 z6cx5BS~}8;YE?NZkpD3+>^4{pZFh#?9i8S^-rKq>WcZrRs+UO)@9*5U#w>x#ida>Pl z$oEU?@*g0?DFv;?!=7~I`|1v1`e$rvL0Cx-y>-4Gq2QBUX>SY3*gwB(v8XgcA@?`y zG5bbBuCniD%HK=|!XCMI+uy!87jb3ZKt(OqoL2&?d~faRE|lMYT@hc;+D{;Rd7HrU zZopz7kplWDKqSg(WeSkpDY85vrx3SsB8`UhWC|+D5t6EmU*C!P+XePU`~Otei;f)p z)(=why}Wx6WS^O{cn``zy$t%=Y)RMeAal#LGyISc&=S3K{|*&X^@-ieYBhliMWeeG zzBcn&y<-V_<4cYecGxPTn~=`jtaC9Rp5M9GWB^;E*s356JXHQ1_zJ8os^8!UR2=EM z;o!bfZ!hkN-f|q8SEVl%{SJift1!srb75xVaNn%e?xhD;lg!8oF)e>v3cD*5WohP) z0lTB!K|gKI-lFf;45N0slFsZdTH?R#kFo)dE-g;h{uPZxvNuG{cxA>pRSTH8w`iTn zc6k-(|J+^qZjSN%_$@Sh)vZ$~^QB_m4|wM5T=|}Y!xQhRD}_lQ^&YnAz%d|SS?UZ# z{rY=)8=iX0bzuh^LdcRL_^7XM63_uLR}&lTEt{I(oq5SOcsDfbH}zK3twyU}$bJ2^ z{Q75^Ip8)~VqZy1$mB^$dA7Nu!4(=zafkEmbsF+F`brf+G|1Jw8^qbTP#r0rNqJF} zI{ss*w`ewm&Rv*HMoD0H@%nWg78&x29VsYVbE!O^wD|+M_Gp!5HrQa^ww}61w&YON ztv=mnypJVm>9a!Idi^+n3YYsAZL_jgev;&$c{v|&C`9NK&K*7_#|!6{;O}tui7vP6 zGPmQ(Vdp)zvChYtQ@@Q9J;!=X&0l|A2pVjBEB8#C^c-%t10eYYoe}~S8+;d zf?Cw4TNhk4{zlL8@?M9j0C2Zd?6NyeQqAfG;%@LL+azkyqV)Uho5K|CSW`#go#o=D z%XoMp`b5=QDWl+zHQMhm%-G|iVHN}!{~#@CeVLF4<+p+EUd9o) z(=Qlhx_4!yiP8_HG}2oZrtI)3&K!3tgT9kHaXAp#*`LhHZ0rVaVjwySjmuu!*Un09_^nfKgR*5EEjYpP+ zCK#7_o4}?qInFKsVEi;kJaMlWW@&^ zNpUW2y#AFWnkIkBv-ebN=a$~8I@TZEJ`FrhNho!rY`>%dbG1s@>H%3s^V*ljh3)J& z&WZjm3VfTZuys{*Zg*lvJWlsaCa9DNZ}xN!uU_$?gxyrGg240m3`Dj%)X&jhDw4but3IDddSiE$T zYt@_{kctrK(wqiWnnn_;zZm5w1;xo;mfqh9P0vBEaDeu^N z;Tvt{qvKzhr&{4Dn4G(;tJ0?6?X{L-JIWj=;<^h6^Kn;?- zz_-9p5B(b>Z-x4(2=`d64j+3-_=M6}puGKo^r=i54dxDOJi-~2FXPm_&TG_SbLK9C z4Oq*9;u1dv{bn3B7~%OHrvE}b&;V2-FcpSYclrj%@Uog}hRmU8k@gEzDUrOo+LtUZ zG6b3z&He3GmLJ&MUG;jkbV%oP)E1_~mC<7F3W=HC9GkpQ@az)-ObjPQUE^%o%NjS) z527aFn$^B&OL}|tqNxUPp$t{-#@t#G6QAh12`q8oxtDE>SuK6$e*b8Wjzbg0aDRIu zUbs9_%r&Q*1X}--ZSz;Cx8HKwg*5^zv-;HvQzD{vYhUb!;5L z*DYvfW{&N|%rIsrT4U2O>5M* zrZnAMUENjp)IH~Ft)a@4Axv4j#K7VdFSsIsL{Mr>nJmrooa}HSqx5u!K%)x|olfHU z3flIWnq6bd)L>{0b~#nYO7uOD>fZnUgkBCQOywJAKYz`6y~I3RPX z%W36S$)hZd|ESyhN=%(VI4ZJ}u85al)r)FSZ41hS*|bL!SWy*KEqy$_2{w$WIu4_N z{6yFqygN3-BFv&~WWD)?MhD=%I@Gxk6t2V~4XJ8QEF@hP)mAmZm2z|A6*Y)KyY{X?O-EHXi9!CE(&9;g4-6>?{%1U1 z$vU{(^O@0V&w?pt+w$l>Koe!ESp_FYjLL=8$A?fYHMM%|LPBYZjyGb7vFKZDJYietuCYy^+i68E zgzOD8N%^EX4&6-`V;soAHZbQK&inF1OK+dD=)@@>J`Ly*)NK;8S4HK0r9$5zK!-7? z#DVR0NY|)pmYsf}dV0n)SYCib;QO`DDP+2fQwj^mGiVasf%)d`DwxMUNq2?`Cxd`z zO^1#M2eC*aD4#I3VgXYFpj%qta7?V?s8v}qsDg}Nq6Y9GPLbZsx2sS`siiEZ#Z*$i zqs*%-I?(tFHcS~4rsI=d>`zv&TLDapcdlbp7aJRE)xxEtJuv%Ml;m7jrLOj0MdSx& z3Y$$qCZDDtTHL!vg^*0R8+!?2`u%sZ>5Dbu~C0t!pW=JhL%Y z92BI#aQDg?#2vbCPXKy1fx^1~RU}K9RDtA*PAJt6uEUj#k0q$GzA!Tt zQ@6wUuG^jK?g!!z5aU+2Q>}R@?7?%}z&r4MF#KF%g6W)1*F2~at`2F+CrNhrNE#_> zjCTdeF02?dDA3!z4@KtF_3EX_PG(fuO7@%=Y^^h|Zt5;4E_R({tPD2e;QQX!Kj1!p z$p536@;ydIQAR$qwpv^^1d-TRLM^t7a7ndk(5UhNwPM0USJj9!;U~kR%U4Wjh`ey$ z{rb33{(|{C_7fjeUltyca*N68>BfySuY^+;TZ43s^iZE>r$Fi?54>Nh^pTs@>CL=h z{J5AtQ_K`SI)CGv-Uif>6p?iGT0{wa#frp_Kc5)2uN6@>W$by?oTca^zVW zWW+T(cuPYP_0g?S84ui@58a@43t0|6EDWR8rWUjwd5~YCtN*?8Ld|=YT)K7k*7k(I znYvT_54nA<%qi_2K6U@D+kxJ?OYW1kufWi?tezpqjPMy%K*l1e3T9Hk8d4ccDvat0 zB zK||mfT8sm_<9`kY(i_YtYnoG`>ie%VIbRn5TAVW7yo$4(F&2KYH|5Rzi zB#pi*S;LyTP>u51>a}})oU=bqlb%^yX0+A|JkBee$RSmV$DV-z$oN~6S^`9d$k3v8 zEtKevxBN+pkN#r%<6Zi}O3*vlUT-QB?Ll!i6nHT{ zhRv{wiN;Je4FHGlzen>fLn(t~Nh*JH>3Fc5steVRY)@g*8S0d3*K|;cq3+$P|Fksl z{MVm(0JVm~1vXM$Z3jn6Sw9V}oKEAWEUmK_;APOFqlehbsmGf0Hx-Cq1DnQTmK_Xe z8DGdP`WnjNhiEsw^vb~&60oiFxJY8Xab!)uEjcPrKnuB4(XLRGB*c``SKJ6*`Rf8? zK{weGk21WC!>JMM#%BdY5w{lsZbh`vncf)D5RY#l<#TPEd2co`Td=qqUj;NF^hiV+ zFQ`-R_7)8+l=GZh>z(A|o7(hIXedFa7w*Mc<$jAj7Je{&%wS$m-jbP`&a2KIlYYfc z&vwo#7#x#kty`&x*N;N`a|e*pDsF&Y$y?_0q)h&EPYId_C#XAsAn$Le_-ADCq$?!g`v5;T`$SGojQz$NVHxd&U(rds0Gcb{O_#+ zm6zJ7-eJdJp4dDvKfPjh6j=A^bb{@bpe%EB#(P^+8+d8tIvkfF}N|u z;%`MEQn|wKQNPe&Do~VFe&Y&&{F*L1x{#W_fn<+9v$7$pB)^y;6ua1-KwX%c3QMFX zFZ_VZ`PTdIX`a?IP%wXt$tVLoCa-dGO{^A`+JciZxGtJwQre+p_r8z+-uTD#T=ZF{ zXH;4|ql@x&ZV46t44tZGJ=4?4;MuILwBdCD2KuC_?Ikz7_0=QILQ9 z7pn6<x0jOr9);{RPFZ`jY2>moli@BSIsi+I_- zFdf0h#)(=u-kTpcL#Uz^=r6c^%9Ju~RDk_^xWCTp0OawPcIo3RMkUN75n^4q%~jw= zdr=x|{c9I&vxo6lSWt5&$7FGgSJBsMnpf4&BrnPk;ZUz!TWB&g<(tLo88X&b%$R~` zLWr_Vlr>dX-vf!Wz1Asx7s~249b5IPyP4f^$9u|iyAYD}ladzIjx};9=9|?|Kh+E% zK&*~{u`lN;EMrQiCkZs$79BGgu_|ku#>OpT?pfIXiSe&Def)4@(W_h~C93AfqQt~X zA1-EiS_O@%41jO+E6LFd`4)qbiZ1#r${f83`qs2T1)%YQu}T0XwPA(d`5Is3BWh`$ zcI_hiFnW3PjsjK5C&8OT?0iG)tgg=MFIdTzB?ty7O7kkM9Zcq$v1q6tL}Vl1u2s{b zHc}ubFn|qA*~F}DJWPzZ35_x#Sx(?Is9kTrV;`| zwYbltE(`w-cXVN`7ws?DSL(iLbVSI7Dz-A1NwmqV7QF{l%gceYt#Lj6*bx3;0h<#J!!J`^tFDu zge9iVtb+|&-Y%r80~!X70^=AK1Ylr2*#5?MHBk4_mXx8A>t!p?-##fHYff@7%}T2g zwSRUcFHlM|>Q&W&Zd$Pdb+xDZZtuLh$%y(g+xl3r8iX5tSEe=tHPC}NE;FMnJ_`7P zLUZW{|Bwc3C@(NlT~`bSU`7@2>wOnCx1|kX2i?oJMdvl-)Pe!cq>)EMqKqQ7gcaT* z*&3?d7~2Cs4xcZlNw@`7if=NBAPORCL-Y9N|F(hXh?Teh7u!FM;=kGo4Nart|EMoC zrgd8M$ubnbM?pLO&-@y1%dZh*(ypwBv+inNVBaM4HgKj~vA5UiyToQ5eWA-w;#NV9 zJJG@l0Ptxt=oy{#7p%}~Kk}j0&+gG~Idq~8Uokx{PJ}@1vgZ0r+?F_PtRgc2 ze!#`c?9qi8pf{&rYdzlMvw?!ahSp_!a#*ig0a~Vd^zhT>%yI9)N7EYJ3!gclnh%t` zn64>JpsZihZ|sp{pKc^i2p9QCRd2oi>fi`G0=j(RuN6ZM;(h45U9v{_nRRGznhHIP ziz#7VKO4iX0xL=)vP*1W&hCU5p8_LCf6puN{ES7fg`GdJ@N*31XFV9-Za$uI-{aIy z{>&+B%AzmsxVMJ)jYB=bq!o@y5kFL5E=W_}3#UnaZTDThSo2c9w>(TzJCF(`T<_jN z*%6pdfV|xAaX!B2;yGTFRMKTe!?;o8PmDsv#lneDGB3@lQXrPkbpb<4C2o4D+5V9< z8xr;h+!D#MT1QkDl1Sqf6t0;;>m+`se$q%cojtQYXN6bWmMK@2;Hcy2neXqv5@^-# zPPY55mRZf#Za#uA8GXfyc++7_q2Zy}e_)=DCslq^|4)7L+m<Ylu+@Q>i^m{D09@!*?-Mj;&WVBg!%;n~wATBg1-W3|;+#DTzmeH~906P6)CV zG26D1;6<3H-XRJ1KV?$lvcgBf%`3U4NVZc6H?k;`KRPlaG$I?6x>MEcF!{0rtr_HE zLkbrB@kb-r(4- z4uKZkp>Dx+bR#OjY>{dgD_wYARlhh?z>h`FP88V!cYf6T2xngmTx8cmXmOWpB!-h7 zFy8i@HgNMViJVlP!3^SN_*E+|vb$MUafKV>WA&p>DeE_kIr>d1P`!MNEtZ9fR*6fi z@5Um%0}ZpzF+NkO-a;ccbi_+FOtBrwdqIN?7NkY$rwJdv^$4i@ykJrI3^VZlJT?|Kc32!!m+_4QKF;peZ*Ep89M3gi`ES=Q@^q^d?`E!;Ri_)Y8{6 zdI5Fr`Z6;P-~cbbjsD|uqsJ4|j+hs%U95hN@ydt0YSYS?6yxFO>L9oT#rg&(D8t@s z^|=}Bb-Rp&Q*GP>C=TU0y&y4*cKOoulH24rviDZ;;%riWVt@%P8*7~oaT%FXmar2) zD&D94ff<4J9kA2v z9(`~Fw3;Obt2??iz**y=#Q#r6MFpko0N!4D(`$Er!n|&o?&9nbf*Lemt~HQ9eN2H+ z?^Z4Ekc!oo_SAD?m0PX}er8nvEfxfB-%=H=b{QD2RVSbfX76$}VPzly>PyEJm23QI*4XSZp2Lz&34`G7zec4JZks%|EOj))Ql0JK>6>w$N5K z&Q454j5&W(?t(_oUR{nARpK0@ItAaACaz+yo$3!Y?T{rcsLnr`wpVg_REjHr&Ku@K z={fUXtE>sM#cNigF79GhVT%y3k!+>5$r7Ddd}^5v0GpJ(yh@2~Jgx=a?k_b5^*Z>e zrXwf6O}uTrittu83kc2gG1KxIBxa)?iV7Qk&?6`&rUHUPRS>0FZ%Rwe;vDTX?c16* zDeZD*h@Z@tchjD2y4CQ(*`AxmD@DC-q8x1<=wzKFXXjYWzNvRcesP?U;cq~owT~`C z(hd^6Qs^M%2yXp?D`Z^5f4qjlnA7mpO19z_(auZb$1<((+rVG2XTQ-3PB@dzVjI7i zHHQ_4RbIO{hBpbjZnJL7i{|s@3*x4aC7LMaUcL-bn?uRwxrN1(g}OlY9QQlQKP zaNtm&go)tb{}N51ld%XX8KHslOXbyG-^`FQ8}z>QfzZ(h3y*{R4!ogPZvGTwsW9*gr1V;F`@Y1j50fUGkg%g7tq& z$WJ96HO~ud7228kR4*COHC4+bY3GzlRo_*|&($ArUp$~TALBXC4w6X7*95`;1#29D`R+e{ z67oG(a7jiv7C9C{TW;FIZ{l`1b92>uLy$^fbWjK?I$kLHHCEBdvYvu(6ybEvFs9K+ zA;a0(EY~#!GgLtD0vf5yh}Vc6;U#ZWi67WuMJ~$#^mgQhhtU%VU2aj#J}6aeE)NQV zTEf!s`W3NhVs?!Fo(Ij0y&n4udU!`j32%05FbJDSYi+8Kw5<*|4j<`Hr&}I((#p_a ziHf&b-xH>I*WeP%p==c&jb%B;_)C@#cT!f983NxoI3F}FozEkncS*VTBW=k~?^>8O z>U#$Nu7M`ox4d?4Ap@$6ypJ9*EEqbg@Y=Rb>bWjo8xs8nbW!PZB7~=|gxCKZ%BO_C zEAR0F560TVvH5B_uWSnx+I3|})Q?-fw^k!K@*Pd(f?sEYUuElF?dG*JyS}D(arY8& zIr1BgYv@!_j*LJuwxya4`KN!TPh3_G&6Go_Z@YMfiS{+J${}&khICV8bmdqOt}e_` z8^nF?^>W-G_p_x(6`%PJwDt1pjt2+V2L=Bbjp5TruRr~;q_bR}#wKQvCLwoI&Z4WC z8)4amVu2|18am89w(O_gu4zuDQay z2&*aCs`8rO+0QbNM($Uen!mzm>T;2zjv--S%>)xU^B4LizaWDG_ns6~pFXgC{HiS~ zE?E9ZQ+w>6T|qPAGbR*S%hI3}jXRK1LPz_po%KS2Kn(`ZPe*`6Jb}d)g^Nc1QbUV= z)TqD<91{|Q;sqaSQRM#kHHxr+8~yMaz&%jE?^u^vPI8Cqy#92~%G_dIRxxOBd-}Y- zT16F(#1S zuUb@hjO;9j!g2CFG1H~Br`P-&_oK`C(aKbPj)lM<-^~B7nH14p&9qg)kwHD)T$7kT(gYRhIb;kX8iw2vH>#}aJfn7SEzE<(2!;#}_dT(NI zxn?HAo1|iw-|cg@)1_I5Aim!50LRhFTX5x*n&06OlChQG9T|hOUp#H}<*&9n0>FTa z#)J4GbPaaaZmt~Pw}1KueJx-v7lHSNuu0$9Uz>XhF0OMw_{XZ9ejlA~ zFIw`1MWlz=)_kHIu~6}2+&pUfi)4rlb@@lEVpFne4S(J^?H@$OF++MRos|jW8cpDs z0mx@=)uYKpv?ohqP3xWI2ka}XL%KFp*(D{}H)A9! z0~23)vWq+?*8rQ1x4z$9XqLY&q;Z_hX*|42@Zv5acevi9`jG{llq8nmyEQ;87{`?G z?=SMV&wm~R#@>X|A#wLksK;7mFe3=V2r9p`kQGBL!UqpczKC^bA()u7LD&7^1OjM( zsiH2!V&cDx5BBOOA~kMc5EwF;k1}ZWQMy4Ph&zEN&*2pD7Tu~uuOGhA-b^r`VyDe< zpE+vpj8u53{`iF~M$sj827!H9GFD$$Mairdcf4gRA$qt?3vRvJoTiRsXwwGOaeOMk z8p%t*;KL)VPN-+zUXLg~eHqBp;|XrLiAnOS4riR5Bz2q>=t!})W%+~umB_;hdK_S0 zk3k{W+`;4D_~$El)=Jq=#!a~U<0yAL6w$LS>aVjwS5y~gK5WGG@CFsgLf~;L@eYOc zK|T%&GX^HJI^o?pzy183RSRa>pDZaK zUg0E(Lh~kA#Eg+9!JQkcyomiZ`NCB3T}#q7#asNa{O@m~33wbVI6J)e(SS~K-2&)6 zh?{MfRg|))M)I!$a^A(&~8?gsBMw*ysSTNh$P#to9>I zCE^S40M4Jl)fQ+M=shU~{4N6~9ToD?E@rxcrmrX9iHjGInqF?ny{X5x>ceNSf{7H1 zP=h~2aEWLLgGm8)DhX_A`KiRuUeb({fbL;D0;XJ*Qe0{rnG`{Pj}!VHN(|gVv}if0@0JfSvV)21d3>j)Iy4-Cn0X!^^3(htwdX}ah&^q?C-Z_g2PhPdg~ep6@=`L^ z@2A^=(s6I1b{Gkkl@kq4z@L^0Khi>c4-NuE*G(M0Hw+n&J|uyQykulUUQ-PpKRVs4 zyCdVMktnoQU6I4!6< z(9^D4w1w_%s>~idzDTF1lWwe<4p&dQifwTuCAcvq1g##7tN-I1`o0EhI(15aS}cq+ zJg)9pr3m4RCQM3`gnq>if_}0X)0MW?Ve#~tK-~y0DnD;Ab^-6_%nRYNQ!Kn&@k|L3 zG^aSp@7be{%AcApg%AX=yBHNn5sDd|-gqmecPWn#;#Z$C?2D(oHff<+`7mL+E)W8V z8Ht=#6y#d!+b1+}C1B0{KOeEjOCB)^Fs=2h1m@7;Y>M66S|3wmFla~{x*4{PI`#zhGO>VeA9#3*Cp*LYaFyy-?-}D1+Pg-AtR}3~&#mHc|X? zxpqx04gS6Kg>~15Fc8lk2iM6Tr+nSr{&a`-9-?93mqEsBgjXmz*QD+u8eoUJbu>Ao zQL!ldH=l?Oa#>y?Udk5Xx+k6}k*IFaAh=%ut+KXN`Ok>+yC|tO_Ych#jNN4- zjc4~Xg1{_JxT>}Ov7&EOIU$qSGwink-Sqsq)*a3YO*wV~tG*3yR*!QRC2Jtl7i8Z5 zAI<;Wum7jY|Ert%_V3OEYLq+bY62Z--cBbZcS;Tn#5Xi*DGYzX;)II!lZ*J&IOjUP z|6$8K;{N0E|HjuOXEIb1W{0)M-fYfio@fv!#uZoP5)Vul8a}3B-MxeJp^|#GRRlz% zRyA5gPvLyIl(6bFVhc5B?)8!tUMk*9wl4+|mais+ZdAF}TE2+vc$V;|+(f|oi?AWF zlvT7-lc6#qGP$xp)x^cB<5HjAP?q>$DWUXW;!aII^W%WoyHt{?&u8m4G4 z5piS}23#;DRtqE|j9U0#y0Vw$jpOfGt=GJ(1E4|w7d#qS-zz#*0pP$bVuQLQfgrsl zd2dI|tnMs>TpVCKx4mAPZ0ye1;G)$6rmi~1%y(dSBvW@4q;^u_zQAr&>6>d71&UK` zE%&JOrs})>3l@B4DoC^#bXQADBSA$vm4=f?w#g%6!)Us)<&-z7!)DSxtL{6^)$$O; zVq6@kNb;dMSTt{aMW4YttW}C5O>=k7yX4>1OM?Y{JO?ioj(Y1IQg|ts`bCcoQPUQjl^vJG_8}B730hag4QlG794aLNPInnCZq^hpDaG(9Pf_u8(ik%u>0-_Vlyda zLXQDaU^-9t=wzyWIi($bam96Fh@7}=jF2frkKvqDFIr=`;~yyBDvy=WrR;dYha$oo zlMGyT+N7-%sLTEX{y3|yo0{g29x;ZT8GpBT6`lNzHlpW&LdQ>r~4BsjIH z&^br%K6Ux(0B}v(X?xd{=oU1m?>Xrk*djAI6x}FGy-7#M@*@9(k#Qr9>5Jv zIii`y0UlZcIWcQ%`+AgLf)@4)p?RW$$})`^b4{P6A3`ZGPqQgb(gS}IbUza)T9fQA z5@EDDG49E>ov~QAnB$Lvwc-R7S{CRcsp6^q5vt6$HHNl_)|fmYN-+fyidDfO2C?&t#2ZDNdZ5lk zsAZVJ@&rA;oSt8|tuG}?rxQgQ>?u#+|Lxt0IGZ;P*Ee5DGYZk+=sm*B=uBqfw&y}1)+W+_y zANrkhC6Kmqd*e1$)UTG=-@#`(w|{owv%`!a&=a$~O(Yc}6|xFIsm-v#;VpAL&R$ku zSSOia+VI0K2$-VXQNG!gb3DzP@W%RkN$du0?ebSA#1bUH7ja$%FepOOOu!KGhd0~3OFdVncO`Va$OF-?> z1mm`IjV`r~EMHAq$_u)DiGc2$SO`{&fjBu3UqnD}0HbCN)*<3Iva=B~ZxI|o5Y)E; z_o&b#7{OzmfiN5fFQMJ+kR3%f#7HHWH#Bwa=?FIMO{E6o=x_k@Sr#h`8{wXTCHthrGj_!Rx`SePy?gN`+ zaEKXU_>`JRur0xAEb;zKx$>Hp7E~pno>zeB8(E5}5=^jzkwTQ#aZ11;4Iuz7gCW38 zy!*vENvW_yXiIrkCn(GV64zkxf2zVlHWUjIBO#x8FDG7#VluL5%c|&^TLXFPEn-L= zzaiJg*(^ey(WoU`{^;oh=9(q%MMNVQIhCj*WKV)yLpS~85yv{kh9;1}-M|)=pJ^}A z*{h%KEGmPuMwqLZBiGe!!}Aj9iN+$DC9hizs15!mIfO_N!xisxvmY=aCQi2#)Hk4r(TH}Zt} zx71pfQd6<)0J(nBaqcOtrWYa`W0HJxD?<)W&Yd02f>5R|6hc}qlCb!qMs^#gqwH!Z^E=eE~F_8E8B6l zGm=r($k=G2mG5{Xl-<17aJnj;IAXK7aj3Hv&zUaaySagc&ndBHOFiZ=%c8xf6u!Vu z+27%6<}L*6&?4a-q-Jfi#*~WZ2e~a0cF;|VJ+BubGn^8HGh_;uRVZ@xJ+Sw5Vaz0W zzE0Gb;&w9?Q__rR1fJ-{dVTlN=eH<)1G#cvkXCUI1q!?&>7w4pcbdC=VEI$GWqv7n zWG5}7wY-~h`rqivFe!PRVZ#Ow&iVa9uKp6eRQM7nb)!3_@)Y+){ObFm6Si7As^(*M zEIeaD64vVEHgdT9H$F_aP)!j=yP(Cf>U?FPf+ixTAM08daj&&yq9!_o_@a|Zm2K(@ zh!NG4(vva!Hz9)oJQ1_*UbqeDXo%DV9I48*b;zuc9WTC^vmaUE+#RT_RJEuOoj-l5 z1t!K36Q2cBZ2W(C1p#M>UWFDLiN}?0*kFJLq|U>T(&5sj73Wl*Vz#Fh&-$bS1=1i9 znX1F#YF*W55~}Ve|AJ=L&XZtAvV280Fi811X(*B&f8v%>^@WMtEK;uBL%V1-v095X z&1X>&=MD2aVI9&W%fPgW0*0;HSFUuhZ{#PdLxw|xbM>h=>MjFtI7!Xg&%L-|jEb{E z^$d8qHEz`Ge}LGU$nLRDJus!4=ksV+IECPfh=xmWXSzM?dCC{t>{rI}aYxpN9?Bse zX`=|*ggw4xUZr|w?77m)v5k!a9~l$%*XW`sDKHC!!Z9A}RYI`p{MGIwg_71b8YWx* z>(x%IC@>BA(l&g3{c_A0XL9DJx!Ll*1rAnsb<--VjC)irO4rZlAD_LG_IXKV_ zmH=G?H1CQ%WXROe85g=7cpV;9@l7k~fGh%0o%94SC(Pv(?>UD|Jpv$s<4;#XA4Y5q z5K*FZ5Y#-L^C75ag(eX($J^w+EQ)mMu@q=VBbzt8aA#(X=NU3R*d5VTjG)SKm}c7P z{9f%tbtSPaJ5Oo5Kb0m~A2CUT%z|CU^!#^WxMVHc#@HWf?U1pUc!rTJAj|ibYG_Q= z@*0YD)bfD#jRD4~iMD#uWJK7=zkZdb7hk~_l1mdZ59tVQSo`~-)s{iC+I}6N>|9Q) z3O$MA82~i9bsQ}hOxv{*^W{hd$^c871Z455+1$R@D{FZpp?SxzA1XtW#b3f~T&p*? zrlX-34GPU#KlK3-E|Bv`3K!eD5ZmVlZ0}lLO^HA9Plx?x1H)0I zGA@(PifN?HD$BLh+vjK|zw0-S1`ECviYY5|6Sp(!m%t2&R252>yo;lnY(_Me`Be@A z3VS^|Z^k-=uDCMzM7eo)Upz>^Fw=ji(~9Hc^6MCp(vf%W1_blhetEfS$Tvh5BQ z_v^~pDXX=KSs$t83xkhvGu3#_NzD}mv%~RG`@lWlpmf0cpq|c6Pr4nfpMA9q29&N2k-5j`Rq1Bw5MC%Yvq`&Wlyr*2HZjORfqYT>Q`w>!YH3T z%+#)iIE9^7$*semsGhROuCYD-#j^>#tMC=WzWyz6>20*J<3xw)(gn=asc((2J$M<7%dS4I7`CMms1X? z8Xm54{5SO^*11n$x|R{tzbV>3o_sP&1NndNPdkAdPXLRVQ$#()aRv_X#n^ujcQ&Fy zLsm#s$r7tz#8!L_HJ^=#q^{z~MXFl{pky&IOMQVcBa21Zd(lh8(F?n{fn4mO`+yq3 z%Q$sX!B2e<(Y=pIfO7VasH9PlRRWm7XxEK-{h~lvZz-Uvgz5;jl;`poGD)g&)Sp8f z8229H3YI_K>hLH!n?^#@b)x`-ftkaq+ZJ~an>8UEN77*ks%APcMCQ^Zo#TBHH|(fx z_AvaVF>b@lPhL73^z#OKrQ~tXU0AyP22uVXBRBr(M~go)UW+A|BN_sdP#KRC(qcaV ziV^1*LfZwMg1J@}7hC~tGOk4?XY(vq8qBpMr`eA$C6;0}UUk=$3FfQ^jjqi48`IAW z8``y%HnL()5Nc6=8~VQ&ieZ2llLv4Mz|zYvE1oW3Iv7hxvot#%=&Cz_lU3>v3fsm; zx5PN4cm<}tRT|tI(H?jL-thwudsk99z@qXZWd)}$B%6C(-Yz>fN^T?iT?0O@7q=4vgh^0l*rXo_CYi>du55(u0Vio zzA|@JRDabq#vGV_XjQssb3I6V(KV%G6%jW&GAcT3bV|xUYGtn8!>0l zkAW`iF{6y2%SpsAokasH2R07@TfIvSOTiK;)AGzGQE+3yX^559Hm?CqLxLo2i8jWe z3M1DPk13OTC`j5}G--q+8m>h2e)sA1)UDxm0qLYj($NuA{&N8@{sD)d^As!X8!T%ndZ zc8wmQaTAq>iM#jc-FwdPo-(>WqO=5+rd5c)o|X<8|HUFBxwf~ z@wKN~zZxZruStotq>gl(=b)9FEdiij#h{GnY24JkP=EGH0A&%42l};MigU4}UWBsN z4oO@&N!pQ6*61)&-0SKUCf?nQlT1Sn`f3(?Q34D1kNzYs#)Z+nVKjJ1ImEh0t%YZb zG@U?pe7>f*DHpt+u4q$6h37bX6MY2>w$*e+R%WnP!ITe4U48yz^N-5kDu$MVtKMAI z4dpHC9k71QE>NpBSd^dWuw3XrKD&R?di-?fNE(O@6$=e%n>`Rz&EpT-cWAWN2?onZ z--IzmQ{|c9TEn`QGjc1Fp}v-|>QEIfLJ3*lz+_Jx3JeSH|AfI&v*?6!0jao9APuI5 z=0?Z#-4fKLcW{s=CqNZcXpSP2YL^2bvR_o2nK1D{F3v8*x!4+cVfuhc$+mybx~)aW zdN^g!0QLrK*2aE*(p^Azo$^wJj6Vy)2;ZpiOm^L1`fCEt@DD7tqxnS5vodgiJcg~$ zF3O0HWP3QjIV!AANkL_h3Y)szi_It8LjLjmTg;meP=Qxq@4r8D0y{KwQwr@eZvI$r zP#Oko0$U7={>lLV7j|He_6hv;WpweRA>YBP`EkKc(%!-DP7(TEi|#4?Y`nyxw5e6?YJEF1Tf zKb|sVhZ5Ztp4X?<(sIx>96TpgmIs44ZNk9+@P8cGy{e zn_+M0^gaOOjvur|OA^|~H^f{M5{%Kxpwz&Gx}?@4+;K2oLxvp~#1x{Ci8Ov$ryX&i zLFSl2Ai%rz@7XZw9?b{@qz}X!1sTrw=KnFS*N%-J{K)S)xPpOEV1~J3qXwQcG`Jwj;fSWW79ea1Hf5#VPv9dpmpf8UtR zt`+EamjQsaZf2cD5ajF-fh*>FaVYvHs-&Y~&y`w=;i6i*4|uaA;!}2uVoK$Wf4W~7 zD#1sHzUM|Dt5sdy=c=ApyHk@fIK&#>Dna1tB!}Q(EU}JRr!sK(&DfDBPp`5t%dtkw zXrnpkkw=W{AEU}g;4nv&5x2nwRftj-|aF|l(&AU4&^y;*Bon~vax00sE=ZB+6D=teA>0c zOKgx-`5Htp=ritP)^7vvd=*PIU#z(?hwWnH3~jVeHzG0O7DS~xj<;PA(w{7_E!;+jz~!buW;6Br zPLicagVm`yBWKVp+J@1lHnf^N<0Pp z5=v~?XgwY*T03`{ZL<)+wgiaA%XIF8G)@UruJ44g`2!}#X<}&GO?)Sykz?``E$&>c z>qa-oC3jk4h+q-j?b_-(EGt&Ltfb%s+o@m(wC=1<8a*v}@VokfT8}WKm|AXGn3;KF z3FCYpz@2BV(WW=aouU@vm=-NKKu4up*-!o$pq;$1`0iiS_)PktqO9c_!@54uUMfe< zx3dVvg)~GLH25awXx(;~rY@8^Z=LCyD&=ZUJq`IoT2DX3l_0FHic=c_+(CiT+(k4X9%-R};Zk8MOcR#U-~9k4YD-7RzJ-mk#Dao7)u7vn z&<2VZ^EYkpPe0#;#-~`FcaU9L+(#gu_a4U=l=4M$9WV66X`E4eypQ4RGs`5VEib^P zLp(^wx_<~Lj(KAInJF_UfqduFzld-c#l3uus>$x+&rLUr&0oBDxoK>}mcpp%^S$gu z;P#eTeG?K&ya~bpJ7nA;ro6l5T5}3V`}I|Jsd{MQ8O58%we9l!IyN7& z#_(&F$?7aRKUG1J2&&bsZo+PBcF7fnp?G>v?a>8SWGoHx*xW>N9)ox@23moWpW{A~ zE>>I99*<{ll3+62E;lb6?Fu`OrdZulo_mhPCYQ8sW3Bb8*O2yARlv*%^efr{F+W94 zLIYe|#Hoqp&=#a3yN$2ANnb@(Q&X33cIX8*{<-oAFgDA_7dNUP?e>)vp$*)Aen2v7 zns|O9wCdQFX*4P*qbf`R=t8?(*ooEL{12FlgKXg}{A$P{GFeX^G4<`MmjPJyCAI6X z^kw{Lcxj+@#Y-o%b<@s?^?jm>y4a2biNQKL0lftZ)r4El*Qr;vA1QAvm@Nxo2(=mM z6vTJpi{xlKn+L;96`}+Tz*HJ}IItF?{{CV!1V#wFR6?05JD=XDoPtkE&mYm^j-|5AAvmIo6<@(K%ov*x4aw46LU-DkTLWIf`)soVv-#BMM(AY+%K@`Ymi*YoBNX>@!N_UlSbt{z8m!g{6+5yx z$@ahCshlD*gTR?PaxzgtZ9E4#{`})cPUW*dE4LkQG~b|H#beA8eEZr87IkqLP>9W% zy=Lh+v^bfVqR$oLcL2OclP6(G5pF<@LV;h+jLWm)Vd{JNjOqpB%G$eAI|X$cHlt_zQ*+wfuXX+W5+hFWwcf z$AZdZ*)4g2jkB}LGXd-}8%Z-}wqyGxO0TBgS6j-4FWgum%)!D@4x0_Q(w?QT5%#&u zdzHcsRh*u)U+ubJ&Tm+&8*j0ly=*uyD=MmMXW3er z5z638Rdd<485pdN+2_{urFs+j2L2_RtTHFc2pt0QVvA&UWT;5PZoZB ztGV75wA{Gpd^7^z=I}a!WA9z7Sz{eqk#O6pB{avT8*)tQONe`13QI$=k|6s9;)~+Nj zp(5qkAjqMgb{TiW@z!WC55u>C6xZ&sB33w9^^=$syq;m%s-gvwt*zDPO%(0KhA(YC znt(6Hot(=Sn#01Xq5>4>yPz)*7m=LVQ300Rp*5)<#~FgfA!e1_z`{IaI)l6$%FQ*F z*IOtFo+LT8vm-Z2;2{T(s+oAcDvvPts2FmGyM7ZfaL||uLex_51#v1=OIfO_tq*B} zCjZe-4d2cwZE&u#Ze3gOQXYM(o9h&YB0z0Yb)pGLg-9sT4QtG^*ZS1S>b#6V2 zKw^M1O&W#WqUH0pct!e}1rmZ+g+cy_dN&9k57FyjfVYZQ-kftAsmeaYhZl$^sRRN& zz(I?al}wfezp&JvBxj~uh+v;9vuVza%|+5e(PvXv9GKT-9HVP^f7w9IbxGxU0eOi! zO$N&QVWodW4BM~%@!vgzr$YtG->%{!Q;;9ES4v}VEG#ku6R@$v!&$*(c%LJ%j|%gv zxU>xMPM8?+?c|IUr)_AUyMFhYl-@BWMlxxmr*B_jA(rS%+%|VG6rbVKnH{ZCjSxM5 z+446wYq4X%F0vOTT8N$bq>$R$=-d02pK9p@i3JO2z#?`h*Us$WzZDY;2QL(oY{HdS z?rF;$4+k&mJ8uJ}$Kwtr(IGY$Su_1C{rI;3WbFXC7=HIFZ?2n zfm+cZyT&9-G@!w~k8TI%qti%?DeiZB8uB8l=aH~J5^rP7F9dD+^t99fye{rf3l(B~ zPZlqG7Y)YpqJ>`Sp-Q$O1n1QWqs_&-wJQ`d=Z*EIc^+}FCUka=xlD=4sswt18iY*% z`^;uyQ`oP&7%?}w`xJAc{T((lcO4pbI#eB_79?{`WzYdzl54nPTxw)uzjd&0L>Q50q*(#AC=;~a#D{X_ z@)+)7QtvBH4xJ#RZr!hYb}Wa8K!mwy=CzW~UImn+RyBBwNCwTCd;)UWK+})wLYl};<7B94=^knaM&zyN@_B*rpxvuZe z`DP_~p0(C3OR`+<+XbAEKpyr+%lUO>VzKzCvfgWv{299XwS9A3T;NIdIy3Z}VoUJ} zB>|VN^fy>@G0Um5j4LO-DsE3^Z9|f`h5oA(^|J2FLXmiAXQ>V|Q@9N8rB) z(qM)~(znu8kr}P8&0II(f+pSa5$#CUR&N2*#7}XbhSZ`cyE?wkO1v*ryXBejY#>x4 zH5&Uwq7a&2Lhs55W&04VrtEt33xEvJ-N$<*PjwDU__(mG$!rZ01(UlcXz?`*zfEI$ zUlZd*+{vJTZ7!&toerd(OxthuA0AbDyl%%Y*Li~P`w9oObHm^lpbxk1R?v?xtkQV& z2D8BL#m%Tm(BqDm?CkSnlC|3FDG%b*Nl)13M<)&6Pv?OH>@MAB(^-^t_tf-K1vr?1x+TL!RCEH|4i%Cb=B^8h z`UNv`n4* z!Dq!QN2_tMVuFh_mZy3ijgU$@`VX}9;z^+)gtOe$W$_TN9i%vI0V)<_7l5G`J!@K) zxdztS`1qRbbyO9j;Agh#{f9(mgr?d<)gG`oU7A6d=(;JZgZI%oX~{x;-`J=3(CIH& zl@%TH{Tyfj?4}u5Dn-Wz@?_dpjP-hB-Fy!(MrY+tzGp+C)k1(WrwfwPq6gWvru6#i z0sc`fl^7gfq%g*uG0XdlzTC^yCJTf5`+dJ{#8lnTUrrYlZDy%{vxs9GAUrliC>dk9 zzc8YL*qroYd=bVW%r5v{=R5@|y23K%2vEIjA38fhy48Wto=LktMjPQ7{^GR(X4L7| ztahK+_Y-ia^?1HdwQpMTxHmd`-6MPXly}|%lCm4OyB@Q#RAZ;#=HM`ERY&Ty`C%@O z*s@Kzr=MI$SD>hsgEW)oN{4&sQ|gJU`l{-=NYjUiNsdS@;>7i<0}dNynGMq=kB|({ zvr%@)*yZtUCHEBl{#%5~fvN~#n*QfAHlwt|rXYL7C8_#6dqQ0Ym&WSH0)nIgEpW`E zW=u#^3%chRhVpUukB8!m9OlxX3(_}!%f44L&?a^R?6a0N!KL&zm4q`qzN`_m4!<-J zj-{gxy;j=t#VCo-W07xw_HM7j7#xvR1x)>VQ=5UBS(A4X-I1SxR)JqkVR@M?FwjoX(C#*zPIgF}TFb2ipG#n<}L0dx$l(N~2tRp?+JJGad&Glb} z#SG2idknB&y8*=ZG7B0fzV(QMPLH{4yFB0xGFt6wEn`H;>|DsZMaySDCi(FLs~`F9 zaag{!s#w~eb#Hn#>VlVi?cA*iB9h`j^k_qJE*=m@rS`t`*fy>^Mw6YivdpI~`zZ&J zK4c;4#IjDDzLzz6hn-|i{+UpN6!C-V%x7HzLNKd#)D!T$Xn%H$Y3S+{NMBt8o?+sQ zF-VKH^o?|v=&>3I*}v+>8sKak$M=P=BD5xsKU;BN`8;AH7PD9V+gHVDStrq4w z@#8zZ5QXXM^gyz>vHi<1GMRFzE)Q}e@umFYGi^$x^5EkT&AqlKLmm>U8(usesO+1t zch>IZ{wF2L-;*k&u&>#NA9E-w?iz%3K5_BjIZ|Qxu_>c?zEzNAqRhv;VqNMfhmUdt zTYug0!r}XQ1~QBBn_OuSC%51*rc(H#H>OQWonxIEGxc88x1=7DRVq+|&G!_umci!3 zBgE8a9*@sUH-{HqPD%qkUMAkZmTRRnMKzw?ga>NWSNA&bsN@%zxYAwG>+v@l+$?o6 zWjS?7QFF(u{#G=kHR|*yMo760lwGpsbV=}>N(gHm+8@zJ7)!S4MS0hIt@$8@nLoi+ zlH(hanwdk(!Qhr+OG!1K1%4=ocl@MLjo$iv{V+8d{0q=oK-S@_UVD=$+k);MO-y4N zlbFXfY_3t|WbAyk`^`Eq>{hZ}K1yJYX9_*YqPjbenryhW9DJGS^7i+sEeTE)X}5Lt zPmwB7Zx`B`Jg##mqR?8s4B6y3XPX_=!4t6$wAbHd7b9HB&S^B3J;h~S6LvrvEf4pP zd2Lc}%U-5*rf)M|!Tn}vHYy(pT*5xG6eSt%sLjAvJ5=$H|CrW}Xll>R(-lcR8gs(G zPnF-Is$DEUr};|Yl*5Ks(ONpk!ux)#Y3aJmLC5_@vSrRVPp7@n^D?$MA;)|8{8ce! zEU{|Bgd-Psz|S$>8*S=(lsKHZd53ig)y>-OCl%4~09EiUltRu-5I(P}Z+p~a*maBS zfC^gz8&`K)$2`i#$xWhYxJx`ytf*LTgXW2i7tb%DXpQnS7W*iv6a&kX+e17UT+)51 zB*Y}J*F@9XXKgr;q;Z<(0=r|5f~TfCNFbhAtDjAH$T)}^0q@dxQLl_Npr3b-5Q}I( zB(#jjW0X{he)0)KQmwaqiDQ{%z?`)Hn&M8!!J~5KIZnuRoAsH*fU3>`&CgUG=>Vl- zc1Kh_GjORc%LNX8IonCzYMVn^YOAmovCghC6!39b*T+ECwvw>ItUiiRH4_eEI(6K+ z3k&UiZ&}r`%`@NzCKovitE5uEY!nF}sOZ?vR@G^3)o{SfA2g&@FFQEnm9H?Kt8CXy z#o)vJXvgzJ1BEW@YgD{NAe8)wBmur!20nN-&`*C2S( z19o^;nw~E)a#W_nS;9R$Vn0R{3uPYhyW8&eCQu3T!%twQlybFkw_(yYzDI6I9Kr`X zazo_!CAC5ZV^+(AaI9L-7L`F=u5OujyX$~WyVtBU=%fTcWN{hEvw0M|H8Pz?)gH{H zv=Z4fl4RrxkKpg0al;3tMv`9wZe-w`PON-ti>fNH%Q?`sPSzl$!Josy?an6 z@AhHVgRL)5xGMk0Ysu{jI47FGRox29O0h8X#8%#+$CsNc>!7vG$0zml=^;~W0pbL9 z;wv#1Kek=x8eBk^+d5tyMl%KeF%~|VOf)SQz|xQqeuWKE_xQb>`a!>)%V=dOE~Z`8}dTTn?V~iWF-xyZOUZz>rlG-&zaT$fM)r;b%$R$N&7U()-RkSTrvjGqs+ zr@0afOK-Kl(Dkmx-Z|6Pc-_vf_*rwFMJ4Mqa7RXG&`f81Lh`Mk15t9mXWH>QVMet^ z&sfDm8~qnn>2o+jX_!)rb!!$8A*lVq6nLK8Z%MIzu#dXiZ05Jl`jj$rzKcJ@=H$Fv&j<{$-rffBYE5s z@4_>)q)Ut=%aLW_o=CNPSzmx^5+O8Is+dV~tGC$^#$=_Jz{o>jdJ%P7FDaadMtm=) zG}c}#J;O^a92MbO%d1WdU5BCL<(O5vF~s%tN5Q`D+F9nf?OAoN951*u*n#HR5NjM z&1-fGW~8jVmBOWZU4&g;ewMbBJ#>bOm4)QM4GPGq=$%0PiA@mQop(n-a(F7mD4<2lwQ=$r_G+A$_3o z+Qp-h;uF<4p#iPd1~%5xp)_Od!XzagLor&c2RChw0&l)@27?cz5nHUlKh4!h>c^3&iH6cyc?P2-GszNZZX_WIBTneyWy|rzQ$EF2SUsy@oSTref z;U6OoT$a|52aJ-n81Qkh1fL9r!N~tqQ(}A{7Pb~ZI;9(JH6U3^P zvgoPN-3f~$FwIlA%9;)qOg;7$*;l(-DAZ2FUbt5wMi7b$5cg_6jwmPXs~~mb_0M<6mxzrK_LF ze-p-k1E%g6*7(qM6U$n+No$J^i7lwUqwlD`<-E-r3bgt%6~}cG@=f4~0XW?_-J#l{ z+O66RX;gi*qy4QX=If)uAA3KPehmJ=-w7%SS_^6qTC7 zbskm!11w6_>opksEz;GTapf|lS-h0~2u*;)>4>}HYm}W*p-;M}Izn^M&IC4{CE7M` zoiL;jLAAR_q};mBx24H)X0oAMIX;ynfi*h5Gpy))!nhvdbhs(>9X>h7w3T zFO2mn8W4i&+T$Zbde^{M0s(SQ(}khHtVM(rEwn0m5^@gB`3{KI*RPUrnROz-bQB`!{{-2dv_1Blq^j?Hu0zTw5G`MQyZbeimjbtKD{H{9 zK6pJ}XC?~QP;Emml?k>zTc`Tb#8f5$+KNu{CS+G=MdWLADlMZ!o?)UvDkEEH^!J2# z0L80xg`5{}Po1wkz@AnpsAl94n8uD>97vYZUmcp5b%0%m%t*i|P|ant>Z}7tjY7#o z_O?)_HG|>Cd;Vl@iv0QRHNy^1hBWCyzx3fGAysJT8k;AYS2^vHDb}`X)!*b zv}V0Qw#b+g59f;gIG?#)+6%Pm`t&Fq+6pX^$`<32z`9gE$KtI_(Mn0N`l6}_Y#2Kw z*~OAtW2hAi1@+_w|A>&(rQvm)l7L{axVf1OJ&Q+V^_bk+GJ0Z!Bb%Wdo@>*0q)wJf z`{kjL2;dRQ+r3BVoAqT30Z3C^Uw3Gn(pc&~7EC(wcqWfEX$cxGZrsao^&*Y4f8l8M z%{MMifMJkw%zCF(d$Xu-19>f``MOpijD=ef@woUcI>e{S0iKLbC19;}{5OD;q z{-mZ+0Y_9$nalGE`{sTe4I3$g&Hl=|K_0vJctGj}EP={rHWp@T5d*b5SJunnbo2L- zZMSSEmsQuJvGB9Aq3pe-h=NG*x(Bui7Z{0WLvaKWqGCoF8U@y6FM4G^dHx_bKQ zSwNAoFIB2alrIL*)>eCGxS(LMt%>|9Q{Vk%`RmIddcJ9#xd))142VOTIa)JJi|7^Q zq_bCMWzv{y>RNBJLFl>Rc@)H!Xk5ld?@iB|gO<2g%2ff<>xDuByv4emHHqyS9QZ+f zr4EL&13flByMW2#Lb^BycfXuST#$%POyZkIYGYRkwJ?)`^sOJ#JH77|9=A1})}ao` z$t3^*yT;}f34%>5WpCCs1BEYxN&$rKBUN-y<3HbSgr2a7(_WhdVHC{ch1Phpsxm6D z;lriNT4=B&UMiN;M9x=4gpn#^FRL)v<{7N)^-y4tVCe_lWBU98&>Xwi^}LWV;V2f& zoTM++@K>nEydX&%b&&QT#A-cnp+k`xQ4gTrn`2sOT#6kL#f(Q zlUzcUYNMFdSP>h{!7T|pVe1=*YmhNhKsmR4m}kOKmSS8KC8=sXo`&ldH3(+&68HIq zHlpe3sbJyMKOISt{$ShuxFh=diGGfXfbhll-U>Ri#Uomt(13plZsl|*x|i{OhOTK#LtS@l?fK%o7trnhiLCGIkn?&kH6QL z)85+*Uyz!^YGf;8^Y=yL2H*-s%?TL&E2KoM>Ns}8 z2YNlnXA|NYjj<4{p)bRXBwQBs^cXXAuL!ux)?-P{F9#zf$8qZ?Bwj~*Y@bmlPQZT zhw@psxE-GO{-#$`3uh0&yLt*WANAja+wT*kWd~*|+2F zMH3%N7%3*$^#N`qS*-l^>p@HnBz{hqzDevzaTPj087t(H-*57gfvc1y=!q4p*^LSq%I)H7;m(N=4Uny6^lZRuYHj)&0Vx~U}!^W?(%HH>ec5%SfVIdEon*YdMz z_r@zMcG75j@X9i0!B$TImh!6{Jugfo(v=FTIy25PHY%<+?y_E_$zZ-QvKG~83DPS@ z_KUThyo5|>&9X|FXKyNgupBt_g|(s6bX?HQLFjFTO4h#*p4wLm1zx9p?I&4u+vWr#;II!(1DP`;^96)!g2*gXLR8 zMm za5}I`IgpwcEO_)f&V+!Z`{`3zEqo*+Nb>MU$9j{8 zdb)R2bywsVI%A_AxXDoWUX}{_$Qrw3ty7M~By~w+4qAL*F`USN%N_nW5YZjVj}HDG8^leBl#!NTK*@?Dwrc-$e>W`be3E62T<$6fnNBj61*jYN8B?Ie=mPueBXN80Qd zlR(i0e^Lc`LlCF^jOH`qXZC(2(Ycte>8CE}XDNHsa(g^rb_Z>J|8cvKhOENiwT72H zZ|_2wi{l_)%@nM9orsoa`%s!^H<$)nHfi-dmTX`G5H(5^JPYP}Au1-WE>mWu=c}wF z@`;Rj&e?o(0`(O2+&uE0b)LTAtqm+4?hS9`P0WuWId0#5TbZ})do8yHMvJbW-GBR{ zZ>cN_JP0o;St4@&@j4qKl?7QeBex%)dxpfobz9@d>O0wHgj2w=Zc2;wW#fr%z4Hid zOx2!TJN0y+*6D|4T2lf@U9judEy03=mR5Co10SXfRC^rbrA?7o#x-(Vb3!mKIyp8r z&zWKvPe#GIWZDOvsyV?V#mGt~lfc{F3ar^$liX3cM7Ez9gAKE-k44)hQ8`8;g67o8 z@+wE?1cr6t+Fi^l)|gH$q_i|h?s+!Kwl@hcL`5mk{gMGX-7^tt>4B0wZpoU-HgIPd z^Xl7(-pZcwYa0@%Nb3?qon(op`%n=h1Xm!~$dn`#Uoclk8(n9dVFd_ut#j(Xpt?qt6vEA zl?t8!C$&hq5-qZx>R&H^^q^Qp6is^M*};9 zL9Dc99WugJ2%CK`L{5^SJyuf7a7teTry4{AQgS1+7BJE!RKSk&3Nv&8o23%Qub!o` zNs;c&Ukjkta&)`C-frRgW@BSZCa5ic(u&USLLP91Do8qzT;<^P&djNT*2Ao$uwp1Z zoC2|=ljnQWR?4>(rwxSVF?&?{6YFK#Fd)4@vT(C_VJ2Bvae;z29`)g1?fNH% zjAdnLRaBY!Mq8}7j{+Chal@c*fT@w#Hps>tNt4<0g(}OS{h6|JrzyQTInqUR@N@AI zI3-jaDqJA?sX4=MVTY4PA4vsyjknH!^yEMROHc!ZaSs#TCe7hler%Z; zdZ&pQ-I}UK91A)9IXkAg7N3bA zmW&lXierhdCL`y~2tNNdO2vw15% zEVrUxMxQy8k(77g9D(I`O%z{6ky%!TU9*MUb3QawbWQX*eFH7;ayL2f#)X!Z4%>tlozev|xh_%~j+|2o#w2#O7Tn0F`XtEQKuh1}aTiWEQOyHd*k*VfL@&;n zAsu)DsI-5qE5OCc5u@}NIXoq1wcv!M*a?n9d%@)#mCx;&`6StlYqFYMNqVswq`l+# z%;p})I6I&|tLi$X@&(u(=K0C54dtnSxiNZ}ljjjod#AX%S~#V$Qs>R1kqrn%lVR6c|Hq_aJG~w@!o7XzM(Hd#O z#*~;4>&3lB*Q2V96fkA54l1awTx@BEA(DaK|y)Hz%uNQIE9ptj14q-%YES zvxMvG>9=PH>4w6IIZ(Z)Ln|=7Pap)R5DFpHp>drz67zUnpjvYgstH-3Odr0+rOA3vY3dYnFeMB~bCU6+M8odh7Y z7l79*`cS{6WYYoXVq|6$_|*dWius_d#{mC9n!y zno3N<%4E?O;q~=X$++7Z7&;*+z>>i2ZogPXssT0CRPnq z^mqyLWeE$>o%S_z1{2U~Jq5(IM~e^b*AH0r=63t_*$R4IhhKl!ZTs-4*LASm$@zpT zXw&yW+K|(&AQRo_iZk?Up4O5Ba7`%Q%`C*2Mh;BYhH|#V03odxtiNr4m#mt$58LBw zYQX>;iFG@plR&t^@X4M*m_WONbovk2B*7C#S1E%jslrhLHjo06LXDNXoh2qJwgVV0 z9nH+v)|wEmeQEW~BQ20zBHo~)Wg?zVrloVl^a#gq_ByH#>j*h(#FKzAWv{ezIvee_ z*lBx~?s+`lXy1u1*_VQi2j;#ALJXqL?Mhq@+MW1aw+sd}9sBdvSW&qF$AX45lPg`G zFixX+z`SS;p>+SAVI4Q90pZ9I2TaZ~fGI*K9VD`oRxMF%!dBx8W)x zv2&@(&Ppf#$_|LdZwBr+Py4p4h2%sQQ|4yM%d+@p=*k#iLhw?sM$ap`Pq=~o@^Vr8K;}C-7wV0Sg)E11G^{n2x|_w2?ue!B17Pj9FZCPI2y0XV+fz$z-PE5QpL@1M8-}V&C5SUf=Xfk= z!K?Hr*co}wy#>t1hWkGWpOmXl{sO=-)1fcf8Da_{i#v8{hO>2CVlwL#oOio+a_pWu z7ijzX;T}F$FrGsyGX*(0aI)0~6Od_Hy*wb}In6k|tQaKE(4e2tivPIRBoF)Wg0_$( zhP5(0Ea|d}Pk_|RW#lgLkh_d#V}kI)PO@SdE=^H`5N$t1!?EsJ$At|qLir?VhQaD|a%^W|mUK4M)x^5q=nVzi{aJDVE`Z@x?=jD( zj0lih1_arti?KnGGGnWY4QDgV;D};@WLDADq#;0d{h<8ogAUL`s+ecmeS7Xa`y zEwqE-?lEB1I6$h-mfVfN)?GB|GC{BWoH=Npt65jOq`o%pZ8AnvCnG9{l9T%YHdt&& zqb<7aQd)a4Lc1kih?!A@8MtU&D?#ey2Q7^Ha>#WbPJ59ZS44p}&HxD2)mUrNeV$LB zNryf0Sj*{^F52sgxQ6nn7fPp|xl;^d+QnFoJTkWUlx3mC!Zzt7(j+R!rR~Po>ewbg z4*N2s`qLt*%eRAr=XiQt^GoBq^)O>)HuGijQd;`sSvJiRpn*!Jr~oX&ehZP`-={H3guVnJS2$X~hS&N0!m+r4X>Xw(le6FDh#;@UU#o zoppdnM|OPy#c)G>yj59hkPAcCIb0}uuzmqn!amq8eE|v`wiPf%9kREV=p1yxPF$~L zXFmVjJ0K223b>B_T7}KNDKAEE;qLbfVA0}VMrmd^tG-q7B_@--YM`1F zWD$|yr*!ualA=AF$t!AKm-!+q1+-f7t&A2`_Y?WMvN_7VC2DOisn*KuUN*6(jVc?o zxVfyW&Uq-@@#}}j`+D!}n3W-Yk4BXIA)dySDrhK51u{4r`RgK~&TCO?{ z-kJ_n3*e8{xE~ML-}@yv*<0{f8C73Eki0VHSvP1zxpFpJYpF*34!&mj4Sp)QdvJtq z?0)7EFd8Rdu~s8@ZSz&PaEZV!6rv4A^XN3MC?w?kEEp=evf*A{tIq_oAM4Mn^#B?> zjQSG?hbux2*@I2B^*vifTxW3Dth9e1ZsDO7Vk0H{DVe=G@xXoIV~$lLeuA4tX`dOE z>B6c`ROf0ud6j8cO%o<&sQ+R2AZcW!tE`m~2!7gdI+=OEC%)Oulg( zbfI2w-rwmp{{`4uUaoWK?5_Vlemw4R!VPb!v+IG^6`pV$e;*I#Ti?|CgxKS|w2dIE zM%GS@XvUp)(7#3bO^)%i>s(jWt|uRf&z@|kHa=M+{*3t*yrxA*Ld+e$(HTSMQt{yOOz_(nqI?mNRQnTOE1lWGn2?5W zj>U!B({G9RcaH`yPesGY7WXpPD7zrbga(mN=L;z%NDM)3?S>Iyp~)wwhX%Qd$LPbO zm4RdBS2T~#pU3R44nGTzvE_7ZN2oHNU5<&O_UyEK+AP*n{xgKP@4L34=xkLEMXOhF zb--RfF*I$z$;TAABvm(vaZJEY$8l-aqY_YMm=HkHh`(m}-8YSiCSJx4HWD3F&c zEjL3o=<2?9feIn3MRkziZW6@fSqDj0y`L6*AoM@V$M!wi7?GY*%im~=q);oefh9n( z*wHc`>@LK65K@PA)_;qgf{V@!i6{k(L~cCK5Bwr(?;Pn>OORE!CDUG68SF5nP+2BM zmO!oG0%F1wJ13NmxG_l3{KF)tDaS=TS;VTYLDcI(Oiha|PO5VKgGZ^P@f{d(JmBTU zvnS~%HERLSEW)p~k2-q}QAA7qNw1Saw^|0COr|u!d-*9Pqj_x=mm;yf$K*s*ATM<~ z#U#i#L{|&oTv(Whv>sdbUbpgnNJkuKq_q|ubGn~W#PPhD`&Qp1(NRdEjpRMd#f)pX z{j+|jNnj=<29YQWm(_r0jN#YY+WY3M!`-};-t8Y82pr6rM_(t}zY)-#fQ{NU8Mf4s zM7_roem~@;ZAjK|I@c)@acCj>3qTj}D59xLNK`HoH9u$|%v8B~>*9eMQNVi6gBioW z8R%#OItOv^kU7sER)EZTIC!ErP_?}@!`qNJMA)u+b#%;X9nr^kiTL~8QWp&`S|A5L zxTWZcgBE%YK80;*_LkIz_o|4}r<6bPIhZ>0FgK50OHFKJ8n*RQu*?5zXF)%%=b|Avl;yG*Mz3T2Jxx-3MoEZ+c|3pbr*?)t+uK5U7E zC(bNfFT?ab_O#nd){WxAWpRkzXGpoz<2mtR*=!RvK z5O!Kw0YoXs3(~$xrvdwFJ=}W@)1u=s(MSc&%1u9sK9PV{p;kxpss-`-S9`2V4RWnm z_jI4YBFDWs-uYf+Um-Q&bw69=F<2a{o>gNwWYtf0_^}zzAsczNf8`p+^@c8Ox-bbt z$iPJIpau3AX^g^6n*;)VldI8LVn0f_&3bm5<`?>|>U4(;Ox$VtZSJwqD|n-_t4Ju7 zn~e|rn(rIl%e_a$uv7s!tu?sjEv`Z7^b6Ta545dVM*HD#0z9PVmY(zNld4y#dRc9b49A`)CoRTJ zuy2cJx`{W?dGuUS%#caAF8|VouiFu+u}f0Z3bQ1D9X`46P=L6~g!3S}-YXwAEgTMo zeJO8$Gq0q%#I)$JVUOjQUkMiTuaATdd#SAyB7&4{U*no$k8zL21iPgCc&qogQur)~ z&F+Kl7136qf_}SH*VkVFC7mEVGXo92E7`%awO%txuct}L`wvnV;vEur8vc`p5Gf^; zd|eY>SA0HATvFw2%V>F9lMYDM_s9tc)_gZ4Svh1|M+V8kHOV6qbiEQ)j&Vz}&QFD^ zh>}85mhDwm(LOh{RLD+!RMyi5&gZD&q&mNj@qN0-- zjz_7h`V}SfC#B1()w#yc+Jij5AKf6F^QtAeFP$F3P=XSnw12lJh|P2W3ga>*41g z1n@QROKYC}%=I1fy3$wuWWC%&bbosG?5m#k&(nQ;GUFumwA_upZ$DPAR^I8yQSiMR z=1^c@VjQ{0>9SVi6o(-@0$8x#;ush$H^B+JlLtjq3hWw7cyTM)iGFy(MtIm*c>E@m z($_iI2@4Xq^}ROI$36n9gw9GI&^R81Vh;rcV!$3o($8?W<^@t1geJVg_6r5J359sw zIQp=(mMAkkv+lqJ@2k`F``~B%hQ9z(h+duSIrR{BR*dteUSZ`4R~^2Jjy^vvbNp4R zbbQgGWZBIGt+D#sP$8-l9CvOG=Y4g0U3QL9$u~to;)f~U7z~4d8yN^bqVDtcjlu<; zWSTysE+SD|o?vwjloM$8C{pzlWNutm{+zNiAkXN~K&He9*YXw6)#VH4@d{N|ejBw# zwA~32pmR|yYjlQFABn48Kg!diB@@{-A{ljN`AIThr6$ZVDK4ebmtp81dcVIsG-W)N z7*93EDW4jeu|T3rHmpnl{xS0zX;*&-HKh(#<0q%4VWn|YaGUmZ`HTm-+!9HJ2!msT zm^&u?+JDDC$+1+UKzaRYosdGBRjM*#Y4^xE@+9jf_xV_0E<%y<65DXaO`0>X(!pNB zNlj8cAVlyRT~OTnj11)hduEtdRZOPo`;{Xi@wkZam@0V;u8$?J1!~uxU=`*R+LC~E zn%?esghOC-2?$|>I+aX2YP?)_43`%r;qhSjS1BIXyh2lY~Wi$aAepr z->e>v6v~Li41tFcSWXVdsJ5~zOy7tcUbaq|(K^T9AmK0XD~9)ghCjm})8PJa2hdWt zPf|=E-}!h%E~9(J>M%#4jtUWbAH_`p#?7?xbI@XNXQJ>5!&0jd`yF78^TnkQ`m?)R4o-xDnwz7XW zs8p=w9etN8GNrLyWM)Fa&spf6=_pDxBijBdp7P453LUTc07oW_30H9C8mb~^Z*V3Y z6Nl%l{+OsH1G8-QfQgaQoNiv$yZ9EIewX!VY)FSck=Qn@A`7DW;_kJVM=$BYu$#XeoVw4o4U^vllQ5`aoSceol-U|+o3_s}jo>M%23de=id zg`Gn4nq{bjT4b7*sm(RRjrI6L$1pe!JUJ+3QDjg|b{RKE9oAk4O{r8>3_G{!W+tlf zFkyD;Y(`ympp)^|?7UOK-*E4 zl})RRQ?5WH*Yke5J=kXT`P0OyTk~hLY%}*BXCaxlzRfiopFTWU-}m<$wJzVu5~H%> z9IFY|F3rB5t_5dVNImk?DE^I0`Tvnu2b-?df8bsn{R_|}`{9=8#p7gSib9Q^t)n+# zwsav~@ENQx2{o#BOsQKT5#c_`dWT2%)*l5cGhgn@n&ppb=U!hA57$?spQ4s=dY<4g zCsu&I`~t`pKs*~}9zrZLtaLg;ius3JQ9lrLP!vwIiWNp7SGzC_m*bfwVXMASt@#Mm;q`gQ8GuI`IxOhXbzenxe+PWl9nvt1a*(~E02JX64H&Ik?)TI zoxMqcCppG1oqLzAvDxrr-R-YK&Q50$_m_^eGk_O{cR!@xYc;8AU(8WZ8IU?;z>B;{ zWYcgD+EzmN)*@>yl14N~ewwPg#Dytz6(Lte2ZN#m7mXbNNiRaN-2}8PuQdv?|)uN zx@GrNtcB1no3nv5!V?%!?R1(#OwvQX3&`k+GR0%y5>VIm+hi(tesFwIoMKaf&aZ8Q zZV^vYpQzo*`QUx-Odn?f0Hda&WBo(m1!{?55d$Xm+uKWB7hf&n$h!i3ziQ91076V4HnZKH=23$)$bRp+{wjnamo)I@Ap8 zES^q8C{+WRuf$AdgcyW6M<^rQM>52zS!gp%5X2pZoLNlcu;mF=TMpiA{G6J`56}6Y zuq+)F1Rs`>7w7Ms9Yk!p{IcJs-Naid%}E%y}F5s*?k=K4+VTiu2; zqR^<@z0u<)&$K8#E3>kVu|Uqf<2%&AS!i3&S^eklN^?eR8)C$8Q%3EPRx0mb+KKx1 zLsp7Ut9^|dH2_j8k@@D zf)5M@zn09~1H1g>(h1C9Q8yyME#9y3V=tuy(QdV%sH@bv9ZYa=5lmh?O ztZHgfgL8vl0Qo8lp7pAIp?O7u5A#OREk*o2=FPiQJ`jVaT6y9Ipdsg2y}JxEvQfzo ze!Z)^ysxxB33ODo#m_cHOnP~210IP+I7FM+rW8byuymI{!l`!T>iBr4puvw-+RjI| zhrClfsrEtg9kxpZPg33oLKI5b{3)`Yc}8$wDcJEcjEUN5BZaBrc;0Z)=LBEr1y{v% z);mp<{>fS2ne(tnnMjS|AM&)XnJ1S@JfWhQN!aciV+1tLHKw(O>*%O3PvV*s+&Hza zAhyaE?#Di|T^~b=@2XA5)^m8Xc#>y=?2FAO;McYbcPCsZbGXK)Bcz>3^9SQ9?L1-w zUS-wZX?s;*LuG-paGB-9{K~2`I5$Iy_R40mMYlCRN@bwnea)miffFb5ZHaoMkH-^=foii( zi$^{lG%$Hv@lRCBMG{b6-#@f0Xr>d3enp6IPInt;EzS`_NOi^{~ru!n%UJmah;j zv4c{CxI*@!&Qd>NDdhCG&AZKOpQoK@qsdBLh>RWZ0gO>+sA#hSAHGWuT#WP4T z$cRb1*T|YPV6{u=5SXYALMV@Y{%RxKEiFQqrB=IVMI$;E9yl&PEb#LAeQ+czglOE; zMj0VXLQyglh=M@?`WG+k?jmilCJ1A`d2=F@1kK)Y_G}vdgL^C}egE0`WvGV8NtDT^|$C)5ZF9&0AM5u0mk1U1cH!cIkqKn z@&{3R{|?>%;{K}~K*GV{pI83NUrUs{e{jl%{VR@t?oY4+`V9cg_yhdA>ks>X_mTvx zOt2G4^N00+qDC^W z)if=h`5g@j2h@)P2>YK%{sEWaU_g5Od(HT3`2UObcX9xaHi;PXcI&qW=WiKI>R9Dw zQ3m|~Oa%QmkKZhR(jp+|7&F@JjX(4Pr2(LR44!@xR~Z~08AOAE0_a8IK|G}}c+de1 zQaGXUilW$%`1=jw<^*=t5VSltSNC6Hu-75X%B~amB+_3G)8OfB^t!URQAeVD?|Y{3jGt&=3XFy-@N$dH$~h(FtBu*R5L^ zDfIr`%t#C-S+?peKmWUQtQr*EYr3lO02V?S3<4+y00*Jk2M3S{0ED7|q%lB}CFq^9)VRNDa7`$;Edz_~0SY)~(7#HAz(Yb> zE#cqMmg@Z*G{2*&JvSEhi*1qoyLA7*$0tS>_xeQA`X>$gf1h+L9O)mpv}9!eJ=TAP zB8gf1I}b=V0wj?M5LJKxK(f`3!y|?~0tkp;2nzs; z0YdakKopQ92G9RHVnHYQg9nBf_i1!9`s@G7@lSn_6n?K`h*0Dq%mU%Wh~5T>1Cse0 z`6^ua@RM{gOO0ep8gPmApBjWe4f7wKTm@0n(!PKQ03vCi-&WfGqQRWpO}uv|qW_EN z{}-Xv{K&n(ny!cb-6TgFWFLIwmUaERbjXAlFEXJly#NAGFF<+#g8-F1`> z^3QnU3TR*?48=+!W|UHxk2Qw@`B>r%as)^);aC4{+>rqG@APJ2F_MT?Sv3sAkO>4u zK`F!l0TBx)e-_T{A4MA=Nfa~fWj*_+NPj|zGxGyfy$*m-bXqLI5TJ20(!S(f9wo zS_T%v-$)b|_|9R!R=IyNVE!5VFC%|b*-p()LR~f;s_8#Sr`7!BI(-xU_5R-ln15uH z>(pyXCTJoMwH9blH_v(OKQjIocO{D_elmFyr+xd?U(8f!{D1e;{;eGF&)-0;-CF0L zt><6F|Ed!H$!h*Z?Ejjm^(!gjD_Sekf7)QQjxhp;O+o&rauVjJ9TJQ@)hf`ZD~LZY zO^w9^9D-O7K*^KY^XJ6?a5=*AbOoJ=fzVOP{|Pztz~7l3VWGNlOw6Rytv}?SkfYuB zJCnp|X8}FT4g8y7?^Gc?9e=w2eF3xx?B6n}3yY_*eC)pfICP^6U}jK!p2S1^@8$lP z11S$7&La;RFc1KZIbD{~B1wO)y9%9)J`75xCH%9bKN&gIZ~+jl+@GuDPnP-rE;dRX z8dV(t{hv09!l^Eu+Gp_Te<}xhNzr!QdG;FS)CTk)$*xT)>>TJR53y`v{yI(lSz*RY zwoD1_qbR5Lx=L;TB@7&2cFXe$dG5~4>XC>U$=n;0P-KG*ug&}PZP{Bf9zCH{9ie!|40Q$ z|1JmU{W}HmMyl@3zZ3sN`qZR;x>EfEf@fasA!X2?H~&JqO; z(3XE7iESn#T>gg%_MgoEMPK-16aLBhe^~_jhhc^N)2;vCmHRUl{O$&X838aDa{2{B zK%l?LfEeWv0xmjMNVFx^H87KK{q%edCFnF;8}S<`tU4)-CMz!fkH>@l>!U+x<8vuo z%$qxEs>-n^p2vIu!&i1nhOd{9vIR%Y%H8c*S|F|1_KL)#jlZ{@aea3yO8y+>$+>Sw zoT?Vgl#E2JRDoUo{;nsgA|$gnw5sP^?j&+ioST4}WjJwacw0>RMlO#WJuZ+2eP*C? zRSmcyl`_)N5OoL1aGLNOz80r)UVusS$loyNOQ!T95Z<#i7;sNk7Gekw}rXR@^@E*E_5mU_`H+8-W9PYt=i%r}^_AdwG$xqB0XF8I<& z(}R*=rpmZwIhSYDpD*LOc|yrtE^K}6X^H++XcaGudh-mVFPNbGiJ9E!)!@P$Z;tEt z=X~CaVuvlzce9pO=}7l`Q{0uIW{BkswHZeepBhmtGob9EnzSq4pTqm0uNK3WlmM3~ zDKQx;{q8F+bFRHf)yh+`<@wNN;jL#}%6L5}9UDWZ3F6bZOWTp)QxGbxQBE(m7a{UE z#{P2=COnWgh~+{?lm@uBaYCxTw+8wov^~ZT*0m>ctO&M5V#X57PasIA6PJ%XF?bM zYQrE-vUW!F4QCAI2{}`=fNf3X8>kkwAJt@gEVI&d#4Q+QG7o*J>&!eAJ!3x@K%Uyq zsR$sCvex=>U!y2Dg{dbiMN%)F!w|(r z_indJ_4S;>ZkY??3U} zo$A4hgV?`M3V*+yr3#345&HVT*u_DEgLN20l8njE$>M1iAq37nF3AsYRc3BPb#I;f zan+uPyUZvi%z<(lWAvENMRPdB>BLe6OBS3n!ii7~-CssD@)pQIyqrzRxy&KD?7HD1 zB*$GLki4Hm=GcxJFFU3U{D8Vy$ZIJBnn?`=9nn}-G4oOi(bwNnU$Ul`-(nEq5zg{x z>AsM!o1BZMg+8aCiqe-DN_s74wPd0i+lAuBq#nMV8Auy<4q@tkQ4}-}Xc00GN|0Z=H1vrgy!y(# zuJidcU8i1Av&+;uv9>sz6hpNP;{Ig8p00yHT&LLz@|??on#hA;Cez`5rg?GWOK_sx zn-=Dzlenv~rYi1usoqDH;O;SG+ucrIqF=$A#~w$S^Y1n_QVU!LVU;R7AU7n-Mc}*1QoGZJ-r<3G4*6NZ48Um7nIo2^Whc18?~fqqi-s2Di#&QV6^rt2CU?g?8mC>U5s!dSDo zMKNSN{r(}|=1thA3nQ8J%{IEW26%R; z`QewOl^45?^nR#2b3+rbre&~Vi}Cm1g;JnMe}eMfO>R3{rS5LQvlFW9PiNd)re zrKOoZG@HbGb-#Opk3U!zy?fO5dcr$(lkf=}$Fk>2Enf4;?}3V`b(El`2#MFyjbi?4 zX?FP8Et=&v!eob#^pY zw@;BKG{RInh1rz#+xK}|=_S2Y_zaJ@hB z>WZWIYSD6b%u@L1`uUN5=x16hP~ck*ol@fkHz9RdHe98C(n6+Y8`U03{4RLQ>(tQ) zrC6IpX6##%L`DIOlIr=+2&7~|@Jb!jJpD$mYSq7#II-F=qR%PZFZu}k(i#=NfkV3t zBbAFH;d8WxiaqGy&yBEW`6yEkeb(75T^3I5m;`~bhgRD?&$eo3kChU$pNOS5bs0c7 zAK8bRz$LQBnylUrQboi0c4sTlz|FrjiBAAmD(~Ki9wg|aAJ%CB6 zGS$~+un(Ng;K{s_F)>DfIe!Pmga=fwlM9+TUT@O_Z9#lgkcpm??OXi^i=pa6sEa{6 zA7WBA7_~oBZcV2rn)_J5wW8FYO1s=Hy_dqUJb~khg1x*s4@bGsjEF6*viIu6EGLXf zGL(q_((65Q7E9G)RlR5DrbQVLA?m?V_@Ejec;p;Pqu0Dd_tV5)TfIN%++1b9)Tg*( z{O2A6d?F$sXT`gd}N@^25{EE`&u(0FT?=!bA#F64?lDSuyB?JW% z#mD6tHGf`9VW3WP*3iB4RlW?&A|n<+Jm1?pk;)1c$)G*7jUR~(Uv=hwz?%O1w zM{i?Lc|JZ~B*}rD`|(~0&Dd~neHf}TJ5>9tz?$BZW!FH8p~=k)9cJnpI+^vBe` z8o<*x1H0Dj6XUHw6EOk=HnE)epxY92J0@*&ZmMv7GBu};`ql7yd4$5~E<<=G_+Fqe zMuiWvtIu_OV9j##Y0$*xrnK7lio)EraJih(q2rIuaH8k;%0#wTFOX}7!-@-r%-bAM zr=G*JEyFYEH7_sg;fAw%hj_mzeG?=F7<n5sb>S5|sDy=b-6suw?{?$ewUX+_byak5m6aO;J&!UJkyU5496NPKkdN z?(D@NU=gX0vDu8{JQ6Ih1k{les+>4?+}skN>Fz4WdUS>5H9_w1CksX&%k&BU!N;QY zDvqux`X;Y^NeL3Zp11FEJL=VkGi&Z^nZ0TQQk_Ret7h|jj@E1M%fhHn@z5)rk;a5zb-3iq=iiN<#?Wlgui(Sx_aC5PV9u*!!&gn0mGhU8#M zQE_by_D65C+Gb}oM;RJ)AM;b2gYN-H|3}?s&F(VS9&VQd{PBHN-s7oo`ggZq zZ;Ta7z$@1@&lmSz0SX_MNyMQqK$07;Fe+rR8!M^y>ToL>op%N*K)e(TzvYsK}fNma_ zQ#h$=nm#2i`juH9MC<^ItM%8@Kdw4>rw(pX=E>4p*6eIVEx(_HhP*y5a59-z4}=MZ zQLguV!KzR6`+@o#OFa}qqk&mE?k!TU?5wU#ASJY80ST-tI0MiS;Y_25A6nG_OPO&f zjV12Op07TOoN_eP&T+!y8(^Gjv?N!b6&5O zoRJob+3SgkW`jMAYIHM^7rH!*DZPQamdPY$}Q&)(>c`G!>uS5r6+t(z(bl!($rLK)H=@iNICP8M|m$Ga9zF+2z zxg!p^>z^p*_o%&nE+v#n4EIbb#unMzHb7dV%xmKI3t7 zWvKn-!y8%<+N&b_pJQ&F0+cDtJ5g55-uTkG!R93S6mx7CsY1b0s`+6{Ay09?^UK9x z?B!l{_PJjdPklBe^Lo{!xL>P&DJ#CN98oA2h!}T8kTv+?`{Ae`-hN_6zkvv(pNh)|D+?+WTO1sv(%B-Q1!GD6;a)()m66VS zDvU#(vY1zww9#&!XZsDC*ubfM`ngJ5o}EJB$Aegg{5ENVRh%e-Dk(ggC4$M07c8NC z-nc|ORA`m7X}!V7qK!hg`F6Yp>qH4l(F!Ghk0+0RKCu6uV2qWGGEFdFGMox(&Z9tC zux+r(X6b^}DTDb)o<6c5+@k{1swIYT*(oIR)zC-f48Hrif+yc>6_XjNu1}8=Btj2_ zEAagwW!ef*MLjpbM?2&T+KJA?_Vuzctgv0u!opDSXT3yk=9#zRp9mef@lxw7NWz3HADMj7^^%)-WCOq=j>C6t2f5h;F^HV{ys$wYB9s}z;h{fej3eP;t>O~-nUGSmZ9 z75ote=WJ&E`>7=q=nVE^G_;+613Q9+K1N~MrG{sqpRb=RQWm)R7$n+4Cc?pHU+aUo z-d(90zPKJr{{~{Y>UI{((y(J6IT(}hJR%jlImhu>#t}%=Wc`vx@Y!I<@rLrb zv-v#(@ZzotJ;#tIuJeli+4q9Dj!o+U{Z)TB_zyohfqSN1ub zSi(v@kDXBF@m#TDm*j8M zs^+>oc{D9WYN?sEde@7cE}%DELwY{blb%)iTljNZM_QGhK*=%&U{(_=4xmj9ME2HU zNa<_A$Bp#M% zdr|XSO0+qaCfrZ6Nvfu~3Z5mD_?TEbp{;3ms33OkrgZN*`#LpWuTSB}buUjTm8p6q za$asZPJYS%c}!83yP~-ZwPEuDs*RGX58dDR5@Kw=!f{k2$YL&8C(dDWq143UHy{GK ze*Mm?dV!DClpZ5>Z{)!5BKC*mPXs81)gDU42_Iatq{$T-y{6yrvnCnNv=)L^d58;R zSaQ2WwRKE?H;G+_yjqfH0Cxe%9X1P6x@-LP_#_chfymmk{A8CvYMTeDQfqa_ zib)dE-NRcoybuGhDkQvmtbp+a0zLy-{@6;{)P!Uia|2_ER=sz4o4|Pd;N=w|`}fz~ z&VcVqD>oYB=6=3Y`$Drz%Un%nv&Fofd7O<_;unlpg`{Nn>Cjo6$mCM&`Orql_JuKM zjVWBke!suF7eSTBnj{mj_%n_-XWe-6g_QeXQom#OOEY8%n0^2O$3;&6Kyyx26c|VScay>b~&eFM(W9F$ZSAr5}zb?ns`%u##HfN%|WM zTDb3`E38}Imw>zaarSOF=2{%=289m`QOFe1_mfYw z+Ujpf$GD7k()uY_LknH!ZG6@C)n#;9VcVESC3FH*-OB(sE(AS|+lwB1>g-KcY(?-(-5AAW~}?mT21Lgk>hQGF^q%Oum}4 z<_H%@-mpAV`!G#B_?(X-UAc6Kp0rm@>kEBu$!VFqAxOu=h&V%H-=X#q<5ruaF%x|G zi??(Zd`QLawG5{ESQ8<-7UZD5Kqqf^8^lDj9KOq@b9;=+oQgij9YI@=)`#a99K`Xy z^`le;DYI&u2;1jrO8er8uiYfx@8fvAfBF>7Z1WzA*OUz9k2^RowVkuNZ20Sor*qrc zi7~ExP6(Dlo-U4j`$Hh>^@YbHH=_!PX-WR%JU!v6t=oAE<%Nq&faFW#z8gQEsK+C{ zK9xKo6tz2D>1Qxz;b<%GqHtzTD%Nga`jSxeeL0b)dW*pOoE@m?c}5LUpc1#?54(V-~sS(_Tx=Eo1LyP zj8+?a&n;S?N$J4VkvnH)1d~3oGsHWuDdlCE(U?^DI_VS6Nkt_P>r^+3)2{8g8@~_A zVWuhIFj>D3MpsF^I+n+@{07Q67J~XED=CUK&gr1jGibv5sjQ!?miGsi+KveW2rcb=K~!m(26ivN;!yK zsM{u4JiZT_#RLj4DeQ?6g7@%o>*#GnlH8B&34HWSWGCpXxNwyxIa2Np+bjG>?huWC+U|*&SB!TyM5f@`pDBc1Gy2 zr`UH8mE-w}94m@7yniXVdsYUgwOPaBa)0fW?`#26oh>B>cM5-NzQ&qs-*y!{7@4yL zp@%|Gp_9chd=YL0HEpIR@M1%0X4-E#b*P&_x3zLIOp}TzW6KNl3d>XYQrxPIow(ed zyP|_M*w51nR*T6c#UErsM$s9kt_gkvns0Bx34Yg>3l$4b-M*^?3T4P+c;GC|jWY)# zmYKUd!XSqLZI{cQ>spp?g;~ysBN7C&jg7sIUbjNh-qH9-fmh>=&KgiuJ%2Tj9;M4+ zs$1v>7iZYPh0AC=KF;{wXY^~WB6^7-3={qatDW~_>B@$7#~9BW6~u|t4#RwHAlKS? z>vAmBF5vv?%AF=rmKZTeXvo;vGHX>|{x>u9o<*sbEg?8AM- z;B&&$bYLc=&(4u|z`AWiMXZ-8*+q(2&_h||t*=GiK; zJS`~hLCh|iE;;@uNqvc+Y{~f-o zS^6)5h)x!LNW!fPix`-EQ38kddzWVng=nR$ zM;q|r6YVeXoL?{2!Rz(IKCY!TOxxxf zhFw80ZE=RDO2d`smX5(-7FIfc$~mp;H~GaK-(}z9M>}rSvW6gNBii({1=*zU4Ql;T zEGAoZJJK~nYE<#foq>%i!lgigYiiRcyU((0*Ppp>Te7>R-YC1;PBja@)vx}e#W?bu z7Qpo-ZSSi%m>m$DTR8v;iBi7PI=|~1+ z0K9iA(uWuKh_|aR)nYzZkV_P!so5gOGxmP401fws2;xJ`z*#!X95BhWMU=XcNXIcLHA@r&Ox!^j&(-19~26(YB%u6$o7Q=12aw&8hyGZ zT1nZNths7pTeBVLMt6Z+>SYkTX{^pjfaoB0=AuuRA6=bJPHG69sQMY-NHbBYN5;!j z6q#~r-yc(WBoZq#bqBQ9M}E(H;JJHzI}$- zUiyV3t;(9|$?u#hQt9k2qT(BgYBS!Vi{az?4YubCbbMUQk|}Y47rAY-yI*QiI^WC= z646z{WjnUb7B5_UE-WEan_;>QVt<D_Z>D0#FYSdv0XU-eqpr=sZabm6)V5wNtO%?8T3r|f`dis%&M6e)N zFz_?L7#aN7JhtTg#P{{3&xlDk?NZbgB#(sLYJ_Fj#TQh7E|g3*tl%9!irTCt6I9I9C5! z=2Vy%SJ9VjJ}yDw^N$py360^V80`yE?Fd3md16PE4s&FeAL7m+C=QhNVfV1REk0#M z!svL8ImC6oUcsJF4{$E(us^xSwT#SYhP8PHThiR+8zO|7j0wKLk^?ngZE$aaq3LVP zQ`n|tx8$zYSRQZWRnlh~f5#<9@4JwfUW=20)8t+y1861=grDDZCdDn+I(ait!j)Z> zwk+QiXBnDxi?AlppP5wP2E9WtC$|~98Ke33st{@EvMp$k)0L&&<_r|&a=aEM0@|PixbHhkH$XG)QdD5PXRzl-0+4LBeXy1@X#LFACwerN zRL`IZ@$-!k6gBA?w|%)e4}j6P{#NyHpYAG9&jv!>Bn8{c8@=93hnXoG+}d%O(^_kNYLvoKeOaI-X_;Vs zPC+#_KYa1M3&f)`545mx{xau=&aq~-0vt{iLzQ*qY8Id z2o%lQ|D169t!QgRyIF^x@=Y1=W&(DVR(E*nZS*)sDDBA*jp5-(L8_YZk$miVsrip_ zJc3FQ7)g~J6!`Amun>`3I@kVMA8e<}d|HRC%wfVLPN@!NqPak~+I?jB;gYB&bK=u= zP7Yyq$4=oXd2O2>QI^0t-_+~0H||A4MU>`bInKtccfBNp%)uac3(dS?zrn!`)>X|W`0z3N?!10lo5UAqhb&t4*QXw8Ge%i-XPOUrh(4Wsb=a5- z^jVeqGE^9fgbVkr6+zm_SxoP%{(I(OK>U;0QP8Ihb7N@a^J;Vp(D=-iUk#KV8O;XU z-)UfqQPJ|KHNh=QmmVnjYnkQW7xh&R2kk>DTPDO6^Nt~pg#UG z4@s=h@jKBEe^HHOjo9!!$i3PffR6blOUg6#Iaiu_tOmpKn4Vqe5Pm48DdhI-`!g)R zhJ(w2%-Q#XNf5SAk5w)%7tt;-eSQ%N?hkBdq}gJL)}TmsN=!NY4zzu(z_MJEyhY-o zH29`nMfLveni)y(o(I8xw z9yu7@1fgm=rRG#9iutAsr0;(Y)Mi!M-X05)Z!}nrg$$wVv(^~q#Sw?T*V#t{p$w14 z#_+vwshgP`!`vb1J_lb1Wa^&;{>)i?sw)<|l80HXq0iJ*a9whYD<=KY$W@`6Vke`S zgrJ9ZEKtldy{My{${c<)&H*I7a|ujwS>0Fz$z7(Oydu4L9Rn+%an z;Xy+dmx>q`F{)^`;f{pP&g%D>Rs3@XTLk$t+6~f$*A^-E!f4)NmVbQ+x}&?5Ds;B< zMvSLf^^#B2lN(UFmt_5H{w}|(<}!ijT2W6!>gWeNxVyAsOs#1SDTRl4E@Lch#H3BJ z*D1SsdFJ)(4$hP;HxsV~(kfxy2)%@JVPiv% zu{dm7lkQTP{0s+8%}OPBN_YfWNRYu%|VZz;B z{ZO`}ho-zg$!!~ZK}S`5lI%6pOH8Ekx6aVa4oe)y^a*iwA@iD8i9RU!^%qlHX$d{?*=pIY1;rLH$jm68zjO2&Q$@$?o%Fx>d@ zqR8_LkjXOhgJobEx`Ohk7ZIS9;*Fn=^rjQuz%g#30l=ByiQ zK}A}s169#Dj!F=A_`^=~wSJC&X>{q!x0>|*uUD`4vQa!DaqJ!=qc4N5*Olr}gWWHe ztYOqF_qZ159ApmuuJ zkh2coiBY11&PM6*Ut*2iZZ-qYB6S5FlFY`x-Y-6^-O1!yR87j2R&(@6g zcfz7=EB-9O9It-bCM0 z8ED?rJYLk3NqsdC))GjQVSb{)ofl{qA0%Pks>CvFibMln< z>*NkO3v(Z=C}ZqcOhMnYVL~|@J(#|7zLGswd1R^1(xv`CQn16oc!xB1^dj=AbmI5|dur;F_FsH5x`&uadXFQTZJ0;Lgde^j#udoP0 zIyU4%ax@5!^F5iHl=MqveS0_s!&eT~79i$P8vQ&^6Hv>SzHl_ebzYX_r@x*K>_vBE zahi)iK<>DaCTdsW@`vF!ceAYnRxuxcR^60b6G+S!o5*sDUGj~BxS}5OxHJxlmv~XL zz-H|rj~;EDWdcVRcN_4Q#P7P}&*V*_(rvOm?WHM1SyB zmkG&RVA|W&d4EyXi{BbqpcN<;tcM4wmt%PD@LGc^asjG4om3eCsL`wVpZ4Hem|U#L zN0O)!Qu8aZ>(va^{0a~D5SGPXK7B!$yWyG8>odJDwJNC|!1v6hLXl5y(DyGt@M8Gb zpLbDVlY)ysQcEYL!|~mHz&e-E8Ypl$pckRULroT4g;H8ITQNOCAxoRlz~*LK=$Jn> zD^n~-0CSKG5nhnusSOhE&K-fK(~e!jGOlGBr=GCyLpP8+P+|6v=dHj_iCr;1-8t|?L@slmZ!n83Nz8f+$WM08&^rP%kX1(y zOwB}&4&~yqGjvG8;}%T$$+$b?g^uQf zDGo?`N2)lBzuMtX?CRz>OAv#H&sqr5d96%8rj2RovWS?Aw!Kc?WCc-yd#bdn9((Zi`fng|>86dn4#H_c`?)t}V!Ocgz|*e>?L>FT zEA>JK?C&B!r_VbZm&M=ml*!EK$NiIsF;$XtJ5_=m>q$0B7k=V~5!Cg4mRJ$|&!v!p=7%b$Gvz6Bpfw2-d-8d?2&|5o zkxIbavo^Jaf2EkSL^%5X>UFBpjR%GniYi(~~ ziJej@j`HAl=UW7E9?DJ$5g@ecz7!>#zDP3>Hjobwt1>y3xX4)^zm8;&F?^HBrtiyQ z^^ROUb5M0V7FsU11!CpfmrW=`5eG!|R zu{$U8FOYxH8W%==ylFt|HNv_KJF%JGG~Qr)83L2^0@rG*FuJ_pV>VDyv=dzbdV9MH z6eS8Dkx_s4rI*I@Wg@IKW?BIf^*Hs8PEASigIebZ|B@cKO3f?veVQYW;GpZJ;dZ=T0wLzp~LpMW{ zpS4k}+uWjC38+1tX~~wx&e?y zg?|HI_W|iPF{o?5Jsss$UzJ)$o_JG zm%k*e)osM24;ofvG-NT@uF)_g=!NO{4n6Do4JgG*?pK%=)5bc-@nHRbdD$_@FK)y) z0a=+dvTg0AklPM>;{-BFa4M41aC#kj>;71Q=Rs?K{MX!TkikbY0*5uc94uZ3n42+! zXDpEiNI6EFf|Uu2doCuxz4+%+nqZglk4d&cT@;!l*R1(?%rP`8FX1kUs|xyhXsU?F z9)<^go_pI1p6IY)*SWu>FSs&Mm?>*oY#ED9U1l=yGi$WL||AZ12<1AKeq{wzj{s?iIM}U);@wHtSE1uXV^7uYxv{bb@!KfvH9Xgm| z!6nPw^41v71bb?G2Ttf5{u`JsxkVptv?zM)TxUH zW}GGJ_&dr1lzA`Lx%0+Nl7FPcgH3ffnYX#X;%2IJ@(VqlAKCH+Zz#oBHZul1=6()@ zdvgv?#^sE}uMEeJI<|tC%q}#?F$N>V9Q$6uiQ5$C-Ut!gJ5D-~0pn?v8$7zda-}`= z&s;8~nDVDS*`$R|nL9d_7{2@%FC+bN7}H*j{M<45)N08`rz@y49FG>xnxlv3;oqou z$i!pKTH@l#lV&)^vhT`ETA32A?`FSYKWxX2NnX}oq)hVF5Z+l7Z$Lo zyNXPT#QGU)Y|rz+JBXPAGU+rXC876O3sQmt!0#KZCN5mSmPnOJ+r)`bn;QFV~+wE;d*6WW3@a>13>Z@btHxt$3#>EJ+$ zYwX>RB;D9}>_A%@Z=zLE;Cl@syyIdnl#=6UTFmLHv#HkFDr(chogmoj%`P}iJaFG1%<`>j-v-(E7xNnrE}J$;5;V~&_GDJOQE5q0gln1-jXH(e?YyCfdg(0P!l zFL>JFrD^L}2ZtG_`bC340{YCaK0-z3iw92j<)|`8U*E>4L; z{oKO(;PlZD8ugbn=AbCTSyKDye^_WB$V^h-NJ{1TdV^fYXEJNK(;)RC&#SF^(UxIs z;Z877H0)>p zQf8&Z&sv-azr;{*A)w%DASJRdw3#=E*fTW)C#&4X-%U3$N#t@D;KtImItv8PFlGw| zr}EJ2F#DM%SzV{n9yKL0OF=;~8on`n1NbWy-WSjpSBk(?i|!@Z*Pf_yg289YC?eGH zDexiCS}HLe(0q_ryAM>}cTX>$mo%oAw^j=jD5H`l!oMQq49Xd+c=AuuU0kvi5jT>dx?+Ja z)Q`kLa@@1HuP)FlbX(fF53se~KC8GoM(~e-%d~R?H6PHk3pGA#K0$q)pJm|}Tm5IYgNc=ObbaeJ3!0d+TV4&;1 zL~WrCFSJ*~OKZMMUi;RQ0)Wk(d-*kwiTyw(gVwad5vNJ>hV|x|95JBAdW;#qf^v1C zxsbC%XllAr6-6R0hs_^T_)wOdT13j7?HG{4+|MRKe8;1-DKBa(*U1j`NuUs6ryIbP zJAx=+%)S)MdmtnQ=ntqk>uW;QMSd`b0>XG)-jdMu2#A?Q6JFaJFl;W~e;#B@ZBySZ z>#g&MkZWm{O)cwUNT7}e-a(skLHw-QNz>g4b;xD{B?ZTwdpM0fjP~1<)OkTF$-dgW zi)gA%3m(waWAMqVwv<5z(w&NN1X19@UjB%ZOt}gta#%KjAk3FZ^k`Xa{mzJe zv+TkpOv%=jnYwFtT!cn`$9km7N?YKEx!D3D*mubBmqn(b4XHV{AL6qFz3?dwe{wAHw z(LPtjMMu!iM^0T_c{kwNOuqK)te}y%wyQ7I_UJ1zvJjop)hAT~iut36j)@U4 z*Qf-vAt%vctqbVP?|>qi4J9aKRc4DIHP`HrO%?0MNXzRiLJ+ZZbs~>b0UGq9^%aDD zzomWS?!)xGP7G_t-Ze(@3nT7?P(Xo&dkyz}%@7v2kQ8IbIcgt_<&{{KimH?1nGct~ z@k05$gH00g>VlY-^~GilUAO7xA@U1pXJ0ndqo>=DZae1h;euSwF-CJ%AsS|gl{S)> z8X6~)x#;qv$D?g4)V1G**8}QAx6#|SQ|@i`%!Q~Mq>?{SYn%QFxXEs;v!vC3M!E1) zE{eWN+r~KRxneF{V>Lda@{p=IhW32Tz0qPnY^@qt=YUqJ(o*ht^vP;$0ymp$4zlU< z>6@<>Bj`qK8rWuC|32cB8z^<0C?TH??0T9{A5Rize*@X~HiYz^$cbAdPdq~5w z%(Ly$#C*~IzLeUIpaTPmgG&YhRqV$IL3fvqNA}hx zW~-mY)@yZwx2*04^VdCQuQ-dk^_>*1l>k}0oITlxaNqBn^#?IiysLtg?K8GVJV|h( za7!xL1q4LkHsEHVI0~SDeFrYdi#C4rp&Gj1U3 zQX4TicHnxL&a=z3n%=|IiAnDRvr0^;5oKS?t7xhHn-nAzKoS_6*O*6Mi<`FI1?GuC z7w0cFbOyHjz*3Q6{}1@> zgRJym71%scSR!p;C{7N=zrLc4aY)sT5XhqbWcmqiBgjD&nN<&A6V?C;?`Np#YgSOx zQqARKOd?8h_9VFKW-MQWQ{kEzRv|iGx@BVn8Ia2>v~apq;ChJl{wA9XZ8C{&M{?Yv z^h1o(w$QuBobat&xa$?oZfvP>>;`qntNZE{IAUoP{}rVFrZMxcwpI9u-Y2p8+ufM= zaNo!IUZ1N{K?RkANKNM(V`oY;49P#~cDMtkmOX_^Z_p+1Q)j#wgWYrtqhUsKC&rk^ zeXNy#Xo)qLGG{8A^_Ea750Ae9Npt2HFcQAp^B_O-VSe1CH4!w{rH`d1tia0q<#L^RA>m|Nta?g-F6A_MdkA{G zTt^P>>}y$qMzd03BL@n7zoEj!lw&hcpz%etNZk+iz`(7Mc44`Y5z6Q1F`|R zy|9{Ogy}@QwSJQEka=Jxt=$lwyHi^P>6q!DM8!b5qV%?z|Bdhm(o?wfxCs|ID>>-) zb?NYIh)^*_jkUBLOTLTA2N~}PydyLWvY*PWb%+1g>)3%z=Wb0&sNVH!6Fn*g5<%zK z%ED(pr--gT)c5IA2+sUb*_Flq$!@@EnC`ytKBL@7{IlaDHn?n8H`5DPDb<%)o0Me1 zn@L2e6in)*gYvu|@ipz5!$qvykorMFEcK*zah^7)9(z{E`T{PB@oIdT5)d?ofK0tI zXP2Y-{_v3{BRn=fabX@!vM%qR4(BI%GhMSR7Q@HbiETH*ej$Bu>6RE$lY<#P#khl$9D}?1ujPV`0dHW)xH%@M z>>&n0ry7*=%YP8hp_Ae17s4zcYAhp)$YR3W0n<2=r*p2jAXt<_Z-E?R4SPtdzk#8X z+E>h8EOhI+AWQGw9*|t1$jYrUMY?xo?k}m~=yDo6bs6Wc?v)oVXqnUo9(tJsZSWN8 zu(~SJzJ?9i^TNWZS^l5y&gv`b_G{zcDQ3tSLONz>Q5va7aDYL&Q7I|uk_O3{p$0_+ zL_m0?OC<)R1OpI|RJtUFP(lTi?jL9G(en?y>pIwH_rcz4?RDSl-k)omn+3%`cRcM} z=TyDaJqywKJ@hd*??Wxjgwr}<6Tgv#yhieQaF#E3Qy0h!`0;j(!YlvJm3B!WPL!>n z_$SshG(soDV$XshVXiwFQTN!o9hh*sW4C@8=HD(W5;^G!Ug&-oVa{%WQ`hhI_C5y$ zYhM_fY<31+$7`i~SOQXfRU~yA@9}QGQh3zpwdgyD55=-EyMf8lh&e3|Lgijtr-0NK zyYGpg0sWtUQt7=x5^GCup6LowjI}fP?VxI1mzk>uKTX9AvMrR4uchx*uas@RRGP;$ z*9Bu7;5AvjK5Cy6nr<#E(0i(=Viu9k~s3 z@mWxUvwn(>;(Nw7Vv}Jla4z+zlrXhLA;UHwF@;2RTX`kTB~8hrnC#26Si^V={AY>m z(;DdwZS(mVwf8n!{4ETM9iR)OPsd)M(XIXUql~sW!nY#^5n4@A*!(qaJQiL7ldb{6 zNFHf@Z)(gRuY%42@uEP^)9)em;M4go`ncCD@kMVmk??K;Ff1m{fZO-#CH~muQ3O#D z+Zo(lfl+GG!(?71@KG1Rx1F_Q1;!ZWMvrF{m_2QU%89bllxxCbDZx}d=YSk^e?GvN z<_^B@<`2bp9eR~^-l%`VkM+{&Lz*Fus~t|H3+fmiUnqzIH``S|`$1rNwVMWxItMhg z;FHkmmdZ~9mOPpb7 zO`Q=GjPQs?9QW=8V|-;$XZIytZpe0F>tH_u+~y{^W$sU_kqs!E18=544|-V*@|p9D zEP$fw_mg1jN_&|fU+NCkQ)sA>j;4J|Q5%9?cUbE??FEL}NB4X1x3G3Ek}9KC=Q>MxvtSyYScr z>q2S?3slw<4ESpVfFL~ntoiG6XA~$P{yp=g_Hip!l?k1xwme@DRE7uF1vJ#TK=XEd z)9hvg71~EPj)XF7;-zK$4sb9#xw|0utg@4QWD$G6H?`Pv zN0kE46~+5zq`rOuRXzrN;D_B46w^*|fC694GXhab&4X&~=gMSZJU zka4pixrazu)%vBXJ{C%lM+GU~NPD$Ck_MlTKQ2W6+K1Bk6oZ&qnUfGV_|dhdf!tT} zyL_@(<%Ax7H7}ji4scJ&0`W=IAqO2HEiBZO}#&gI^*vaVh zxPv)`T&KT;cX3uzU8SM~vNNZEo^{DF^9{R^U^e}LRT&UcX#`-KAwz~5PX{Tb2zaqh zo?3Yklg0hkLqYGcVR*1tm#!l6TdujtBu)1WZF4@BCA$wAJ`KO`1>EaI1Vi=6_cW^* zYIxoNUPBr5Og+*2P50K^S|I}qRhs2Zi>QBePnAHsh7%d2K+8(NLTW=wUPPmfj`+^_=&-e1Fo9DJ7@H`2N93TmsdD%?l2A4{{ug+u zQyrv?*GOQaA483gJXkx_x-F=?s^W@MZQ2H_5}@=sRE{6mgTQaEjJ*1qh&TT-iJUij z82Q_c1NiIzr#YUgvl1XZ1cEBtHYE|v5ny?``#EZx@AvtQooU>Rrl|LEFJ|2IQP?Ss z1ApwJE7c+tm4Ly95^6usJ+2tRY9^XPG|RO`rjOZlZ6IRqzaDfLse}xsPqN^-WuQpT zG`Npc=x{JfW^`|-BPO_=@GnIn0$SEI;OhAB|{^+OW@O>-vL8#W|K=Ei*hqZWu4-n zM?^H3CmkO``U;B<3?i4MyhXDWrPQf}=E!|Y zmH7s@-pgemuP8#j3UEo~0>Yzhd}c+*GuNAy#XgMaUiVwX7Zf(+D8+eFdy{g z35cqO>kDpSfirY%&zu;4SoQ$fBrC+{6xe$O~Q^U3#Nrv3_rh zG?dMaUc6K!y}I^nJ6E@=nmnNTE7Pwm&=Dd|ri$*$GRC(=nV7ALNMc+;ik|6#(ZPMe zJuPHR?h+6GtI%e!W(1l1u6*;8Znf@o_7JU;vHNspGI~UT>dqp{UxCwu$(4V?T_A7Y z>fwp02k?5Crsc(Va{Qb1zw#JRE#{_Tky5cJ*=cnOi6L#lsP9tmYcPw zIq238LDj#g+3%2N=Jz;J@ldDPLEzxAkp*~H+Vvq2P}%fgm!{Y^jGpy*Ynbpk&@`Zw zUf~-TI%%Mja^L<<>2ml``1CYhOpP!iRYh1SB!jPhBO!ck$Z-1k$58q$A%CTOol9Vb ztw=P4-PQ@@PCU6Q+Y@~^)vfUl6>5ZoEkfu3b(|Nkw~P6wXU?7?CKlHY40@lzwVaF@ zepWbxyzhjHkhD@G%Qz+-i|ZJEr);IzR?r=G3$gOC>ZKt1)I8LD9#3YW4_z*niQ?&2 z-$Hz;EuD>@y$Ts^T$TxD^@n;+Q<8>CpW^f0edC$)g$TmW-9z671=9p_TC*LvN*-uRr7Y*f2oL2!^R1Z_i@PIVDCvVJ&E7DcF2^R z7Sulliil_xLB3|m%`OIAZ9kv zLa$|3_r4S}$F8jxQ;wFnR{1Td$c~jBL#1LSk`Qb0o$`aS>id5RB<4#nGo#8@R}3S6 znb+199*5vCpC7AChu7`9Z;Z^AwHO8G{#H2(-`Vfkd^%rPp>|>7;(fquGyUM>Y09ZN zvaBg!IP<_!M=;$4C!?b*_>{@sY-%)-Dx@zPnmVR-3pdtC(dTMttX|efe?p*%2z1Pa zvmbr<>l=SPF~l30M{q-&UpJe57gioHy(NQCQm2&x{za8&djpcKY*DQ_F;!7{1Zt67 zXv9>J#*^)F5Y>-XcWtnD16O|pHLQy6ksr)Ln9G~-!Iwo6cF4X@;GPko3}OE5f${l; z8|~9ex>t#QSH(F~MGM?f$e#E^;7_cClOUbQirO;4J?3Q5Bpn>ma0f(lLc^@!?D1mPPfzLlyPl>J(B@vKYL*kp*TMTdBDVrVS4Rb!F3wehiI4P)STju zjcrm(*q;=^LU}+m1}UB{gsXWfpwd|ZBJ+_iL!OdH7LnF)HWy>I!#0Ex`OAHhj~s)A zmJ`Vi;!u3dR?sfMAnMZVt+z;}x|i(|xgPl-ZG-&DJl+C2nQ@b8TkwP(wq~MaJgq34 zu!TN|Y1Bl#9ql(F8fK&m9*f#Gh{CPsA{>QJz<_l-(;(Pn*Vf1$>w z11&Mw(9QBe(52<4#2=YS0-idN&1^YxEFr~(V1`*vr+XW+rv7n8zVSSXwDB8aKq7!P}xGJnhCNemh-ApY}!*Dk67RXx- zK415Neaag+6Nn9|%j%|`xJ<8+yG;GLq85>CUP64dF>-xD2XVi)L#bsY<)WKylYsB# z6A4`!PX8x2e3xN+iF%q%VtD*x0a%zv@AFHCZ*_|?%4obnYY(DY_Z-lCSwbXv{Xdj7 zYU)A_nA-o;IWS!+bS9XlBbb`0dWI0AtVbzT;WztFsO^6jEoT%$K1@0O+r2=AM;_Bn zu?@{efMX~~y>Uc&=(+}H6ic%F)6|L&50k%?5oBKigxm6Ej|fXLws_u)tX3q9U_&_B z^O0hqbo|nV_T8usNEb3kdP$SIP&`R5i}e0Z0n;^v(B9o@#(oUC^@mnc;i)sUMW zi1y&wakva(J9PFj`CahE8D-(qQevOLrw?mV$%o*`;%E4QCF$oHw!D-x#h>{q?0#L{ z;Pxn7<`B+k5tH8)*^<1&)E}n5P%{&zb7YFTZ>RZhQTc@(hS&S`v3ylJwMSC%(IUb+JAN0~s|f+SWQAwmWh^lM)3*Ae zy)uDM8*I<2&cw(4fsGPl723&n>HOmCbD$J5Wq0cFq!*8Ge7!>r_w|#YJ~XuxLTl=` zipw#F5Vd~piKMU>IE)ozWY^ZI)LzMkC95XM<77`A`KfpsD52@}Y6H6ySvPdN=rB=N zLjk5`?=Yg~BV};^u#PW~sFRqeU-~fuO-t`0$lzC5nppU1sU>7^L1;1$+6sa7x~51x zQ_EE$#q20&+uIgawE`Yb%&IrgPXgG`r-Wr*u6n?x=qstR-KYroNdIpDa(Se*9FwF> z>>-OV_-fmlp9`IO-msVqlZ2J!h7_Q!33Y3T@TSE5_GT4ttL{iz1+CQh z|I?us*0n?sNHAVkV^opUgM4bXjXC1Pnu$Qkqi7ff8hD>R7#?}ga+!n5GwD!@#-)u4 zI%e^12B|r(_qFICAtkmlH;RrrsX4&mShO+&t#H!-pgm>Xg2MKd>a~V$zZ!O)ewB)r zrblvp0zi4x2vmsXQ{wf2E?5( z^#<+89CjJvLSbSPvF8AK-f^&dF&a~*(KaJ+o0y1*OKm+d1Gr)Mf9IfB~ z0b45UXhc|3dZ`ts!8P^|Do~TDS81j9=}gsD^Uq3RXAWU;b+oUqoCACnc~$_y$mGCs vbN*x0>GA%5gyyCQG{A^cm&eRSq7JuLzUHY${73jTA8wX_IS1ySoKO7^d-cdF From 4692b4817410cde22617cea4cb7568820e24e39b Mon Sep 17 00:00:00 2001 From: Amonoman Date: Mon, 9 Feb 2026 20:29:29 +0100 Subject: [PATCH 003/145] Delete assets/images/3.jpg --- assets/images/3.jpg | Bin 297637 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 assets/images/3.jpg diff --git a/assets/images/3.jpg b/assets/images/3.jpg deleted file mode 100644 index 7783996e519d05aeb4774caf220c11d447f70b38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 297637 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 $MBn5tq>S&tO?)2`hCMu}` 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%0A%n*oPu+|5R0qLbX+*_-N%-W=!?dUMZLI+* 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ðUg$xroETDU3`1K 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

Uo4;esuefkI`OvMl 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 From dd9b6dbfe3d105b5200699e5a37d31c50b08ef50 Mon Sep 17 00:00:00 2001 From: Amonoman Date: Mon, 9 Feb 2026 20:29:39 +0100 Subject: [PATCH 004/145] Delete assets/images/4.jpg --- assets/images/4.jpg | Bin 83336 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 assets/images/4.jpg diff --git a/assets/images/4.jpg b/assets/images/4.jpg deleted file mode 100644 index 9b06dfe298c23ad650cd0c0d8f8b7f91451d83d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83336 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 Date: Mon, 9 Feb 2026 20:35:15 +0100 Subject: [PATCH 005/145] Readd Screenshots --- assets/images/1.jpg | Bin 0 -> 552112 bytes assets/images/2.jpg | Bin 0 -> 830449 bytes assets/images/3.jpg | Bin 0 -> 106344 bytes assets/images/4.jpg | Bin 0 -> 124573 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/images/1.jpg create mode 100644 assets/images/2.jpg create mode 100644 assets/images/3.jpg create mode 100644 assets/images/4.jpg diff --git a/assets/images/1.jpg b/assets/images/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..286f377def0af086de6ec31426a031dc3a66554d GIT binary patch literal 552112 zcmeFZbzED`w=bL)DQ+!p#fw|9Vl7&{#hv0sLXZNXSc^Lpr&tB4;2I!6(EveOC>kI@ zkhW+-f$${2O@HT}_ndRzd){+C_x^F^eI}pmS$k&9?3J0x+H0@*u6?y|wGOb>277q` z07gat5dZ){1|Yr$01y+XKbhjSYyV7>KS}iZXBwb(jp6^H39Ns(vk5K#uh0R5f?n(AuYwzjdoePivP zTKuVVb`A3Xlm9!3(C_KoKV5Z3{NLgE|0*VTa}RPQFdPxS{DFjv6NJ4*pl^Bp6Mg3o z?eb5w${#xPng25a&!azdpt-3gfp#O%cRc?U?eec^SO362{AmOp6`*hMpKJX|f5wT* z-Os{|kdqO<9DpEzDL@yX@#psw&It+h4ggR*0RV`y{-w;N2mol02LM=>{-uon4FEtJ z2LQB9{Y%-ujETQHMEde>(Y}>w(bv>I;CLjQk0)BGENo zz;*g-MD*9L1_9iEhMSb=&r1B~L_&IljGTgqnDY7`hB!LFH6r3`*NI4oNJ+@)$N-WA znixPrN`HfahwP>_`7PcDj~Sf* zk_V4(@;V2Q5`=r(`0ePHY2OV#S&elxV_lR*U!v}<8;hsdA7iCubcvXFf%31g$9z6Z9B;*6 zdPw^wEj~Az-br#016PIdE;7b-2}X{=Qytzcy_3SjTau*NCFjU~*xJ1%4twk67e~LH zTWJXKV=V}ju*!T=NFyCRA#2~ZPce|X0dB1dKI0t$Suy8uQGc57Y-<#Z9FAzu&tFJo zOmqy;)zdrt@Tl^WgnR zp`cx(HoNp%5qsA8D!~CKGiEY0|J&i4shON*@N#ywNztt;O035j%pCMiF`G`(GRV<=M>>u#Lgs^7vM5hvQlb!n)coWcFnzlruF#ezNMHy zlkNRoC$+NK%-5!$5-I(A?_iU+?3v@hR#aUWT49XErMz7H29uV|$FtO$TnOgnbF;?v z`!7k^8T_omyT{Cxjq+z`f|vH?Iooc1G_tTT{mTN2JwEjXLr#Gn5D)r|_RKXGStX`E zS>C>S@THL(93AnRu@V~qLf4)Z^H(FZ=Q)e~Uvi!Jy#B8DN(=XFcqs}I*glcx{QY*U z7Tt4B3c3gH$W&?K|M$>uW>c)_g9l^h2cj_RCKd;rc$chq%tr&W06vmRkbbH3@Ymg> zL{Qv#(e(Ej<4!)^XDu^z&)PnJMN9AfHLjwJMnvRJ2KY_-W>wj}+1zV%dEO^7*!6k8 zKUO?zfAt`>gk-ra^V)ppn-Sp8fLnzbciZQ=o@N<6RWQ?v@p;e*YZcayC)ZD2^ldN6 z-uG*8Z7VMQ3kNRZT6x;E_(Z?cp{||WPoS^?IJY3KF7s?u;-k23Dr~5 zg$L^Da|S941k&?VE&)ukv!?d}LZdzx`YSjxd}_oc>Zt)feV=%+Q55}Sx6QeS{b2Fr zqq3gFv(`@z-=+lR2&4_U zbG^$%MS9(Zwy>F>dff%jqxiN$T3%Or#11<(&@Kh7 zydjQ0K<7yQBKA1>^v8kH_p0{~NBuHnV(2Q{gtjTT+Z*znmuhxua8 z|7;_(#^}Uu<^S5|tKE>(=G$;(i0q5wNwdL@;^B1I?V&4x+~ri_t3aU}2FMGu~ur&Dnaei0npZ*2=0#uJR3r@L5AYHny{ii%bDC_Jor|+fmcHv!UKt8ndJL z+`cvswf$Gl=hw^d-!|FBLYJCW^Ei0|#b;dA?4OTDr%Y$>r5YBLYo_J!BJORmRwKQ? zeu}eAbstFp-0K z>!Lm9{dtSd@^s|bA(|Y7=E<>!QlYN^lzK;~I3!22bzq&%;4xLK@HS~}J*nrGs0*@S z{R&XD(7m<2syJH;(dE_t1|ek&Jy*oqDr*^rDHFPQNClADm=90wmlku)7@j9IpoKskUBPtL|oj#teRy z`UZpHvgjHbEm#h?XRC>Oq|p2acH(@~S9+3XGVH*@XDYvp18R~as?4!5-|<)5r?%!+ zS{g@wEaQjz%^jSov=evFS7lL^dS?*tS}zS`aeMM%;{>pv<~(w4F|2I~dwdPrurbxW z)I?Qg4Rzd~)O|&9oaWMw<6lEq-L`1AYA;6YM4UQqVwn>%sUS=wud#8XQrNIngJ+v8 z?O_YdOi!9U$6H+K=+jZ5zJU(MA|tb&U3#h1p}X$AY|Z^`Q$?glE2p=>*lB#qF7Tsq zxX!^Aup``^>lW~XZ)^3trHapk_SKe%os(jSLDICN7uyeu*7%NI>Dnium~4<$F{~|d z*hP|o`fK@7?rJfXl0gJtN%gZ)%Cj) z%D5}QgT0>455!Hk?cN5p|LmPX%>vW+c<}FhQA+|73X>t`kkG(tv zulGjG>ezs>BEi#*3 zoarWQ*$lsy(kf$Y9$Yur5Ln9Z(tPvtMD2Nyfn-vFx9vV{*RLpw9vyrsx7*l75pcyK zGv1R1>t`Y%I&AWjSp?^xBm&uVhrOb|d7KvT;|tx+U%oC0UwosBHBn(!6Pvn@$WgbQ zdVnK4^dUBBi_VyUN zeNL0%9{U;w4BVl}1Xd85a@ma<+DREa`gpWl&QNloqL%dtY#2EFOtAqbf$NL$e;k2F z;H2PJ0OMk(MWiR|72xMFGg3YAbLc1LRG-&hC_{gM;MIJ+9gmQ>EHU} zzpp>sT}w1ej+Z~pmy4??fy?PEb(DtcgHC`JKu>Qj4yfLd==jG3?p&Podzrmt8BSml zdI1Ph!ySsr&xo}wm}PYCrs%T+$xY{4i7OcCM5m82(cP~WLYrv^YOesphiUw-3!zEN z$JdIo-K?nQv_#$82e`=v*+7Afi1@M1D%4`r?l{@_YRA28RGP%(j8sFFfmf@OyHo2D zJ`d!XA-T~zENHjknJ3nG2=!cBl@QK$Z4=Ga8fo2B*~%a5tk2}v2mbz~GE#U;#T!>7 zaW7=kw%RC=07ZiAS&w(GsN)@t&dQ6GvR)utvKIF7?>w z@w7a{hzY;NI)ijun;r+C46umt(~ytR_S!^~d)(7ZJ>5~Rt1b6W_ronWai&l!>E{D{>OMy+ebjD+>WR3B9|-~*=fJM1_Ktjv z`$=`Sz(7YJ$7hmkEoIqNs|E0EIam8=TI#fu?mTzIb?i@iy1DXw$7d3_RWDWQc6$h}_ z1e6o}dK*c_k?=dL$ddv?a!g(tk~H>SW8_JHn2a>O3dst37$V*!3Ujw@GKjER4b*yS zrNfe4ui6f%NWZ-e?L&^=J+*|knZK&wm@ia5PbeFk&mUX(E1i4riY)1!l@a!PUXn_psOU-d}>kHf2`5g9Le}!bLP_7=T`mrJQGiN52bZq zX`6crTJZ{St6jBbVwt+TKFpVCh__s>am{>nJT$IKD@Xs*`twTVpy3&(3Yi^xI6P`PYW_g73s^VsKDjhq>5}R&et*&JS98iT)gM5Ylu@<(p2e-8|9M7U6UaWIW^5C zoze!IDWls){?^SK>$*xiLCl53&x`tq`h->no=R{*iz z!II*k(FnVB_SikhU#m7P+Ea%e2S#8Qr7M7$_E?m1*2besX!-7Px?f!U^U8|n*o(bZ}oN zE}XtQhr`4#W!#Ar&nMt}?%NtZ0Tf%Un-V75E074a>Msj$CK-yM$k)i%bu~Q`3@5$F zj(~dOLeQ?Bx*53E;>;TRmy$_gF8IvQ)Zyg`HZIlE!OJX%cGTiM z_lGc$$YWEsr0e?!G>3=PGZw(=gI1IQ84hqW z*$8JyKBbVzXS%lDu$)xSQ%gHSwRmp(}9lK0Phj%qlN>@X0-&m0YsN1dPFO>{w>8Axcd8c7^`Yp)bGShkNqb@Nn< z)I7VI-bo3Km3=K|S3wTSOOR}p-?Z;2L(?eoRn5n2+PL=vUr`Te0{cA7M6B&kpS*40 zm6L;go%>$PNT=<<18CBT@zF}HKQ}&4U9zO<0383X|rdYV9W{XW5iP`j{xha+iYnt=qGdn2eMfKn|WsvyJFpcR{DY{wfpNzxsv^wExGQa8w4-gSsN zPgSac!*1g33$(#d{R924%0m*sTpTd64m?kd9$MJ}YryhUhgsxxy*&6D_e%I;9!Bkt zK|9A$+$$d4`JYJja@Wg&WL1F@U?%3t& z3!VSGTBh(>*rKu(#1s@8EZ%2UO||6m<~;eE`7RR zavBN~FrFYcx#-`v$kJjvNGd_UMQYzZ7{P$M<8POkl6gywqr2#mrD&q57}$QRPYUi! z#heVOri`ap6cvCDwW&$h5K*Y1CO zD_I=GpRaii+d7Vf8BYOQ()hBr7vC+)@;fP_avO#*tBRYc!2A`4*?U{pXF(m4vH7ZO zh~TsFs2$K~gD#z^KZVE~;X4pGr@z{vb zI|_T8jIg?hpRp{omg>{n&<@0LyIK8AlF!Y`Ds`n77f6kJ zK`&qX)F%!Edu)x%vVqjAUhl3St$<(~mOr1LYGQ8e{&PA?Y2bqJ68S zJvs6=lm|^>ds&NyLAeuX54odCre3nIp3%1@XK&r_V04a0@ShsxJo(VVW>h*>+KRNYB)E2;;ANtJ%K*23J z5v>m(!j)v%b@ERg*#mxtKJ-<2w3$R--*GwKybz*J)dU05|)##JYIj^!G9=s-APt6MYDUOx7LG_WiGuN zb_#W56OR*khQWLIcJ<+7F{axv^xn*|@$eutfzXjtLzNhlFhflnJOw=p&dFTOV*N(W7`0*;$85W1`?M>^_meu^^5R1M@ zTHARp=`@UGKa+y;k>0s;7!xCG&X5^ql?Xo+*>VznIPCzeVs;f&BxM2HoaEmW4c2_ME$EUWr2nj{{>0AqvaHV zUVhN6!J4zr6PG&L_lU`E%8)if@~uGBRj0tLD*&7v9i5JGN|vN8qmx3QtYp{WH%FT% z&ApK#DoOrflm$A<=Ur*b2%vs&@u_sIE4}2Hmou3+a+E&%`KlyOYCJmZ+%>6tE!5Yu zk2}FWm3~rE%55NZECLq_^Jq_i;G!Nu79sQWuOiyrm^~O;dqGjS!e-Az>3j|d>I|Fm zoT4RVljitFfUpDk&}HFKJDGoHgqDp+A0A;Zf!t`-y^8wFw0P@+b=H8a}1lxBcw2 zW(mg@2`Ne3OSzCjH7aVrrtt3LrfB&73`0@rLzMD09to!$aW~)ias?1NF6WW;F3${b|`hXj1u&C|TJwq;PS- z_&KfWh+>dZ$5b@@sf1RZPWz(0A*Fc!@<0Ia^s_n3Ttvvwbrs5IicG2DV$w~ZzvQB? z08HgqfF6Rz>95rUapzZuM$HCz^zE&;+ev2hXRTDBz-TYQbMIJxGktmid*826dDQel z)<LC>@^@kao;V#Jwv`yGAV)I{a6A7U`F}UlEt(J@yBO(ml1C*BYqr%3lGh4-1q_ zqxsm28c<2txuY{)x2Hw8FiEy!flow}4auQ<8rpUH@lKWZJ;%=q7KSA0rMK%oi<9z6 zO1BV0D#rru4C-!?<78DlogJsuqIcMZ$O5ZHk9LyMI@NjBIMQL(jz(om))r$h&(vrA zO}YVHzzJ~AgA9j_@ix2K(3dPLHPZX@1{#|U9pW1OnZ)2ZH065v5I=}Tg|D#v#jz-B zhIYY7j%RyQn(eQ&9+XIwS+b*a=&i0`I9bm--=v$;i#)!qH&;qy@CLb*k^K@sN?6Z5 zA|CV6#=w%DZrCS#O}&Ewi}T>IO?V%^K~P}U*JD4rn#Ii;OLS@j4nfwU8UZh0>`hv& z{P3HirE#1(Y|QqVsxGoX-1t*%5XTo-_Vmo#El2f8IV;I?p`L3hdRVcOV}2;MbfRjGcRUssQ-=SB%~r zi(3z8!V$IOicZSmb}D{>lHB$#j?%he8Z+?^IuUb;PwcDEy4=oCv&eE%O?M-Mc*)!S zb(w=D{rQ7GcKuvw6yZN?o0axi@ihk?XvX11bxN$%hCx9>M6^`fXyazdQ0~^_Kna)U zLjf6kK6$+qFJ0qra-))`Q4omVW~~EecMm+J#K)bJ?tn;r=4#TfpKO{e!k?|jpKUzK zf5u<7S+@CIbz*EzeuQ-IXI-z^sOIu=t*h?~zsc9TCn^P7!uo@a|Iu#Ae_})Be~87; zv zn7&32vR9SN{T48c;V9ciF{OnV9U#Z9qr4zUK}0R3;_uz(+f}H0i%bq1w^iHQy3X0K zpJ6tLV9U^5ou0c@(PKwP^?mr88|?iBLTH+6>FAaDV(?kclbt!J#y8SF=gwut$+P%( z@F=EKSLrp~F1?qM0QJ9K5>IxpUWkLBX*B z*L9gxuAnic0-;681#@V&LmStlJe_flCjI7xO_QF14x4iO&q30jORGgimG3;g0dUR7 zRx2|*Xd7Ew!C<;;x0EOkkR!F0SAaXaf3=h;M5E8BuK@dsM<>s_?RrnNV=p&12pf`| zjHr|S6twbikBn+%kC=*%|K6yTn)Fn&Zxk0ZxPZa%hUBR|ZKnT$q7R=p)wZi~@(tcl z`jB&Ay4qc@J;ctCpYKyl_U@hEjxYBwPM!Js%t7=t;9``P6YEA>b;lQ7;WnD*tj*RK zTPJRzR5-G97s3(8&JcKOaZ<4A3b2RmzVtizLEq6)@!i|kX3KAW4`+)P(G(@AnP~MSW2q@jwSy~X zqZ5A>S~$NX(*Lc)-kCCc&Ug5ofF0WxelD*24T#Lzd=3L~$WIN%f@totJBqpjN9(WF z6g_XFl`_X@|ijmOI+ zSGb3CM?G-yVs)r@%CaV@6t(o*voH^Br10$J?q+|WfGPP;7u#jMuZC8WcBR9Q;Tw=b1-7Gd%cT3t!RGs6i zZ~9{Tov-Pgo*piiFv&}>y{;YRqngzGi2Fxp0xp4TD}K=6u~46r)A_)c($}!R3S8h$ zy1MvWss^9MeM+-+Oo`MxuNdEEV;i!wrNy@wxrV=z=;jf1=#!xfehs3tjD zQX1B>Ug+nPZYN!Z-jNzz+&a(_WqiG!Y+qdbyv?qDo;^HN5QEFf57v(o7;3A5V=C~W zU~_c!Mtz)k6pKtrXs*rLUtS30yywRAg*W`;OY*fcnF7__E9m}W|aZR?f1kk)yBUKTUspNT} zN2{}DPoa`9%01tNGrqeKX}WPMXPK>+ZAKL1s@hzlnRBFebMF_LyL72R!g(Xlun`+I z8YEg_U=*9Lcn~?J-9H68st+p3BvrrtWZF~Jxe?X0l5we*B*f*)X*9Xx+1;|b-3o=> zxJ&<)XiW!oI02IGn;?D$z|uo-$JXMfXV?P>So7%3JnP!Apk0{HkctWtFkDx&RF*xI zL)41!Jv}go!k~?ztsNPgX|~7V0$t3Irip2{urn*6fw+M=WXt~VW=trQLhtBgy%Kq| zXxu4e6LiZm++vrz{ocxw*m7v%dw7lzt>Jf(qmvZW`R}iZ1_1+{-Bl)Ipx1|B+hbv$iT zl0f*I{m|GT>9^N@Sm?Gg(YXR@v0GaLN;xm>TS5YA=iu(0vUIhlyAgu)Y#c07D4q}B zcm~sE0#G^`2w$mzr(P92Edsn_V=GX5?B|Usl4a`G;Fzs*1%4Muj`aJXRvVsAk+?gf z5_PyL)szL)KK;N;!kt_0=MAo@!pp?D$(HV0lXae zS-*Nh>-rS3l5dYnM2q0IR$xWnB?9Fgo+p?bhfHz8m7u-QQ( z|HP{!gFcD-_uZb!-u?NcE5NzIQ12PX<68q0QFkMY8Sos-cu}y)_9VySc<+AcRhf+% zPF$?0xhPFo`lbTYlPWi+*?Z65*d3f9x`$En+Dm8t3Y3+@?)``NG%?abR){Ib{(7P^ zMsTt4&ZL^kQ23por7hRKylqKZnFzDlm#L=N@Cd)9<>!=( z`{raO9=DL^5`}`Fh~7Noa{6d_ys^Grc=U0@0i>9pRY$aSs*(SUvWcLMZ)XfVDc>!P zoLjno+2NzF8)W-+1q$5oIt|{qq!ue|)46on8V(q5^2ru|6;Nnd5k!s1t^EkI;@Xnm zeFk~xw9w#DweH)jn?yY@5)x`a;i%R9nM+Bk%Ab8I!B1?2$F0NI{P#nAUU(ziCAgXe zwyy6Tv;cwVm^E&?DHH{~*X`6!JC2$0_It*j&%+Sw$v?Yp6u5$+Jg^$AH(+(ebWKh5K5qu7w53ex5Kf z`H{3Y^5Z^{@1|Z4v=3y=cHyNf`H~rTy6z zz~tv;7_z0B0FY%yWnKY9CBg}hJSVQXm1WhdrhnRDWYnM5w=hdJ8^UA`IiqA`N}i(* zt^iN11J!Rr7sH#4&-8pvk`ME{%0MDi^}(e~cSkP6DKYLa7tKgXnWY)+`Z6A+W5IUy zr=v;(1sHb23gHgr z7rMx;|5cXse*ky-AF|H>?(^^2{11V2f3K|n4BYVVQTlt7{!_TrzZc5i3+3;H^0(#j zx8?D-;qWqR35yE9vGxLE=a_y-{=XzoeLb^)Iyx`q98+G z92-$L_XQb!JMS%L&A9lw%iX>{)91$H@X(s^eNL}X?~=`+J#fUnu{hugz^*v}9=HKl zq7^To%x@Hp8-WZnI+tfVRSdpl)2J}c9{RK>KF8CLrz-IMZT?H!X;JU!(X`7d&fbg5 za}XiebXZih=d0669Cqaj5W}5pawz-Sva8ZR?CGaY1RQpN&%giW{=Hu}Ox|HMD~0VS z*>+M{7qj1MKH={c(UdNtP!*Qw*|7B$);%$`eA|rS+*MWo;iLM=>8x!7dG0ZwUNJ|G zKEy@4UIAcCQ}~mV(9UT7XwUZg9sub?!up$O)OE7R8OX^54VbOc9y6iMi_r&f1heVeuf!b33Oq?jzUB zbCp&?NY>F`-+tAyrxtRADzv}k`~7HLE3-R0x6(o5|7K+%Nq`dmhx2-;S5Wq^!Vm)d z7q``WDa2CKiQRD^I!5b0gEXq?DDX)W@YfX}3wda7luz(xD|41D z4Hl;_p+g{}>DL#1U$SW}9=k~sVyt_1fuD`807L5A-0vn=60^=Fc{?o#806CVePV)N zsXi?lUwGbp8E1w+=zS`L(~cmlg-zH*`ETB~y41I&z0iR0b(vz_auDw)gWAuk-uCG7n?B-g($pwADu#{a9^ zW7jc|#`gAKh;SyIpwp<&kEX_!2>o6GV!6*ox9ZR96X!B5ppsj_=;i3XOxz}B9zQ>@ z`%v-Gw!Mc|J7Qy`R4pQ(r!StqfFE8YWA?%2u65(0{%Vv~VtzuQdxbd6!S+$yM}5Fn zDi(N3==f9boZ3{DlO-+!*YYEz%bb+}D;PHWhuhq<%D7a>QHIKgVNbo1 z_T<|g0=$4N!uo)1pO?`>$4f7#VWb)xh9N(4l}Mu%w?d%TH&F)qOkA(xz4u2f zxBueM&;DG-M+Q|mSl`II)^-JG3}2XqsgWqcShoG#hHB0|(^Qt=Oo!pnRcBa7bN{2d zZ-}hbYccOM{Tls7o(u&@%NlmF;PlsnEQaAe5GwbI)O=7FbB^L}Qh;bx4V9#3aT*M>ri3n^>6(hav^~+M7_jGmjVT))#4VTlzTy^d1cVY^?%B80~J7h{}~r zb^U_kG0~$Tr}+!Os^CwD>+j`r*^bqQhm}l(k^@D53XRZgM}U%T@GCFM-Um|!--)K4 z`ChS(8=`$_!pub_NwuV6;BN^U7N5x76E)UVT-EQ;?d$)8iEze;={?YY;{qlZ#c-4^5KLphocNKfAX5{L+ztI1>-+kY%6Z_vV= z7_!-4h)7SgvDQQjv~Iy;#j89tqExC_!{`LTs_RF$FK}|dir7Nbd7+?RK3|Ty&iFkQ zJvleV8-;|X0>?usVSHoR%Zl~Lrs6?m&bLLfTs(zo%KoLCw0h4H76{0xeuh|^?9(Nj zu+wQk5U`pL^*E>#`(!&z!6w4<-rDiZ0gK>+8&wvEsr6?<&r1ut+`Opm14LV9iwefW zj9Ud3+kHq5d>}JNz{{>fit?jZYska$)VVJ_U+5dnYwxS-4!PT1P?xtlU7pwjuK?mK zgy4WkQPwg+JRLUuC}(o+3P4@b5eF))J538b_8e>o+w*Lym&U*?IP-Warwyb{z4n3MmIKI?F)vL5@PZ1*oQ`^JA)lgIYD30#0wF zuLP#n6tyLvHM6ZGPj=UfMIm$C3<|#|=Z6D7O(Txh*^8}=LMDS7XkK3nG0MD&8g&!n zNGAM)+}~J{UmB~o5*6-+>?Q2LSeVJ}>S_ed<-?lQ2v|p|J0AqBIPBMASeN{%y^tPRUe1|asek`-W!ZixLhSXUXu+siU>M?k-A!^WbT z0xpTUMP_EX_k1+Cv$OQv-`w<#0<=G}N^Po4cfuM46&f~4jEI#fmX^o4=L_9&rLH^i z5BT0RPDwl(^d3=vTyd}EotX07(P@D@fe@Q|`JZ#C-BrGyfzz|kl6gtS5CL%*l;KPNSn;+>&0+>kKWP!MgyJaWiP~yA zjp>TCPhj&?0h~78$<=c2FjMgXIF!?o&Bdr(bqlvFq1fd3c9g=5nVqrj7w5R-Tw?Kq z;NsZ=7Kkmk@0&)Y=@tRI%|K)mQQwXn7S$gS=n2-Mtg=dTZDJjP>>~_sNU7;=lHC;l z)!!kh&|bq-&s<_Ums)aTfBKy|tw2_ZW$;RWiNiwZ7y$sN_D%h|J#9f3tfX}V79(Z2 zdUN$>z+l~S>=Rtz5D$f!hx`NIcoFk7;CJEH>$B*t!0D`8rgRlKu5w>peM=7}z+NAZ zzuoufEL8$u?4Q+T+Ej1$;fxx6Y!8^!k5w>QEe!=`uKnkZDAFIO#5ozRO+(S^O*dwK zR4ReBK&LXdl;>zW<_D1SSof&|4!bF+Ye9f}}8If(F z&JHyCY@?$O4;<}(S9;7Z6z$bSFR4xz7$Esl^XdnT(WoG>%I;_r8m&r z?w6jF-&j`W{NgXBvoul7HrQxqZ>O3ub?wD-n`BaUU<=c)F=U>=mku-c&flY%>cVGb z4z-y1mYgqEBp%a>$b6`+e+YBggRBpszLx7HOO=iR=O2O%>zGWK=Jrlpj@fPX0tGdI z+3dcRTW$H41vDK+n`gRB$?8#)KDVE+m(MJ2K+mDtxEmK$W$eBR9Bmh0G6EfWygamu z%QX8HaG?U9sH|%xw33^o?Yz><6|N=iEAbkA*-%5#=g)NnvRrTYYu8u?l?Gi}3ahNG zwhg8L4wnG6MS+8NNO+z;61Kf{J^RfAK#b@jAL&$sqsdUM(iFP~xc;_t=qXh(mqWZ8 zQ%N-&RM#NI3uGsmuNPFCQ>^*H&RFDb0g>NZpk(D!Q=K|wF5ljZekFyJ<7mz(PJb+T zM>e2^+{RN0#WLFgX;8Ae7PC}7a$F$ZP^?M412N}UiiWNf1FL21Z8^6>wa1}$KppKO zxz}K#yo?^cldb7iIvb6?o~Wtbo=&dm+?rqi=kAzxGhYGztkJGTWXre4sn4oGeQ3nChQ9C>#Fyv`8E&}g)1=!M^-~zLSa5 zyaFJ0P=q%_h^YA!c1PuM8q^EJQ>vfdRjx#QGDH15%*=3otCiWH6o!TUNpMqHMDzJ7Uibc zT9t1uLV|8!^Bvm&Vm&_*)b&h-ah(u#QJ+iHB~>(1D8F~RhuhNNTu9nga_BJG>ofV; z$R&GGN`De|&?}-mzDtb%uegFI{i575x!hSb6LKV2<&6zS+WLLakM@arh2Ak%^k^}* z5Dgzu;iTk&H1|6)gD}_`)UT=rwmWOBSCF(1kqvil58KM~4aTwiOfw$%^f-<BD}jm%phm3-1Ll=GCB z$WzUU4rTsc7rK3BQF#l0ebe}h_gU* z6tGTs0GJH754y)ok?bW~#}d4)+V@cRoxL3s{7JB8=XAo$iw8CyfQGUsDW_Sq$e099 zpZYVK^wT*ks15u2)CXKR%+=m3%ZEx63B9-0#6+G08dHqnhc-QE|3QB^KdvKhmdqof zMzwYe<=KbNAmi{PZQf;+eWs&7n;-?Bw$Ed|AtDe2qFC$185NqnY)L zmGC0(jZsCC-NGh2xz1+`dmc*md!PJ7U6jPM)OhT9*RvXnrUTl(f05qaQs`iyJP=&| zVAnJk-YKUVlK$Kz2KZCW#MnJ7X<&nf|Ba zM=qF}!?@DaV$nM7t78PT>(WL!LkPv}v(h*F5VF#%t5o(FM(3v>Y|QW*k=x}bM2(C4 zNfBuHGd0S-+9B(>PIPRkTUGUC^!JP-0$3qA%oBHssdX=(<=(!ac(0)5^udA~AvR(DH%(S6jLucRcvya`96Rvx3UK-k@(w!Tb>ma?c+>-K2o+6##MfF%&AP#+;$h^7B~{^ygmsmk%si(t6K_Y3U4?5>*GKA;hl&%mc6we@vcdPh z>0%7k=S4TVE>HnZ=ND}4bv0(ubl8`e`foPHVT`Dc6CsL_DH+VF0(na-CD-QiDU7}X zkgLEnljrb67bwgb^XU6C{%Z>WK)vCVi%CM4XMzbK)?z&JbNC~S-=l%`ZX$l`1tr01YD{dd`$L5_o@irZ0ayYGz08{g2gW&6q5*~ z0~T0I`vD)6-(wG2aPD}TEN}m;lQkQ|On{h;)34cGRIc8P#ssZj0si8&$hhBUPJkhP zvzd@kzPP|AzMekAII7s}EC{7hp2dw8bfpiZC{5jf%P5CP^V($~8dr_Jh$oM-j@Htb z*)F}dIM6F`mTy`G4xh&C9!Matd44Ac@1?zEygRz!=>`1qh z*`rMW@6e6zPuM#XP}B<7uK<~@A-gs5JBTg4SZVsLwxUOy>WoKiTmcjik@w47dtWSL z@UY*N;=~l{(H`?zp{Zr8T%1r-Z1P%uezh}N=x(6SHJ_|^O&&JVB~81&jmC(sP_w=9 z2ZbvA_A{uOJl|QUrAcH9y>0ToKKj#)Fqy`Af_WwJi!6uY%!R5E%S2%9*|Kfsr`&h% zmtPMv`rTc)@!`fQ)2&Kj)BiKAm(*|37+3X=i3BHUbdTK?U?U)9?68IT!dXordM$Co z3^%@=mm>M!7&8BFUuy9cASHTSUAwf4mIveb@L8p3z>~*m;g`sTD*$?^`3j)w(p@Mz z9L;qFXg^iQ2l!qArmX8dDCW!0)z6|&YhE4y_nlb+y@DpMYnL7N@1(~$Y!0R0EC;)0 zfyY)mKvl|`{4FC5nD{sHs0{g~N_grEhg!PA$EKBfo$ah~5@t30Th*Ah-UIbD*aarc z=hA2Wq;mdNV3d1*N$HEgGtL$J#vOalLf7+Qp(-wGKavz1^GE4RNtJ95MAWR72wx|Y(TlonB?3J*UvM!_AjJT97~gR*k9rOPT8|- z(HTMT5a-c|qVZ*vd$UQVe>`mdC(EQzih~842A7sf$FlcyTFeQ^`o1Pw+^Po1@Y0n3 z;{x2?-M8MO*;X2%{h7PLcK_6XIt4*G;nuX|3~RToWlD9kd}{{`fk)^prII4Avr`Sm z)5sd>rn6Qpy0^&3RXEycCRnbv=phylk@>HkL|QA@)uui*WqS3>+G2?oaT|xZ64}JT zwpdAJ*q*n9K)r(0R4wfj7DN9^Vg6*+5EFN=FN+fA`u;6>Vt|M*5$w@*xc+K9KUe{` zvvJbIy(JoM5)kFlYlFXW06e<($o-}K|W#vL8WT>oF#d&{7< zzqVbJ3Q$~%Yw=>CKyfXUB7s72f_rgyE$$ku*dK}(w*;rf0>N4wf+hi4Tw0_7y7Qb5 zXXf4KJ?HG1{ps2B%rKKL4E&O;weEXe_a$=gO>c|`4u($=e{t|i?^mVPJt{#o_l81$ z&Sb^0I$>j3A_op(R~z=j0%U!2nKb;0}pU6l{s z8{-$X-TQ-!LLs&9yh1VecV^fpm-pz{cRSqJ4N*;CeNaMq1Dbgb#KP)Pr;KhtU#S~q z=36hST^ss5uH`c_TDi%U3yaF(uXK6s+X+GFCOe&vpYhyx_J{q&32)TH{&R`d6`^y_ zzR&s9@x4($x1xtxv}qZU_gdo%nVyYqAz4^1H0^j^%t&m&kkAr6zgwN5ObMo)SKR5O zd%b@yuNfnv3*Kqv`7*)b^MayBQ69c@aHwznX|KA&Rq*I=C{)L+m%yS+dC-#gLV@m_ z=g|)27zkG-^k(sh+&oWvp6=Xha?$ye^l3o5M(^uUL>K4+y7KX0YOn(d4M4S9_*4dC z6(%JLM`V9-JW5mk;@sBU4BZzkHHbEdU$~20y!Xq~ffWDCly%*07~B9Q|L0%!FO}H) zx~}?Tauj_Xi-u}=XvsQYi& zhVSNj<}VIBy-4Bbl)gaSs6>Yk{%|KoZZgy~5Hsr8Xare5zIDp6xK+Nm>mDjX2uK*Y z<`v{mO}}+mGi?)}_{5j^V#I3@p2xfpIb1W{(~Tc@GM;}KdRn6DHqpgy$#b3Evsy{u zg>6*=+g1a2w8bh_yxm#jK>~9^1oq!|<**e!YyHsY3-D?EIY&@^Pllz_Kzu2lZ1q-j z#u+P(u5hE!HjC8Aod2lkm`=lD*P29Ni)m^bWtZ15H@zG8k5QEEVmxUxXoiD;Gl6?o3=_b+j9BB&<-Z(CH}#c)M? z8Y%_=dUC@P@+lpZlqWPzbf1;$qm{yyA3!Y<65F2AkJN(U8RX2Cx^18TeJLEv11rf# zn7=qO=|jX3q^!GIcOpUbm!?JGLE*)Jaa4;VirE+CCvMm8NF3$lQI{U~Rd?NC2h-RC zMD5=4G|v)AbY2zlc8y@_mhIl!a>{a!hy2`#I@||K+x9t{nVOkpp${Go-@wq`&iF?_ zLyUUAqSqM?i95&TcT)|)yO#$S7#iG}qBDPrO@mZ*`rGYT4r z2p7-|zG%*lD<^*c$icIk8=MMUYYCXq&a>;mGoy4%Nkm*dWPmW#Jlw{h|JbwV*I|0D z$6|pRP#fWR?sDpCGft@Nq!UDEA(d)QGk5-MqF+1eAf>Buov)l*0@Q|T8uEzB+G7dd z`BTa3th-oV`8N{7dBg3K2mqUQVWgTf}{`=m;yuu#@QeDpLV- zW^8qti%}0ET6KgQlkuHPFmD%m`F(lu2=>h_UmvlC2*pI~QKLAXR({5C>w`5ZJ%nJq z_FukQS0yO=G-!<8@qI{jYJPrG`Z-H|4criSOMU;2NB)}U6gJXN_hJJFNj$-T>?{xf zIsmiHFVwCkK6<;(aZXh;>1+#1pcQY<4{0 zuy58*^MF(Cr89@qNihx{yL~BAjX{~EUDDm&7Si-M=z8StSO;y0l_--h_n6r!dWmwQ zgtx{oKEBg55fUuV(~T0ltbESE@SKp4fX6SJ%pstCDKo5q{)+1ypUJDAdYqONICPZn zH%RIxjUi-)Ft`3nUga7R=M}c})@y>q>9cxiaW$5c8!BaE7yB=hlIK|IUdI^-D&zvVibP^$3NRb>mi2#g?{PbjRfN>~gXIVIG zSSB*V{P&kr4i{SC>-Q8|Wqg*yS{g4aO_XM6Be4~Q?5tr6K4gyyZJAI_Ok->jWgz)P zxkHo+2RNpH*yBLXa|zzAoKrnRP*~Dfq6gGoycfRoK7}#;{fCF5>UMkSgtzsj1lil- zdmMqbXHS#X2p{+Zs1;IuuGM@ha@e}A)~Zu2{T_fGjF=YaW@#j=b5#8B(OY z*EMMph1$}Zmj5%_R3ar;#R8BkqkriFO#r;Q@B+%h9Be|h;#DIUk!$LEk7_90I=>YN zBZ}9Fdk?L%>}eVA)sGtibUoY@ns&>A&lknIKQx)ojJ)!a!4B|^bpFp&1O1<;Z~D(W z`u7Ae{`1uTo!YP_r)ny2V8&s|ro*hsNQ$RQu6qv803I6aN}6ju{=A zrqQe9K%rg4t*@Df;Q;{ONg!k{yA?ATSm&UiXkvPadEccSx3!{C&?Wp}be^n;f#F@0 z;@XGZq&X*(2Z`G}35Q8e^-IOWUBP&oX38vPRrH{=Ua;A!|DSlOC`8$a459R_;Wsn? zB-K&|m&jXQMIyZwZGMY+U1^=w24S7z8WK)^rl@kHBA;&V*?Wkh5-_?O_RR0}dCR!Q z*wqi)L66I~W`A+gRwByvehF_&V0Zu#FxP?ayy5ukJHM}45?Y4-^LzjKT>i5T{tW+Q!{Dd3k^8zDt?*QgI>#1=~GN z`!rux(N4P+hJ}?rsHABaDso+`BTXkP4C$xG(!DQ&O&tYcYtf{NI;)^7sL^|UzZhKd zbK~5+;wNdjrlz$zRVLa|$F9vH=mP;FZa-Jjm<~~jUQw;IQIdP!1OyncTt)|3?kIrZ4pq|*@2v!a{ zsJBagBQ-K}B>i$LGV08nCQCe%w-Eyxu+PTYP@&vq2^R=q1m>R_9;hpU^%G~w)1|i# zTzixCM5vL!{l|FveEYPsy-V(+v+SB0!gx28-70Y6Ok61$?ZIqSUuVfPSLWgQESAGbsiHqdw(4Y6#Miz21z-e3}1CdWv-D&5}Fl=C|UR|WtBJ6dgl4->dhLP zg^#OR-$62DZh z_NrVLT>Bb3rN?4h|E4cB4WBW=E&Yk&E)9dr7w-7Fow^%ogtLVHNJfy(zJ65IV!Up< zll-J)1R9u*?oD5+JnB zy=oysQ;TQv+@9o{#p}~dx31M4`x4fs^EOFW$vl807i_%{?s_PhM+3Ey-~Xo(J>kx! zsTy;Lx6_SyJ=AH*x-Z63Q(5xcAGX)#+d7l&Rx&!|Z7Wn3#i zMCuxm&ytY!_4a`dkC0xO49An9ta&zw+dA=gL~yM-{)}01w#~AHaj!2L+F7~927@n5 z{F!QnSIo-}9e0i0RU_>JWcBzVa?X9LC4%ZHKUSGuT^Hn0ppUwAD#dkwsk_!{_QSK< z`R8#Ys$kV~H7fkG|1T>=g|cnsxBz!?)3)_hrcckheGA^YN5YE#>n?8^M*G|hv2_id zRj_UhCv|5<8J`Y3PRhFF+F>~2Wy={u1i-T4JJYmx#Xr&l{lA91v+diuk(6Z&FZ}S> zp18uS!kg^3xxzIbB(yoi$b?))bHM>3$Us&#Kfp&evQnATuZU^f!WyV9D3@{t7h9dRa zHOZ~^x6s>6KdixZk!fRtd-wi4DowId5XJ;b%I#=)UF!l$#LANND=F1EmqvIE|4lc_ zf49q3E)i)WNM|+L;0_ zYdyYpB6c94OiDAq+ztDA<|~aXUo3X&#O7A3rmJt=k;(dWnOI(P@YBVyAVsbR#ISr- zN@;=B!sSEju%ro*D~B`41kgGk8HZ_%)x zZV7OHn=q!|!(7G9mI61uV}5_7G{GnN4*d2 zo&G>=zcaDZx}6Dwo&7jN+S18&i}To|QcGKRURH3+l;u!%&>c3k5VDbW&LWl5Fu{7q z)Vkfekvfn5r6>mf`ySW-*D7qp5&@Q?rj5_4i_FjDrM$n}wILU;mt-fz#ukLdnfzt1 zSREE9)IKoicP$l8v~X7~xLUUd9epX~x}m^Y9Q}IrjEFmO_Zjn0 zWEg>lI`c0%zWY$T5Z*JPCta18tc%HV*u>3JHX%2zH(eVfAy8DE(jc(&OT6@6;6>9F zd#Bf@bsghOmsl1(@-Ycsjd};e2UaU+G1{XB2@GH5(Lwm{+P7LGSeh@wdZUd+a>h$W zZs^k*U`3oYW7(~(cd&W0Yo^<@d?b69X6?zjx#c^hL4Vx_HG{5`*ogP6#<}6jorh zGSJLtJ|heVND3*Kob`%?UG7QH!Wyr|rZ2pkmv?}E7`Ko1>%HZC?&+rN4V`bcY+rPJ z$YdMMKqadYyw5lT|3p5&yZf#=q~pthI0voIw*AZxc@z}4VAVM@cr){8qUnr7ft^Oq zXVinP&`8z9=`H(H+HBte)l@#+EV{JX7ULLZ5OHm$@j(zB+rYAiWXI+26tv-re`qvw zkD05{#Oq7&@PRqSfP@gk|V_6zPbPlZFBK z^QYk@V@)a(WBDjoe_BSMmZ zKoiO;!HGY)hQTR;ExI~0icZ2UEYcxwHi@p;n?LI2JpZqQlbq}nktf5Uf@giTTqKh- zLQ89dNN^?pg-I%xC-QM8!%yu$uf%zn_l-01bWD_T2bndz(oIY!m&K38wiqQ&-@oW= z9pYWosko&)rm54YlA2BB&{j)pWqv~Frw+=6QV~$gwEIaMFYT$-+Vg6({;Q`p_!G!| z8HSDCNd?ZYNFu&_HVVUbq4>|yJ!sV(#dI-s``;_V2vdM+2vZ5T%%b;t4ttO&g z#+S$yfS+rZEmU)G>>OzhYDbZ~iF7gCJ=CCF>kK$VaKqUJC9J9}X^nSM#8C?Ws#s=O}o8_Dzr3!i4e;lw5AEG4cv9CY^nm%EaibF81k$yDgOxT#oA zrFk0KR0Rj{rEH`sIM-u#msOZwJ)`6HIYfcFWE8PM5H-20K)8Y_+JSwyQV7XRm38y+ zd0=HHeUaztaFzCJS+wTNR=k=M_5p9k3j_i+WHhGLG%u1d{V$aNMbKU520%2N?u18BUyeMUM7jP~!{wi_{kNQ9a!w|kmrbW`=~1^1dxp25M~3ybnQn&R zMr|MzB)1%~l+)vxn9)K~g>cGc6B24g>sqT=6W9=|uEczMl9C||HF?lXrbC$F`e+`6 zEpWR3b@??S26>|2YM*_|vp066*=*(~nWKO)j34WYVycv}xHi7Kmz%l+QRlH>j+(z! zreH^H7lo||y~aEzbqu8L?k|p{E<|=auxsC=MwiX&iV4^dwbEk zDBP>zUClFiMmy8IeJ&qD$@$i0NA0`K=%fe@$D5QS zR~x9!m567oF$nA2n!Z*&66>AqlpK^TMq_#_S$9=uKfQ9?#HZ=NKBZsh(rP0Yw6R`7 zG1_e5g50o#xt$4m2bD67m+iO?xe=G6vd<2S!m33GNS-}7S>gv6IYjfKKb-KjRVkP0 znr9reGHq=wy_>S5s6s?3G)<{-GKkfD``vITKs8%e`Y6zKcjl>-US|a1R)86NkEE)? ze5bW$5knotaiGhP7cmC#7;0Mu1wwu#t#u{j$Z2*j^vhO%1J%cyg(b*~KU=|F6h;ya zV;zsXZra>u96CaX2-GPillw(oV6wCNx>O<$VY*bnuUv%Y$sb8*aYD7%L-e!o&;km& z)!}bYW)}%P;_$~v>@fPP1h(0*I)tj=#FBiQt)f~GiGOf zDIu62lzQ&#sTnNmyazxMX={|y0~o7J5H|}SAy~QMJtw3>4r#t5Qx*_GWZ94VLK&D0 zOLBO3ZCej&I+A+kLUzoty#JhhBC-~u(Glb*VD2x@Ysp^ZSiptzUCG<rwRnz_wHP!@qVEMa?3l-+7y6Q(*c1&lG- zhR`i>S*@hApdFslQdmt37cTOIh~EH%C>)aw?n(NCV!6CFl_{fHlqc-b5M;6o!NfqD z=*r%dY&_VLHu(UcJ#rSfM86}sRC_5?dBATg6$~KD`YgE8>5mN}MLf#llp2ectEcoYz)YD0KxE8KCU+e>al5MvJ!{SJVUYHr+5K6U-x~tF}9!hK$R`Bp;Y2=_AU)~Tgld;&{^#OT1hrFO7f5 z5_{9-hnCxHmWN2%^BVmR^y^fRn80*LjY7{B1-?wubyrH$R}te-8H#ec0^#cIWBBaR z!EdoYWiJVSG!Wrk@&>L=3Tm1IARD5nO7*lv!PSeR?KwXUCYLEUC~7-bb|gRR39k|g zo-y%#-6)5PxPkPs=|IAaqfpXc9Aa^!^J!(&<#DI1L5$Y)3)4a=Lm9W#Z>!9`2~#b> z&BItqpQQ`fa7n%roaV!c@JO>leKAwdJ$g{R!cs3$RNW)onQ_v-=+$+Xq{*Ssy& z{Is7(i}?XT_$?BnJK2ope>hnHBEQfgVdodCc;Y<6!T#5QKcVV zPS4$de6B*&^Z?O}?JnFsstJKSO%HBZqez3q1j#;Cvft{QPt1Nn4qhm|n)_=f0ck7( zv#zB7WO?~nhL3p*{`doYTW5TYg40PML9}McOzc+v4oYxBgup4<9#&7zY1Rm(*U%^u z98IJr@=|xIki3o8r<{C$B*U$o`eH7Phffmg7{M{RzCT>pd%dQYG^NKIXlsHOd;#64 z3^=FSm9iRheG>HS=vBZxq;vt`_waF^SWkvfE#|81nY)t>OO9ozVxZ-o5Ydf5sO85~ z{#*{gR=h>)+U*!r(}T@A@_QJ9M%5+Qyc}IK9u_UmA<}ohR=whmqU31g!fOwdyNtNt zNlgw9@<#UNrLrKn9r=n2l)@9Mf=nt4dsU z3MJAUY%^}`cd}{9vlMeR*(5o?qJ7<_jo#oxr%6X=-u%TWob%~rvcBUePRoc z>9xBY%a&hhM`p0!rR*xi+nuPLZ?pjQ?r%qWDyuno2gHDW#QOnc=@(1^MaVPz#@~2| zF&cSe+w`Lq>h5jtWH;!p_+%g5q0BtdCmm3)YBbH|kWcv<14P2xLFRo%!gFvYlR-Ke zQ*=km9EgzNfJJujb3cDCqYqDXXg1Agmml5GjDw--NiJg*S5Wwc6L_9yHPt5y>R{R+ zYSIbX%%rD8ct1&L;mTu!=gub0X>{ix~dh{hKX&*DX8s>+G zZT$AqQ}dcf)cXhe9A8WbcAg~?Yz?`~>Z~@RR>em81tgoYnt_(d7M86Sc&5Hzr-tad zmW7rO3;i=`9YMtpJgJfwx#GE*V0$Cs`q`9N*Oh+Um$zMw{nCtMC#_^(0{CqUW}aS8 zbz5E|Rq4@m{@#TuXn=_`hrZa2U?1FkW zwF&-XUygSb60G{qOkR7$N+pu!V$6c8*Gg?Fu=rlaYHB+3eRifp@^p5}ay{fA)WunF zGoQbOC??SV5~Y%S17~B)T~+OeWLBUC67Nc7XQnI|d@QMqNr9sh0x~$Z!T}t^%zgG+ z-W;UcG`mek3Ga>stvygPQ~oe^!QHhla7z2Z9A;*%w+w?4=C-kax#*+Q7Ux{p{ub&ji$j17*-F9w6c8@OKFT2(; zHmx5kYC^?#HJ8kbl~B({-T0M6C~-~dUdn28ER7p4V1XW^KIFg`g%{VdBiDNZ=*jF< zkKAKz|E;MHMKot%v)y5X?oGEYc@GloH}+!Kw;{R0_ocOZC$(&RR&xv&sg#{qYdREo zkL)MMvgIXsvDD>2KNoZT)RC#Sz8n(q2Ygqz{MUH9th9lSoQ|??SzZlUb0f#iQ}^;- zfp9@6eNv`#Vc@H_HtQj8<5@}$#UMgad&F3nRt2uO{=PsIMECMpM8oFDUz~fGslu+j zDroVWWL~egJLod`IcXy^UtgI6M4>r)WG$C2Bkj%U7CA<+H?nsiJ8Fng4D>bzPpTMiQj=ZJi)K!~Ez zmm)c;UiNC$bspmQ$Xw995!*-N5grku@9>O~#Q?RmGFO`O>Lxup$Z-gp@bPF@fZ=x( z`cT|XjBp6Id(45=O8;bKnw&ga+ ziox>Cbf>a6-12(rO>Qa<`nm_#LU6kfMZ;=2wk>F*$Gr!{wG>(F4U3Cs8s=VL%B^Lh zk&#&;tSrM0z=#Zy?m*ajhto1FWQ}$Y$IV;R4U+Fn z8M$z4Rnx%POX0sbO+$~c$8b?L)|-4!PihRMC{+J{A&B$WuG&wp%T$7122-k7v+e z@lH2X?5o{8z?{+E{}9$b@ryT9aN*GEs5n;SS*1!emCKpMHd_2xEu|BxL$#eb6^vWv>ztt*oArl{IXS#aC$#Vg<`w-Wmu;*f^$6CJnFe zZWASYuDHWq$JiFU@jX8opwq}2DlMd8%7srx=D4{H1i4JGsd=e+`s&kS%cd+G%l(=i zHs?czDhKNBl@du@`?I)wai@c>_IuY$ELtW9jSMP~JW4UaF zUWZHKs8blTEyOhrlZCqM3<$Z`N03RM}`^Gc4>CY^fU-)P9@)oCu!_^R*aLGx{`5l|(|u?XDSjBz(bJTdGxY!1+2 znte~MWl>CJEPG?wc32ly$!9zK;My6N~{G?Q-$~tNe|m^$i!R8LSEe+exdxO7RJ%3Kcj_xBmG=My8vm+w-Q-EdE2G*4Im+ zmh8@wu44w-Y>*!Ib;0YhT?6qos7bdX@R0u07Jub}5BuA|Lc*6emap1FOhv53?>)=c4z@9U4&)u(Tbsks+lwUm$Ho!I!gFR-$HGG_~LBn?}dlB|zDHe}Lj=MTg z6tK3nw?p(dO9&X9W%1aOBlL|MtYU4~_*i^`Mx)O64TD)hn~jMb3_7AwG2<;t(uw4d zac?8~X}i9+-EG|6;=-t3$p^Fg1h8xz5Og2Ew~ou zK}2c+*bwQojPF%B<%!%NZnmA>8|z826iBq4lY~*BAd~(_zBXUJ#35fF&f1>ln=Maw z|LB1HN;Dc7@0qyrosvbN2GTo*~9 zHMg&CD4bjm)T9y)*3elm3Eq^**eSQoKJ30h10FwmZR}yV(LXn~@M|tKsOW99M)$t- z&9A$l9|sy!WF3gF64Zg-6T-*FNY`Yh=H(!pJu&UmeS0=qM!69ZKG(H$!Wg(2{^N`e zuC8}X0;;g%ETnRW=D7) zi!BYM1@KupRdeQI1^Zk+FM)1HE0~svS6x~iG=@`ax~Kv1xGmkhdv^=`i__hYRbhU6 zV`{u}&)8RbOxW~WyKR@6;#3H!JWT3zB$o%H4-ogx)sEzOdG?4z(f}hp4(RN*F>atA zf*6lu9i95zh(X1%6cZsz3a?d0tu-IhWW4$Idv9gJL|i=~Flm?nk|$$RI0S8Bh+afl zcp=&`T#GW^-aY7`=Qb#}Oa|NMnanV{Fu`7M_e8SOmkM6>7n>Kaud)6O1J&4?1oy`} zqiuG7DxU#I;rl+ShDET(r{zpoF*e|D_Us&QMi6h`KSvdg0yDOlT%xQc*5F3{J9#Na zR4vPZAgU%|kD#SQNawo>?ROcd*@O&KqCONo!7F0de~`7A2pm~rIsG{U#gM@LKHk`F zzt!(GX}la4QtARVLMCZ2;_Kef$utQ1bupeTF+7ngp8SHW9nm1jucZGH>q78SJAoDR z$A|T{+F|n!(^!8TvInf-&7CNH={+@y@JePU8Qhe~Y$NcjR?3B=CgMdnlP6L%l6m8q zY+eT-Ooc=`AV{iMUhc=-Ujcouu+E(`+e?bpEq2TUTJjO3-LCwHZ}!q$cLnB#AIlr> zl&GNdA+nZ_vK>;39B2=Vll2p!uBo-@h}GV=xe4xYcNvl~ugF;GIFJTBiV}}PHn8l; zSs60+xrPpz#5u#5hSDq30;(mZcQ`ou9kp0yjD82N24|LwKtwhc&8^!@)V|_?S^lAi zyFA^HQ&{?hjK9MKJ-YYQtdmeLwamL;;8>EkRB_U;a3ofg@&G%zC&0nMp-$MJVC85S&#xvlPs&9Zv2c&j#GC2nq8f=@O#r+w@rnj# z%~5bAK23Qg_46NcKMBvB1%(rlk9~tA) z&wfa*s(;7-;o^W55sy9)9TkePB)bQ6`cs|*-RT{=&_N`P-*Toqm518KXt*rVwUErc zF&~_9;TTh)mW-KV4>!#4&nM1ethY}dE)h)I17PeZ&d<|Jt_I^SQ%NR zZd3s8t0x7)L++G}nGDi9Yy{qW)*qKv0vt_A%P{&&6=r6iR@Ge!Yn`Ostd51U>OOo+ z{felO!A=~t?!fYlJ}e$*SWn3!MmtC|>CVSf^`l{?k3tSERElN5ZXmV3}W66D=rG5IGo1xeU!cNe- zTR$^Ngd%SvGU}gp6+Y?QTXXH=O$IL?=B-@sDlYUZeai!M-i;FH9~E}AlgTAW2#KtF zxys@0DEVCiJpBuiGxq6irXM|J1G27XH*@VJ-QVFpZ7?#H&B+SW;8ZGPcCS+D|LKvD z$F|nc#^_*2kSfCf6zpEj1|=CpAWx`5&xYv8*w3&UL%|E$At?>QXFLA+LY$AzUM7mx zx6%|v^q~zE+ELW4$NUK+{zfZT4{*0CHqP<&xOu^w zE0^GUQTE`o2@QkDe{i#rT3Pik?`yH$J0-TCa4cNLx`zeyV;wkx=`1g%Cv-g|L!QaL z-@ToixT%s$x6-Yrk9fX!qml+(`Td!{Rp9Ej^~et*e`rF&c0DaeXi6#=pAm1Oo9xcT zuMt_)_aWjcWEX3GTNe8lNA|G8x}O~@=B{fE{ICZU%o|=xkWnf5J{el<(!S~zIx}`E znA>AVyn026+r9pfTW#K0$6Cdy<~#cvr<$`*?O0+!+&6Fv^YOxs8iV-Z(=p%5bhF($ z>wy+Kg(3WwkZdo&YgmxX2*4DUAJ5=*w@7Ll&}=F~ZQe*bzs&0VV~u}gVcGvSgmrYy zFD9(b%V5!Lh@eZ@B$;kZ1uoVYVs>k`z42TM#nS4P^PTosYI}lthPeanwP@rvE)s&i zN#M{3^rEFAwCHUs;kHLa5Ik_5aq~KXRHXm~O?z!F>=?DtZO4f6&7{WfXaERSqx3 z!>BgO5^Yod-2N1GU9z3*eQydt-VGHAnutF2c-mw%aS`r&P4_9VAUCiq1`FuS}HYvr#eQx;r!` znD;ssDo;JJ@#(EKjB?*-VP2$dQ26x3F`&(e@hl+GYeT9Kn=$v*L0Dumi=nC`U94cL zOTs1+G1*XXdFZ~7_Vp?`wSPfm<6b!vt}MYj`Yb9rTpUocxcOf2a(n4%5lPzy%a3H| z7B47mS&ZOL43tGOg0R>tLnF!5NyGsdSd7!b`}#9p<%;ntXFToG4UXV%Kj-oyb;P=H z4jPL0dr6Dsfov=rnhtZ~6Wfm9^_FCD`*7&4@zJkN_)3Xez0!yf_G2BEj*(ePUvtlE ze-W<}Pw%}aM#^KyMTJc>WeU9Z5t0mRuM0$1S{Wl>yo3u7B#%xZk-gYF&Y71tNug?Q zXzZR0O_BGW5B%y3F{neplk3b(wqNxeOpWzH{iojrW{kNB4EVi~isRn8P`9sQAn6g1 zZO6;EZ_dGqD}0;rqICOWXep_8cP@BOo3wLL?Uw~-F`vPl#PIoT1Y;jE`^fM^Xx1-7 zG}sJ;+5gkA4#K4l>-al0XPloz>(@PQ~?4CfkDH&pLJ>7Xlij zRdqbl7!B8cE?Ju8&PVy3mp9oLagsd(2Ogq;ZF_gRzU6aVHrn`xnz+PWd(E|vOpnyDx4HXM)K4rg3@25f;%kq>gGdaJpOj$8WeE2;`Q?px~ zJ_`iMXz)C`REJ9HRzkzt?6GNV5VwtI!cAUF09})Ih#;92^A^LdTbL9%S$jx>ROFTf z)vMWCXk}6pPJn44i%9Q_AqSLPzRPYXTgQmAT^x!R+T@C#QANJxq!uXnY90*kr23~> zDeXiR@gcZZ0wuykbG-CY$tAWqjv*oOJe#`8qSrx4cEW4+hZBkP#Ys2HNl|G%Xo-q& z%|u*bi{mah9DDq2)?tB^Z)oS<6L;D_zdvCeOg8ZU;*@E&)<-05o|k?%%|BNVujN^J zw7to^H$d~^n7*=KaFcTT(AkI!*fwJ?^31~fNuG@TCo1SvT65KAD#L-#?Y38ZR?Tex zSMo~8FQ>O6JHD4OJ1pN>BKCTB5%H&3$0Cd)k+lie1OsXUz&OeT^?-T!V~C**C~@%4 z$KeU7$CJ6rLhDCMye?U6AP41G4?&T5np?ePUht}0KU-G_RRLQZpg{GhnDlf!I7Uj? zctg*{>^f5;0We14zVWB#=(pfIYxT^zoe?kf$m`V0iV($4qY^2PWNZV(V=rkDmbG&9 z&0usHj-YvLhL%g6NccTSbWwpgronG$JDRQRzfIJsXvWM)qmn?Medx9DvKaV5-V46M?arDM$jL5@&kIjGMKOH%)G(Yu77zgH+kJmD>?t5<!;%!A71x|IM|{rxd>u*{ zxH;8Jd2V+5@w@9=qYt|j{~)BE;`!VB6V^t$_AA~kz(s#k@N&z}kV~`l(ls76mh{W% ziZL`dD={&L23E((#CZ5)yz64n@!9o1LsIOkxBYT&-4k!kMuqwO{`gMvEztb=MF%x( z(t62dR^P@k6*sfuF5e-YaM;GR0HJlX|Li{+*Dn(kbiS_sdC5M@EIO$#yW|@XTm*Z# zV}I?ut62==SwF*olP%~9Q~%h0_6IHzD?c|GZpz~Y^qNQf@&hZ2S{&pFYXkK=KEpzF zFwbPwt|MC4MN>qynu-JW7nm_I2M}6PeSz);Cu`h2K%OhRa$BkQ(|535`Fk<{@z-iU z!p+dbad0&8TSFhYBzd-*%5)0yYYG!C3@1|No;7FP!Pdyc2ar3-XCC#C2XS)wFFyYl zcm4Sm>p~j#e2BLBPV9Kn;iXmo)Ds`vjztC`{+b`s6sm8P?Ws(CT}f3B&S!Ip4_upE zN)sFPx3o!_7?aN4sQL^x7PUf=mfo1hOM)xJ0ufZB^-XGRt31sylN~)lh2c@Zk~<*! zGAV(J)z4Z_GQG3w@iD3|1b|o;QDD^Y3kU0w(NFfxLKM#sg~~77vb`rQzUFQmz2ks@ z+Zf~;Z$i5i9XPN^8Dm=h6xS1{=WosKx;=NA;%=XrQ7|ZNjb`T!WeW>**3<)wleWnQ zQ!ci-s?b1(b-CrqhF35|EUz1OnEX>yGG6*Ai1B5pq5>|y?u^s*LmJjzvA{hnyI~3J zb&K2_mx8FLm7ftzA_&I3UHXo{<}GVR7S>DoW9rAfOu9cm%?gM2wXOoh$Lo|2dqMhf ztmCCny(F)fXxXPoRZ3?`OTJ}*@vgdvC>z)!{A;v*&vqUROht5IZ|w2Ztr2_5d5L2L zE2BCpE79+3AP(UG-y9rVYEs>>nTk7qX<-`}5J!e9|4cT-&QNlxeyXNG zrIeU*bu-&`^c~)&NBiq6W?;tm0&k|phP)NxN+BrT%mdBZ;Gn!0wPWD|y=vZZL84hA z(++3}kpf!WH8uq&_(|?_$(xG>c3D}Q6{liK8aa=sj);nKP(ccUlUCjIz;9>o=@65; z+4iBvZlll;qZUl?tN9El&oQBD$vdNm#ol>W|7AjrQLpUX^dzQ5u=Y8BDQ}Ka5{qAx zfQysvH=+`?$GeT~IMh3+KeHrjLF*wen<7Ts9&P)GbzyXRANpt$`S9uGT+mXKLC(w; zm5D$97P(-b@a^`B1M(?_c7%2AjD8Oq@unxiGYDxZ8=3O^36+W7Ujzt_?s$;0Q{6O2 z$I$B0=$%E)r^e(x0;;}yJI1UCKK#(|apmc91oZ~bmL)CAv({bn44MeYVU1Sm&qMq|&QznlWul5sUIzz^ULTm>|M@Z#k7c0sdX{32 zy#{X2Tjxh^#WCUb^!w=^ll4O(q1a&b2ZIqOG9358XliA-WFwi(vw@^967-`FJ29E$ zH5OMK>x54of#d3_yFpyMK~Ma6pMUe?^~7n9;xC2H+<7wb++VaR>@~hJygLN_#j!Nx zz2A~z{EMS#V0o>8iO|2-z8fenlb?4MR$J6*cqkBk=Ol{aD)(SV3)M( zHa*vw)m>%yc|NA(`UHE!dgbbx39(cN| z|1gcjWf}}|yl_z^$Mzp_thjXgjmd_BDXSud7bK8*zdpMmH@>egRK|f=etKQ~XzM}I zWp1#M^d>225wq&mo8WLyad7(oSwbmTpI@vzZ%uXV)9@mul9weKs|EBGt9%z5jy-|w=*5ptr;{99`GpC6EN6cfoi418|G(sK*U zT|XVoVpBqzVifLD;Sswf%%CO1NtTsD*XdWMAGU?^&LFg&!|ZAu>IL%RCC&o!Dqq}a znZjLma8)Qc3q_ELoIwORd`y0wEIG`iE(*qFzvU_oN@^}|7vUDeWWRf^2e5aF1IJs~ z*a2vDC4TwJEe96<zxeBIxgEhQ+{D?A7(Z=yAh_tOiMlr*=h~775OpoV zJ(x$|L2KI$K9JMhv`~hx%KhUUy%^AjhCaW{y=LA?XSg=P2T3-up(Y-eDCA|XdGMo& zAV6muAT8#t3a20aolZVHV9L@-Zfc%{4do9NZ2~CI&8ZAOQyFk4!d&PAk(H z^>brq!@2mS;Z3Q^2iheyG=CGYw2f%(ayH9x_fPKCn4`qw6DT;n8E`0EFwmTd~7_m*E- zxP-^d(o5{}+$`^&S@a}L5+jU1oFH_Xj1{9E zhGX@W0;8>#4W|YC`uOk=mjCjnI!K2z9lmlFkMo$G?HuY=>4BEHZ=G~!oCE&c-(qus zx{38L>(&eDt7EhsGItT->{u!-9`D_8^FHjz&ar0m9EzqcY(8h@tmHM+g>YU1xAV0h zT$`TJqLyB?jr7_{4l}h5#dWYfQ=ptkSpH`nKq2U@VWufnFswKiGTEs3xPX?-Lb8r3orXm)?7qrXr#gk#+DWf17jbI|YVqvG+jiX@G>QJ%1T+9}2PCh|?zPnV${Z>uCnL68zHnDklGH;lW>vPN zEqg)5?u)5=55+;vW3b<6`!egHcN@6UH{QWl5@L6l^ z80HgJE}T7b{)Q-@_ejeylyCL-FaqDTyPx*8{l|uKYyrXdx0xo71IjWqU5v+x#!j)| ziSKZIQf~}HR;opZrM*tUc0i^1SGIBv0MpY1mRou*qbA7W3%@+azho33TjCm_%bKC= z;Wg&R4A0ARWNx!tsZ2FzZnG)eIp7%XaTl7E?i(0T4$EGP5Y$g3FgIVZr#*kZ{*Y_V zg499sHF8vJ<1g9kXMaA|T5!a=)2Iq68g-xrPCW*sgmcf0Jd&kwnKs$rcg&X?W1$@)*yp|{LJBbu!Kg@U>$R%l5qZ8TM~sfxawGawy7zDy3W}&6j<>DCibm~a zm|r;6pshy}D1fTI>m)ydL~3!^5=-@uV=LKJ|C9!Z>wYUv33`UmyuaiTtpv4FEWbHc z%bR6FVen?iV^&el(aGMMmEbt(7Mfmz$z!--?9#rpKOPjq?uK6bIfx zQAS%3U_vlKlv@#9wY5tVzHxRZErA^Ykh zyZd_#kgh~0RI;suK2)baZmKX-@LQ{k8AQh6HL^stf!V&K5`8a&C)Ii3di2PFKZ&+X zG3ecqkQ<&G2r=4HZUZvgEN#_V)_D^~2mBB@njMc*#iSr{NM1xnzxI5dpB-KI4Efyrg5+{B11J2vi2FbBws*)6z^1<=M( zn%uTixIedZecZGcb#?LmEP)6f90mVsvS=b2NW4)DY}M)*ggZwI!gdGfO56IzfhjFV zDjzJByeG(+cItcGqeRqik|>n72PC&@h}$5ouYUQezJF-Dq!#PgekpHBzi%biAex^A zBA47}swcXXgo5t%L9^$MU!8tG2wZjTUnJ74jT@)u{6M@C8YW;eE6=sb8_|%}!6)%% zwR+9s%>A|Wsl4>!5QdL@wjxRow9TA9bCkTszW-`I{T<5F-ZBH8$f^A8Lq@H*)mDuB z)ew(kq-pd#Ntn2E>L7Wm8lsd;xD328?3PrJb&RpvAfG7$+H(IHntb4U2gj2J=o2kT zd%`#GvZovtUT;DYOArqomj5sW)zUO0t?{Z_IBPtq;GspTKK`>>zG6u>4X*&c{E}&7 zf~k{Dw>`uyiue;*&*U&&WjMM|+s+7Llsr>9gSPOM7ryX2MGNlF-gy<=3t{ovcprQW zO!8#SQ&mAzwLI=xB(X~Oq-#dS?hm@AJUHREHrrj>NFzFqf%_%sqj_HfE+6IyV?wOc zJdnKWod#KoZvOnA>JL3JIGD4ZZmSUP$`g7jCNHkupl2S%C6DmNw_EfcGxLeiYd~3& z!gg~?6YDJ#c2sic*Ypm~R#tsBrTE5j^Oj0Svqd*}M&LrP1k+Wxbh2p$-PJfxU`Wfr z@3m-d`QHM4yJm)lH!?Wyxd*w?#U>&cD*0tb=Mr%ZG0?1q7Ky<*nG4~>!D++xBXwxG#;QP)$ zVYFleyEXED8yZYthCDS7gd2`bvr@OujRYB|IsGGviZVItIlio>F=MyNT&I~U7$In{ zzt~AYg9qK}ti;kG&=wrR4=Pv9)@ooG(Ax^k0i@E9hl|O0w4Q-WYM)_Rk1{eV1(@eD z=;b$C@7%cD$>}M)S1?9ux$VAldOS0nS1+Bkj21f!?YI^9UXKT5DfaZxyTz*BGh^E< z>vv_z(>n+AZ+#C)+rZ$XYeL!yq)L&1DPzNy#l6{+?VA6*Ft9y@@bKT72jZ8%PDvo_ zCrn^hPN4jy-ri-!B`+z`vtkMV$t_w#Qsz-FO`Kde`BRQPq_iv`9b7xgWk|ib6Wrkw zlw$ywYGqOvwf`$;l22U0a}tc1F{kAFe>mr8`?0IzGfIUFYomMkNswEU3Hria&7__q z>5OUq&Yi2CF@fUzN4A;udEwrLm9kbiiv1BOO+9X4T5zy=*~M{BZEt>v<1o_R z+WnyLQlH@R;rHU&k#VLLpjo)o7{Wy|K0s+H4R6_BN)9vmvH*hSeM>m|e?KgJ`AaKq z&hVam!;>#n(nj8iB`gr(F_V&hsnrO`qR?wkLCdL9`Q7|nfFb;LOD);c z4?8>Vqg?%q|5d;|0$5{x*SB}YPRFM*j)5{pP$0VaRMFVcyj3DuBqa0|->@V^V4DmU ze@l#~nxQpSj~ZzUJN3uANq)a|_ms#J0h^mVEK=Qb=|DVd8GPycNc#r+6HeQr7Y4{X zZ_^Hk0B;B?H&58q3@sl0ORoV`P<33-X-WSFNgCyNb8HD^x5Odkn&L&Y0c*oOKZ{9C zXQ;qescJ2zf)!J2cQT{{>~HA2`Hn4{;oq36rtH0X*8U|k066?(Nz804lgW|Kyzz!j z4_c0xe~^V#QW-&e*sotqJnUA_z@HbgK&fBvc$u)xzZA(4-FPF?5+N zN>%0@+DXnuE?4y2Yhhs6p#A(Y@nMMQunA9q$7)S(?pe@sgkmIK%B?sI-H3PuQOQjZLTLiTJ!oR< zyBx~rEN~r>=l1)_H=p(4?NU%ClwC-FjEMIEd}o$;jcee};Onu1-L+n?60Z4_Pe^p+ z{HJUWRICWMe019_ewX~H)A<0n2qu# zSE*ApC0u7i?xp&6=c{UdHe6y^+un#kN#9wkQ)s^+A@u#9fojtjGL`5JT%CuIEV|=T zAg9q5OK=nKTwTNY;Aj>-UIuP%p7Kk&5Oz9rY;}r5n7Vh1o&F`0TG_Qi06zl76q5vL zNS)2#b?$d<4STabz?QAW4+^+sbtuewLbtg{3+fJAH9RD=;!0U3S7|;Z#>u#r#~nVs zGLto%czHe@#CJM<9D1tqxtWqb!cwrEKr6J4P&~!g0q{hlUX8l@RX?!c<8^s>Ox=mV zLlMCREo>qVA^f!w*AbQ}DVfrD%9ip6-+c~Oe}bibM2vtjy&nQr8$hX_zn|B~wcIOS zYh)|Sc4h{bP0j&Q`?O?>kj5{od9bpG3Nkj|h5Qkt!&BCS#bMQ3&8~?P9FgPJ=xp z4Z7dJc9Jghqzmpz#&6Wbkjhsl8waD&s&b2Dl|Zy;9n08^P!xGKd`vYio>&a>H_#Al8iy^=S0QF3DZ@lV3k_iLAq5ZJqQfD)}t zuP#tjz;Lb??94{yg{XQg^S4c7S2B6~!Kwwfh)MA8lS(pbU;aw@kKOK9!xX&>$vdH{ z7tTmlWU|+>^=CZmtxe3^+-eD9LM^gdc|!EK7R#HvWM6go``kvrf#R-+Fs%Db;#pZy3^0Z>=?8Bq9 zt((c55Ro6ZGepQ2Q6;e)P$`v56+zy|_ZD*N*^m}hzgu1{mZn_8Pj6s63Q^B-MHe=4Vt$@Pd8hGXr^ z=XO?Iqexza6pZ%DOu1nprk>-nL7xNj&ya9i)eV13{eZSI{`PeSsxZ66zKp)55YxRu zc1M^cO{tSE$FcQcGQ(-WfmJfWN@9&8=YbBZx=iaxS7>~y9lIg1wS2L5J2V$MC+=@g z>?0j3a~7^Q&1|XSUYu65e=B5iaOk>TjE9;()0QOP~XjapiP0GI^XzSYm8# z@c=V}Jpt;?$!(BGy7a2!U7rz)AAJ?R#uFSLC!B6#op?%eL8W%NpORzMZk&I28g;)E zRMwE}OI|gZ4+3X(Wc#tj4`H^&o}9$HtEZ;*=1OibWfl7Pc06;In*JIkH7{18$}HKr z`GKaqoM)0&h~pz>f-WEj)0RHlfab=Jcesyyw82`@d+KZ~gjSwBTOi~j?nW& z5R*6o&`S&;MO;HQ&741~=lqin-0!~F?^>-+W?!edlF49HHM<=EBM$7bHI1iKLU4!M z{@Hha-~-Ohe%;-n;aY=q;vkXW@o|Vs6>obhV`yYWhPOnWmT#DUcajtxs%J6i542#N z4df$bcKcL0)6r7%1vOk7NI}+8#X!l!DEC`Y_i9k#rk2zZkL6L=C8Y$rTvyq2k~QvT z7*nUD=V6}w%KB2jcE~zgBBa&IE7%C3W#88%>(HbkG@odjkT)kLjg$94Es9U2%_9X< zbG<^CvcgM+#+;mrE?)l9lX^=gAc6}9by^2_AE-nHgg5WIbA^R~JfLOP-2BOB#rd{Y z4$?j?wsddA$1w-*L*u)Hk2m8sksYGoKovdS7KJJJyBt7KE%lX;KP`#Bz+^?*N@o=B zd#Ejiy7E#^4f?}*sMP!65BfH7Tlsx76H@o>7(4kG)8m#bx@}JL35&Nn(X1)!jA<;T z1C>vI^5N5F2^XLH?AS-^z&KjkFi}Y{-L>=FLrn^VtzuWV@7^{jhGaXh(}A1%F`rG8 zf6%g2w@d6Ve?5~mO2O<{UHiLbR`|hRveM^IUO_%6EIOASeSPvHLzXpp32K)F9#)AX zou5V&9mO}!R|-h$oCjEHIb%O~wlwbsJf4?rAM8v0P@TX-?8zo!o!oiF1#hR68_xls zrcDW2ty45;UtXLSog0iV6XRU3_*86fIriw$jdUujZdYJd!`Ru`IsA-XueWvDQo_y{ z_$fgS7N;;dV4!2tF8m&7E?a|H;MTu^QpY`~MMkQwjhxbPdWPo~{KL8GOQP$~}8T2wjFjw9vk0TrgVIz&~>UAF4EcW#${$o z%&QAGING_k5__l=lN8ZmJOLD{qYyRQRZ~6b1DIL*V%K-NO4ZYq%qm2jw*=3cc% zBhbCXgYxTqa~w?j(7b58ZNwMA8wK}x zx7S9Rz@8qXWkmkw&P&hQ$9$hu{PsylN5wSP%JnLDYQnMir-ke|uNhX0u%S_jj*nu- z!<1V2@q(=_cwl1L29ubKgSogImVU>?G|bjP=av_#=uy$=#k5TPSPhr2XF)NSom-^+ryM(>-a-`|ty?aBDN89%tbRncu=KVe3ers$<=`MR>Y5Gdt*L&HlMQ&O%(nyIWfEF4@||-G#mpR%^Kp zwObQS$>X`CDwp~8%=6_A;D@2k=Wusm{n>NbO%nz)PjQ88$&~)Wh*y-@D4Cp#t&lg_#Zf}J>99f#^R>jr1AOk+*exe*~0}X`w)<6;{RPa~H z6&IkMsia^klaDp(XX!sc}o)on`- zuh`fyPn)#Bt9^Dup!QFJNOy&A3MLf%-51%Ylp;O3;CHV}YuZfTm*?i*<+xX-zx3?{ zIpebVML6^osZUL)bwWN&s>fT6f2qx8jGukAy392!_bt{QfnZ1C^u~St*2x{M(zfN4 zi`O+Gw{}rCgs^d|-w-dWv`Gjt(i@({`}+qrk86UzL7lVt(?*yx+CeXo`5?tJ6k(Bb zm*g-w!?0~8;|uyiO(QCta&8RK$JfFDxcG#4`g`fBm{}V*RkJ-)+WLusCxshMm3hnw_oUZ_P8wIPZCB) zepQ+azyzi)(pxS z@SM*2SJ#G=IEBc!{*vi%Sbh(^T*@mLzT3xte`Rmnp6UKco@n;pu2-qaO#D(Dc!qVhc=K8EGZ&qTCO^b%uo_#7N1^-dL9K$&r>M)Ac0d4V~OW zG`x8sS#9Vm)19m~ymTi2!kZ!&PYH0!BqU6MhQ#*C8;}adDgGmcmDAtV7NX|?aHezI}`2b<2~6hu4aMiY@ZHH(26SH zBN{(+fD?L#qG>OCkkluV37fMo;T6ykKcd(cxm?<(xCEG?FV`M@$RN96@{@w|_iI+N zt4V!ucyioHbmf|`o7yBtLo+e>6UW(0M-F1f=AUcF4B_qp*Zz_Tis1f|dFAW5s0m;6 z{mMpb2KUs*om2zOj^#GF7Nz3+*6k1ww7eh@Y|djANCX@Eihfq zsy;rqVzuWjCPA_bodd)!M^hhjt8w1avI;D1&nOY$Q5T(3JbL_)T7%{5CgJ(7Rj2RX z$3nXQk*79%zE;k$e=>n3k7ahQj*eM`YM4(RCKy6HlIQZ@$VDAxD!Zpl%|~bx?%gA< zRQv?MUu~gQUv1au_4Tl<)K<3W;8~M!nG080rbr&BKsSl`sHQZ-)m;YukhNN|yHpLm zg$dDu5`i=&A+l;brd$8jD%V7Mh2(|)Mi~W84wUSXYYy;|=s3%W5-gkT5Ny7(CaQia zm+tFuSL$2ojef>8!8aKn%COZ(^lA6f03|7a4=*0RYk0!;nWBxmO_to0yGkT${S=o~ zU=q?GAU+bI_=Vc_myqAmaUrt3^j>kLYs?n!RLX0%L;6es{)&-SLpSU-){A~v$oj%T zX5qmFf=_J-@%yc#u6+yxLW(>pyz1i{2i;*I>LQvzxYLkTwzIY#;MX-%~V-G)&Ij%#Il zy1R#!_+iP~xt=r(Vtp@0fD_YUJFn|`o#S!osVSSc+>0qM9d+^&wx%`r=^{`gTGm>rIa!E zUMBxi@sWbMP$p{><}K1EO547r_NDbqWIZvZ;irDqy8`QKhcz_HRs^*D)tW=H+9U7n zPvr0|;S-No%0vkUzp0c8aUZRumg;*(J_T$fuiaIYxQ&^sn4PjoN?TdIu?fHq<>2-5 zrr(~Ev*W%+?^0wGM(f%(!-m{EG?E4vmanOIZSrGHMpb#?KpJBWIT0;HrbXAoX z8v8jk9=q#+lN(p=p9@+G$X9$)26;{&@)?|PuOt|qbCQ7>CW(7J?>yLobKcaxj$4N` zI@n_~`f;F88+(JgnaVARd03wseQ~evr$;HxWs7N(X1C342eq47)os3;mcyLJkfLj_a{3l$`r|%vHPkU z?3O5RIZu9`R5rg35jmDtn`5%Qtbv>{mS)|k;?f8)^FO7pfC5^S7;WlwW*LthMJ*%5 z{}g9!go#lk%B=*aSG6cWmt<8aT@GRwmqPu_#!y%^4v{G+M@-fOkWZVNgPuYSxJTdI zh(59%UfuPHKQ5%VD8^+!1r)X@OmJoNyQX@oh>aCGc@9t1>9$5Gl)^cYTtcs-O8d?8 z1!8IdGs3i8OzU=ofwKhe_W1+61gS|jHpkSnJlnSk&g@%SDh%!?QCg?&rQN*oA<0(# z@R0S=sYxw-uzt4ltNV~BV8^SQD{9#NRPff5E3ckqU_oZ@OP}#{-QUQ$Js0#R;wpt< z&#eRK(6P3+>2cugnUT|xt9Pu|-cU}0h20{Tp22ut1_ADH5F8h679FJyS0?|Y^y!p! znxDD$c6;C~L;ECF=QH3l>(bs)VhqpP_fkI21Kv=6a(Dnt;*usXx`I>Ye`R1q;yTRM zRf_e0pWXv>2{k-f-Qs%@v-}xll_XWMTRS*O7wjU^`->Tj*Od5J_X|-9n>OBdrjf!4 zh}HtNql(-1Vs|y4KE=knWMvBY2+1zVYfU~847{DX`vdXeik?T)z4R}2bujDc&ZD!B zAAPDYf3A_Swn+zR$0E(K6r3(u zCVd0_iwcwVw&e)X${w%npC{)<#~Ev}iEj0>LWZ$Hk8)E3ylX|9sT7`Cggw?pwaBd` zA1_3Iye%4>qHIs#Q@@Y7afN1iAgk)uGt=2g!5)TCwb-swx_*C#$7V)l_fhzY3>2T{ zpO3W+I+fCPYGQV=YnZfO%jrVeb!uGd;}x~&!qD%cGZvn9l@OBIi3=}@4dWuK^z#ul zi4OB*N5kwawotm;A#Y|r4cKa%a!a?6uDOyFK?T2jD1$V=&WDqwF1>k|H=5xsfU}u8 zvohzSOsVPZ$^Vtpq)8C()P}pDA?5p@T2E72s(4Uz3C42?lPxgNliN>+0rzA89}REv zwC{ZsEIgc-zT9dG#}Vs~+QWh! z!WaGY&cl5#;eY{w1jqB^QIgVPyYTdK>q4xs09$xsL4w_JEQXhUYt-T*4W-how%fsf zsn-6NOuKvo;dn7)_m>QPVzucj0{HiD|9j5=z1IHyjQx9W`Tygd8aa#tIV9>7%9+k3 zm5hWQwB9M@SOHo23JL;}D+S3JsDA&YDq@{1k1 zhso*&p?|8s{%{jJFV1Hcjm8h z^vh{tj79Xs*De)5?*q$k+Xnh}F9+DL)$Iffb9p@n+qEprr!rxVA02^czO(;~TemOZ z(--OYY*w6MuFlUa{if?fJgK}o#AaO6QvXzM#C}DOYD$^hBJiCET4IYr6=z0D|6@-b zg`!^HcgDMforK*d}tgL}!cXdK@N8*jhaAEUI(lSBW(a@g0ADerq&`IysApo^m4shy=4qc18kDDJrM?X zQ1&~v{4)MVLShVVy1@MropW>TwsLNfl=*@IwFIkBbV%9Q)47TJOr?wEcAdO& z&7v=^|2*E(txv6&;wJTP&javFHXXb-Cw)CW3Xa$FcIUj%HeHpk&#pc;he02P{s!ut zmK@MUp1#~az*urCGL9*P)2)1MGHc+h6xHb**P2D(vIbgRz=PQ+dqvi*=kjj8_~2^k zy41Y15>0ieUd~4RUoxKeppebr4bIFAAWlZa9dkA5We5hz zg(>M9EAQ4kXC_-)g_%cI&BduJ8$!N4^OCOgQjX2>9zuvzN|c~;Z>t~hJqxYW=N<4J zBG?Qkty+-yrE>r_`(ulztj^QZB&SG0bCO>goylLaF}794%fDo-Bqfy)6A5?_E;OF7 zz&gdO;k8bahhF|P^ZKLsW?4e|qG+?*j8b9&zISy2Y*0E6p##liGrKX-Xk_#rz1Xrp z&I6nCfcc*#WHqp>OemU&HT_VwkFi&NmuHvuK_b;*&u#|5h`@;-$H zawRJ$W2|}cP3Jrl+d5TVPSCyW3VkplbtRId(mPaD->Zs2O75?>)b*#a*P_I%!SX&9tz5 zzDTkJj$N{N5gjP66mf-x$7weZo-?|>H=5+Ns&BhK!)42u1p4aiEufnv=<*amn=@m% zbt&B=(DT_n%F<>?L2eOBa8z0kP?^(!ra)Ldrlo0V2kILvs4OI>2~;sB*D<^5jv*=x zJIvp~eqV0{i_KefL8Ag2ZAiVoq$9Q-b@!Jf_BJo~Yl|BoFygFgu=!GLOw>~pRd(Mu zm(E?@5mO_o(>|Xc8-Rp!AS^`TEZu5>N%4)vAhujc$CG!^mK3V-HzOhFUt!$gUI^oD znw~JLFpWUTDTMX;$3pvV4wBL;tx@lu^0mb0g9^g=?np`+gDC8qIysfW5X3k6`ll+1 zWhtC+4YT!D-Jn`-hyqTmX&ifR{6v5JxdmO6Vd#E1CIo9@5@BO3i zFk+5r{-Igy5UO@(xT^P*^niYw`)+H6HKBSZ{>;91W`|)G!PxbkN!q{^laKNy)Vss; zH~&mmWk68XxA&X3`w!-EkCsRh%)Xge&{xsEyuf3+Ax5~!EI)_xw(hO>pGFTm3KZXT zy^Jg6rMEU+U)YdZotj&XuHdWJ6gbZpcR7}INDr`$XnbG1$X%yBU1OCM&`z>;n$lX( zagwL{II}l(^@#xV3Kmcztoz)4!w~kpXCk48L6s%60CY9bAZu6BLse!_b);{9Tb-t{ z-(vd2`$S&f`f~u0KC0S{k(Y1LJyhSiblw8?#RWN(?UC!&+83m58o!S@(@~{Re(d!d z8t+x3mU;SMUA_WFrA^X*dMXB}BbA_=P`CKZg~o-L72!MbOiAdt@4Y(0F>1GFI=99M zGuO)<`0u2tL#UWE3&#fvIk!zlzM$l{|AZ153pa#3X$AKoj^4OBJKQJ+dRVQfH)mSr zn`|LjjrUvlMc^SuPs=zKyb}}@Q!iu_>&+Q2NFa_03d{WJG7l~39xq@!e(!Og)buJ1 z)h+ls49f-gx>FIRKJfZqJ{nd2i05Vc@@4N(Vyhp9jfLYhAbqHLn1iNu?oRudSHxOt z%7NNLn}x=}AH;E4Pw0po`{Vq98P;b_^{vd~wP%!BEzqlGueIRPJJXq0T7FWn3JPw2 zaOH8QWxKf$R@}F?%U1_6lJlUAsS7^P&6gfrVh<>mTFV=oepX)lAhXtMuJyARZ(*wx zW%I9O`@x#cz!&!Z?;~+;t%zTfU(=>qi};1$?!#Y>8|E6ZB+B0;gS2-lvvAYW;Epn; z|4>RS1}N5sp<->VcEdgqeC;B_QUb@fU6XuGtOipqZD5f7i6WE}YZLFe}2pIuC`>>r%37?j|0CR2NqJ7;3^~d`gQ|jmANv0Rf-Ri|fgk z?xBhw{@C&p9tqh<`+=+sIN`Jkn3EIcphb(rHlDfGMlJ>p5rl0~qJ%L~`iF}-$j>eq z1|NS;`LI;MN?na|mks&{laJTkAsq?F)plh5i5!lJ3A(}d=3$cy;1wClw9WXo-- z46b-OyQsUwuPa0e9jiojj{!AX?hah<-5Wv7Y>?yxH(_o)>9v2!Hroykt9PbBu>AsQ z&}o(Z&}97{Pz)K8I<$urHC>EZ0Viz>`o|gI+=;8f0~9gKj^VNXfqF2W&$w>B-rN@J zQy6dbEN7YhI3913imL~O&mLKR-*&<95hkwU6gAs5LFKgyJrbTi^IyO%nQd{Cj%y4P z;VBuB`t(cNm4JKK^vy!oI9si^(%Y)jPEBMwJtxF|n7K1=_)&D`4)W%jLQL2G+|JO; zz#+Cu0;cXWXc?^cWlgch%SL9?aRf#e4Y;4JOb~KAX}sa0rUA{Oy^6u_9$@xT4$f^= zCu~+~V0uc!Lcaow1BVy0L-RkZEnN{FB_Vxp|1gv*Y^ex$oO^xE`n2 zn7q2)bv3YUKh5=|CHI9}L)01x-w8MLZ!5tWJe*H>E2Fz&HUQio(E;y7PdMl8#uC#8 zlOV=0<&k*M6nWx@z~o)OjoRc~0q%B4`$hCd^NJqU8&^EIdp!uhOu4|%nBmdsFO(T= zC6?%gzHLLllP#iTQFZjY+bC8QA7aA!^uAjcu=IIbXa+c4eY};%cCRnzTVU`2x*0kc z9--}UH}JrqchA$eI@NJ$C(8%Fk!PKUZeNg`J>K+pN~%vh1odr~XC?YZ9p37MhR@wJ zd(Vm?*3tUTRalVL3n2 z!duW^H6v4@=G78r=Pw!A+}wo88hbz(>&*7(1Yt;GEF~wioH#Q4uA#dGw2eh(Vi3)MF=69*boEMRB>g z(Ho-M^v6x>S%ZpMxPrTu*~#`WmglYiKRsZ^Hl zeCQOF@O#->{qkptyht;R%mC&eCLyl_diUo)S;ILZhC91rOufVfOO@MBUHu)z0{i!; zd_W8fs@DlWNG9CZC6j@P#g)##5(wz}HH%xUv{*(>6 z-3u%U(F|EyU#~9U^qH>TG>9%sxiKYV7ijI|M4r2JE}V4$m|je8BJgayqQR5Y@Okyw z9L3`ttKUc|eE86Gb;x_z2J$j21l?dO!5)0d zx65>WeEt^6r{V^O@QzV=?CAttrGj1M!>Xixg_mZ6rQ>kZLi0n<*xfq^K))xx?d#b~ zdYm!uTr8?-(W(Az($u%#&({Epf!vjmhqv^~HOT@(_i@|j`#2UdT<|NkL1oQ>fST=O zsgYeRD812`obc1~wa)aRZ2DYhOzkg_sT2fW-T6IGVzniXVf~?5?J?2kWk@y|OWX82 zchzn!=7pJ|(9M%w^J|i43aZnL;K<-S?kJ56;Fg@p88X4hTdTE4po2v))#32Usb6lR zt7CJt((6I__~aX^f+1{n`ku-33%t1p%EW|ajRN0e%DbM>KgIWNU1J#&0YKVfp4%S% zR?X!l1Z6$V1%AHe*;G46pdBgM=XJ)T9@JJRbUwu|gfieeyw0A_+x8VT%a$nNbHA6< zblM|`hsQ=IDZ{9`i4f51du#F8{ENc!C!^uL%Pr8}65~2g$tAb_jR{jIW3cjfwC9!C zscU0>opX5^ee!N5(gVBT?s>hK56cH3G0S|cu+_16A%${h?ib^qu55-YahX_1rn5o$kZGk2`e|*+K5N*zE#!WO-xN^ei8IS(hR@`<5Olj@>d3N-3!>b=LaY zCHOG%ddlhUeo<-w_ksNwI}?G)ORN(2zSIq;NDgCuzpEd)%sDXR;g&i50N6O28885W zRhkGiF8Wq58offEMSC*Vo9mERNk*gLf63_0dk$~@cIFYK7X8TP2;8sw=KFg3oqtfc z5x$3XM*O4){Y!SQ6PoVne$C^R{ZOFg-C%gq`Z!RdjzKVH>ET$adpfPaMBNEX7`2^; zR=glx)OmcmhmK<&@|M#RBdclqGiCPgqw*~9v_e{hTWK5JeK~tL^%_LF8y{!ilGz>y z(@!7F41h%?$6Qa|QJgYV_rMK%L`{F|=DTQNA9uGmwhGy=9b~miUq|`A>-GlsGN> zl%@0#iZ;?H6T9)|BserSMS_)py*dN8kr%GvcbKzG%hi}Zb8N$kgZojTq~PDeQm44zv=<4LBni+wgOOcR1*2Z}g8Okp`H)MvJL0;UPvkbvTbrYDt z@0W`t2}4M%l^1noZh&tJekNsg9|m&a2*zY!XB>fUZ^3SN@F>xS$$81{rn`u(Occ$v!JThETEJKknjN~S z#ti+#2H9d}%X=8!E$!V^Mk8%!(fwwdR3g8J7%h=|$I~Bn!k*?9V`I_mrjFZwvSM_*Smo?Z#c-t1nrVYRD7E zTQTUc_9G#zIZZjHUi6}ygRAw{f$etj!QPKzj`O39U9uiCg$pqW%tCy^V&l@2Uhw!x zsHClYc-M-F>#C%Y77Vef&SYS${zyAfnsSL@%Q|4E-cC5W`A+INX+y zl}TfuW6@aCM~SIbnZ&tLCaZo4Zl1XP@{9U`9j~+69a;P#ZlkYL@79uZ$cumggqSvh zU2;t%@=JoEOr?QIGzjEEN|;vJLIK~7T5U{%B{Tk#>COWl>r zuq4$zLJ#36i}Cxt?TO1;0;^n{BeRTAlZM3iQ1)n45kjugumM ztM&2)NU5X(fD?h0Nwa-ZhyoXF(;j;*us zrwt?q^`4G@;q2Wyu6U2lM_M)g+>T4DV=~+*vgn6Nc5rbT={dzBh`nmf&2owT*(Izv zm|{N0jb&^;Sz%l@)1|N0-9S3(`N36MK z%axfh+S@*<-RqkpW9K!Mcr7LyUf;abLf1_c?+n7R^GFol3Z#fR@5PKSko>!j{Gp4G zfXt@8Jn00i7>Zp(ngJhwb`K)e`M4P?f)qU5cs9xkWfI|y&3}fXEAB8*oS*I65!#>V z$HEo8-DCDf>qK#<9uOu!{Xf<}l+(B4;KBp#eYf@+CB*m1fDW@d*!=t*FHh$#AQy0N8>v*kgS{+SexH28_AWg1Qn}pae9YO2;jw(k zIL@*lsO-tJ$X8Lx?Ut4qrNWO$4mkqa0g*q6 zRx?~G%gkdRH9QjcO3`lK^aV?`!q0wgY5u};5Nwr=%-Yn@4BEYl_Fot}{RdQUkMr95 zJ&QuebQ^pw8y?y>oig0m*TB>k_<1JJefb72u@X0~5b;Gx$W>|GvVFUMddDbti?-{? zZ1TJ+mj?=O?tt#5&inS`xF0j1YKs8T~mIw3$H1(Y6;CIL6U-PxIac6Q(Wb9df-{>e-xnPet&-MO#p ze9!qD`?x@XNCheZY_)CA9w}_l(tBL9n5Vz~1r2)lB_9&3e^eqyE9`tr`$a0KRSlvK z|9xvf09`U1b1q(;l45ByK2gBwVRzHtM;uDnw<|N}@(63l6YR9A%MTccjU5?ROSP?< z6S@C|iW~LSuysZ3kfj+7sLfXI_ZcaWRi|^3N-@Qaf4;T*+2Y7fqwqGkG9Ew-UkgQk zT<}rg+tgA8v&R5r*__cGLks)T=sRZzY|&S8=*>9qJUHMV4VEKl#&Mj9SHW4q?nmfK zDG8}>=>c8Q!ABOJ4YXaI6{FX>4VXRfwBF71xPX>QeK;3~MFlAvlB-U1VV6r2KTCif zSo(>$pYCbb)}u$CL^mGlm>BTAFir~*Aa~DNh>4MD#pA&_u7diwq!u!Islo`{sI-7^ zlsDUZ7y)$et7?A9XR|2m?QYCeP@-+DO*QHpu8@UXJ6y!Y9P*n8KJAyVXPXzvvVEic z1df$Dv)gbVuGRo}mpsEHFNsONW5m*T@qVhhqNzT^mLU-zCPMV#0k;o1#9Q#aVgbN8rRI@uDzCSxUHC1SrQu+Lhf2`h?=vA;c7>JI;kbFfAC_$>x+ zm5c-9qlXDn`aNqZP9v#eUcZyY^bn9DKOOKHy|X-d@nAEWImmRj`+~VBp99N4m6Ler z8JiI=tymkHTRas_gM*E zne=OPNXH?U{vE0_p%s<`H0FaHi0I&3ub+&}=6axq6$PDfmsv&z;6+z15-?)m9dRM< zw)mDgfy6=0ozj`c9}0enh7|ZxnOwuQwjn3)bq=5Ns)#N@@f!`UD?@t>Qevqo+uIz$ zwgpc0rt}s*?Tcsjci^gg)t}xX*}&Au zksLY}0ae#_@PK9C{myV1y!FAB{Udz(Lfj1JyGM$Yw8GaB+&4a?`N-mx8ry`0IhuWj z?AP-s9aeqQp;pp)r2VyOaQkwZb!+N+@m)Ncqwsg4?X!2Jp17R*$qEsl2~n~Yj`!Dv z4F41Z?H$7=_NR}m9#8zEXvSS+$#;pUr`6xe$F0f5CLoPI;>c`2h%w+7-<=nO(xM%f zZ4Hk~p3rl|g1T88>E2fMX>7fB8aY96f{IlNGg~bPub{6-#l}uOi7v^DZnl!5jBgF<$lNu4u@w0nK8Pxt}%ba!>yHC!9#`oN-j%ERC# zUg|pFL2#Q`hmGW@8+(19f5_rlNc*Vkjp|uYOpobyPI{@zs6WdyZJVS={%pKTW~oin zxOe5Mb1D;QN8!glkdVEc_G?fTe0#3$VLh;@cb>T|b;(5uA14QlK*!r`H@CIN7NC<< z16plLfQ2H)Gwd0kfWEdKGhBW1cd^7UVf8CWN1JRWuk=2PSWfFqknziJn_U9QZ<3z2 zYlCm_iG_U5M6IuIsmHzaLnVE-)wT$kXy@23zGPPtJcX>fmt-zOVbLwP8(qpQd(cD? zzFJ54pq0|X-a9d+rSP^4xX$ARcRXM1J$lO6!bicc)+`K%ABST(+A0|amO`+YSb41X z<>OKeyeWa$9;_ZKEt;59)nWPUdwv4<#>rM08)Da_v^s&S8t+U`iHyI#(`)#v-Z)Z)%*?{<@K0P)?f3Rk{ zqLcPDjW#(7)gWf#vIL?p%2Nsc&fun(LAGd$IhUD}a*R3Od`A|g9LD=?n3n*MOeNnk z={*;anQtuHi~C8b1HNCg-yoQ;j!IaiUvrse60=PPlK&di)x2TUcY#d5yZ|>`l$G6A zkSXFKM1P+iAgln)^=P3Kr?A0JU{25yt~Yd<btJ&_YS=>zcr$3P=WfiPhy6C zsVDO&y?xU7Rj99a*!+e26&cTideu4DTKXlONmoaynejY6he$4qUo~V>Dh{ZB5grBM z;wR2Y$E-a14v+r(8!3Nj#4Bd{p!{XVPUnJe-u1`J>Z)$K2(}XVBUqW0*r<9=*ngFr zsZK8^FMk&8U;eH7k1=3)pyReGU&W0E+{*YiQZkV)1EWCQ=N~Ax1^z`2CwEqwgHV-cfH z?rvYM|2Ai72&{KK6qB2PJ+b}5kz?eg8$qa&$Z5Bm`pcJ-Sgyj~#b=TWCRKFDO1^oM zHRa36yCHO@Q4x@f$*q)2Cswd-Ge3H4%E=wb#Fx_>qIRP+3YzCz{q&iu&#*=_6!SyA z_(QNhRA;^Bo|0cQvEfWba>C!>0X`T<6UQ``(&h?_f!zIzinXnZ(1cHbV*` z^xJmO6vo(ose&qXSH~;EKF@+1PqvljY^CmszO2Lh{XMO2A!XFKeNbc{!70%#(GBKv z)rNL$`Bh~~qNSnhs*cFqB1o%@LxYXz*wP+tlkgrGG=J-ZV3A*1AGGoXLQnUq#s(^$ zrL07LD-r2(`)=9L3F(mbb@O?!(yM|7iFv#(utFU0oLEk zFz9m{K{$3$RB>dA5!+Vt71!_I*0i{n5O&y4XCMo)SU{#j%uo}4%x?{4a#sIzaczH+ zzndHEiMj|KG7b|uC@0+uazO-S)j~@F&EZNx2PJ%<^Q2o348&#pOzLQ^CsDnK^t6k15*y)#XZ}wm`fIiOsholzoI^xfYim2l163Xj4U7vV;Em*;|rPPjh&G&lKt4*vzPv{+&2E~xaLYS~UT`!kEB=B6R$hj7=K z5EM{&hV#kytw?z80+djtk$aHE%&BG6Y&ddA5@WB3a{5P6Nnq2g#(B}uTyDJ0oooEf z>u<Oj_{aqnN>Es_ zyJq2^I3|{lFy{hr%rH)oTgrMe@&I^N#)YeSO=CNda~=K}F-~Gj$DV#!_JjAekKF56 zkRF6%))?RGw_MW+-cd;xE zL9)uS+aK+EnEAo((~1w&taC4U9KOsR1{iqIVO2G^?pnWej*r(B&uo}m#M)!;dd_|c zgq|}t%qkr{@%t%CR&_q>97Dq}uz@hr=l!tD_$t?pZt=)M`F)PH6SjSHA4-eYC7V+S zImy#ox{L!|-Die3l_G1=dj+-GU*rwpIlTMpLr<9J0t8dhLuSscBS3ff2F(9?Z} z&)rya1nRR+>zy1QgJ9K)?V>Myg5VKgT$|U!$mY2cZ;h;MfZ&TbF=KC|5>;4Z|FmdZ zBN~&p5Zwf+S0pA2Y`4A57)UAI{k5VMxSB|Y7>Ga5+b821wkrsv@?i5>=i?}1j2b9m z#yzko8pwnTv2oHZ@Na}z0B}17P}0&l4cety;MW-9^H#^+QNllpO$1qu!|GaGI&ij! zp2Zd@^cG18D#$5AFao|z7S2t)Y|Zuv3)!KDilg!&*0);`qAz!?%!W5q?l;9}4&WqZ z*X=;kzOJLeQn0T|CFfV2RR30!7usps>_3WZt#TbHxP<;sa#_PvN$E(U+5BdE;YCT< zEF+PORJdbLUop|? zeJ}DrFP3V&33Z(LkHXXhnM{%l*7-*fsx$!SB&8XiS3xKLQ3O>#{6~?GXmUwE)f(!O z_=d3zIj~Y8eb?Z#8>=+ZdBNAk*#4@KUa>@rl8`kV`|}ULec?=!YU zu1*I9Vc+BA$XO65j6A=0F^iZOdPZ)-F;ofbmgBx8u`60l|368MlSSQOIcGAoH9J1P z;5?+_ps!nD^=^eDza$b?vHK$}QRLN;T;CacE@KBaIR1FluuaZ@9XO zqZAF*>d4#Txx&KecD^dzx9*z#LOnWh0wF$bsz=JCn`EtjzL)AGt$Rz&-p4?lm!8}f zkT{H$<;YCHD9JcW<_Zha4A!3pu8E`rGQZNlZ|3FWdrF_;PuXuQj#V7O?B2HBltUee zOjR=qX8i&K_IV{~?%W)G7EL4l+V!zzs@v6jmfLH8Ik>E-bv1=+G>oI-K6{X@Ic zL6-Vhp{UXeNT&$^6;QdCd&W=tTVS2V)1R8T%+_R$J4!2EMO6XR8f~)wBwuB8W%vre z@otg0;+G6b=2|7wphoq38SzT0tmELEU&f3%Etgg<(V-1GI(f!FHR9i%jz>g)SLN#Y z^4N2DdNFJ*Kyp7G2kVn#B*+FHpX(((ANCEEM+_&yzu-wuxfn5KlGF((ov4)?wYbhR z>1S$TtHB;K4{dBW61yIgr`8txgzH*$AwIOXthe{bN|=-_67|b&+U$9pl-}zT9~x4P zPpee#_~Y3gY6~U)%_W5o&mx?kYmTEpg)z3by`>-ar_ggaGzoz0K@&&(BxwZe4f&C%b>vX31+O=)dY=#s4T0FtWitax($V`1TH=|LhpUXjtj6l7LDgNC^E$ z;om@Iw%{iZn8MwtczU|7U$-28iU(5HZnN-J-_cI@5ye+zEYCj|HrJ9+GMa+(!Z4aIUoReY61u2V zZjtQTUg1@Ms|L3>tX!|VIGtUfnFi`@qPC=70H5=$OV|L>O>vLzKaXn5Z+p4o`D?xr#-AK4qr>Ig5J{YX4Yl1XL{HFa(|EN@;_a!Pm(+6#dR8wGmA zCoZ_O;G?_5te$D)+&T!XbwjNP_NIb^=K%vaCDSq|ODV_h4BW9Lny@bVK_`8p3 z+NhNi^ntzLat6@s@cZO=IqCCkXPg_InG+fP-D*wf~;+g`-ZG(P%_;Baf z@Lgfl-z#CE*S-v&G@UGlrw8eho@xvM-iV#Tt`LKRvQ&GrIZ*5RL*HNm#o`ag>-`;{ z{NX>?9vcZk_;UBBiUE=)yc#UCbKdyhK{t)32T68(R@i6=>zhr_tsA5z$jfZf*U>oO z8g=zFTw54XqJts#>;t^ihlsx`kzJ1SRL^WcI68Aw#X0AWp9{%9GgQrJN$wTD4wkiIRYK9anTs;BH7t9* z1SV~Qo}+?8xX`_a+bC=i`9+!oXP=d*HmeX)_1d!*~umW8RcjLa!~;uQNPAd7E>3^x}Z4s+`_nYm#@N zVy9#c!`LqVexK@t-Shg@PLYGP*Y5U_bdQ)GTOo(ixjl>2CjDszD&-cd>7D zGt_STN9xyyI92F?fm-~y7vRn^=O*~;+4FfcNPZ--eI(v}E{jZfY5Gnhi)CbthBu~s z1>C-(ZT!O6luR(U#IV^{Bfnj|tDvGIOup+8L4|FvdrI1KBUa0LpGav8nGFpkfn_-X zf>~*{rbkZk@JlbAXXbYz1sAWy1+yqS>>$jkc65(>PU-iny2-$DO7sfOkU=RY!@&Yb ziC){dpXxCL{X2WxugUyvKOrwM4q2HEm-P6EWnAZ@*9{Em&T>*~ve}U=GFSIFEH4-e zPJY`QlIn}UvxsvCFzHwOn}xtYqTiKodoF!7gve!l4d&|-`C7KdYg?{e{=x6YxL8#T z=KBTX#`IGvObRer9%kn^BenEa9=oJ_2c|2^=OL^MV9pQ3qF+TlY(1>b_!^i52r^pK zJ+L4daw-#@hC7+}WWmV>;TFaX8wL0@1#0 zoy8nDvX@>nf4$L^O3&Zf0GvHmBB(Bjb}#)I^D~1#f2x8#P~1kKz+%%BfvV6cQ6!e} zuD7cW%G(PO`!D?gg~VNb*AXK7$EDi1toVx8fu4xmz?&&O*NFD&buLJgIv1%w z0R`@%2`XcTJ_6BUtn#699}X!U;!fWYrTShRlOk`9?(SGhjL!Y)d&_se!QwKXiEJ23 z4Db^P$(P#lbpteYHhU8n2Dvps7jk2Tnc_}%GD}|1|52c>#Rs>FTdx3*gHM@>chi&9 z37Fd?hCxIWb_tcYHBi<@*8eNjk9NY+jhStRNo}FCR69tp9s=aOzoj& z?Is#){(dP`3R{cETAUNtWol3GX<^Hr1Ig#hU@8(}tm5TIQv@O>YwM|4>plN2n(`~; zCHc$e%s6ZY)&GxTO^f_+$eUR~*Y$rg+j#it)VM8)c8o1UBb!eBo_V7xvSsic%waZKmFX(|kY{&n2l#&T9dDA8pSAH(^|9|i3KPtaWYXPWiLgLX z&h`scLHoZog#gvR%vZy9r1zH{AerV`nf>$rH_NLAl?^rd<|wKvR)s);+Z4w$#L&9~ z(2ejeW~;tCHyx!4!hf(gXX!aoa_-7OvJiqe_UVUdHi?Ntb-+i%cHAj;cp1l3=d=V5 z;JG?nk=LTC>MXuDAcl(xN{D7h_bzFTpKkkyPW6i^gi0o@1G4>&xmqN+2gGE_`t?u>qqB8A>kDNzJpk{JA1H4T6%c1&qvx0rDE^F4T(e_xY2zH#mXg%jnj|9E2& zcAN>=!lSEvgoJF_=a|zt zAFTQvZr)9N7r!za{^nX+h;kl8rN3>S`T+SJbZ3K2>;2xHx$#GRXHsj41cgh@f&BZ4 zPWN`Qqnqi@pRk|T2J@AHZ+obb8fjFVl0|YLGDEZz}BHwX? zi#r@?sTV)id65#*lqxY?b)r}EsCA+8hB{ZK@a*d*8}q@|WoJ7VY0sEYlbZNoi;uzd z$bAIm;r$hTqD(qYI5oKz9{^$+7r~8YE$6J%YgfhI$qTkgvVAomIbxF}6=i31w#k(oH+&~;t1Ww*oxDj(roz^B&!{iTmwQnwtL@7est$u|<<_e?bJUT4 zbD#se>1+P~zDKU477 zS0yip@%PNT7aE^lhwBs-E}{EW3C=nG)S1OSUC{{h$`~6%Y0!A! zh)|p?UvX8LjYiSr3S(Mc27IpctAstClDiS7iMY4Xxb|WO*+=^UO4<0G^H5#-dLn^h5q`8GcDo8X;r zr%u=sSGXRIxHdXJCRh+%X=mJ^d>PxYNZ$F)*#D#O>3pDiB4ctv>qbUAUVQ7i#E>^f zyB^=hCq^+xwB@6ISjHjK;1iH9!l>e+c$9Ew$Sdl9g5#`P1a=_b@{Ao|e(Km*ieUqyGb_JEQ8$ zY2<-dn9j;=iJ}K#>l5cxhsfSOgLfAkElP9uv+rlifBl++Hel1oKIpDI zx_8ZcMNVaQTV`T+D`Z?_V#6F$ZlOGW>vUq=EZ!$+9VbDPBG>kkq;x5n@WBRWMe8hu zFZvL3BpYgEsnw2ewf^EOAq1=1&Fe5vnt7&K;hoO#tL=optg`=4kBVBWoQFHEqk?igCeux2eVGda$y1~%OR0&WDBu{32^hyk$LC8{K4NBDw$fv~c~G+yVe1F9XXv8~$$dD0gcJwe-u0|Qiz=DquFps5++*{% zgg8H_%NP+C5-;NWnH1*Cbz~4s?(*)rcAIR9w5`$HdyPyKqdE?K6H?K)C%cv|?LH?u zlpydCASS`sP(xBwS`-WT$g05AU&j>s%i0AX=?zjcm?WOe6=x-ek*$mrE0a9dYG zgL`VQt64hZ@ZL$=wOZCr>#WiPMZeF6=L>qT4p1ps-#22rZrSmXEHI_GLiQG|l(SHR z=N;kwb*|*$yw|m*I<36oJcHzz$95n>cp-_f`TGj(+nQnH2n zsMlR!%jq0LX=_#m4c2V$e>6!`>Ms5KX@l|mC5su|Rki*8{H34Fa|Kq<)`v-b_ktp59cfE^ing;zpT!J_z1aHhHJ?6O8e$?oSTxC02C0Np)vfL$>1uO-rkxvoZVM_obA)~auSAWr?zEEV1dzgZQkpe@K68C{^FKx>Y(3VFl9wI5G0y*>wyy-8~gY?8JrQdJ2=hk!Y zQ>N4}#YpCiep9CNdHil`a^Nt3P04Eemr~W>scm!Wnz6r-TOxjF7VwAUd2GvuE~S3LaiQS+%3Y=FdmA%*(eq7~&PG zMkIZt@v37%702=9h!h9H3iMO7rKEgI>`_A!KOuoYui3V=YeCsNmEWX+>IvX?TAJNH zFB)CQj9ap2ChSr91{!h<;N|GP%d!b@@3$dB>L-NmU42z**2+Q1P7C>Yv;R2XM7293 zci`YKU4p{(y3y%<{}@#9NYTW$xQwBzqTQ3^kJ}*ohPoUEB`q7t{6YfFLf!<&B49pP zObI$PyfZ%oDOeeaUas7ez-w_fEx8CAm&nkBe24$1aSOYo1?QOXsOn8QO}f}oZso0CjeTDcYA^MnokwKqA(yJ zLiGM>Co=3chHoFx*0)Z#Hfb?V+xDtiMW@;IJNaUN|6Sg!kp1@w+b}r3jK6#I8;`?JEBkO8`E+Z(DTnMlzMwYGQ85vugr4K+h_b_c z-PRsA2CVC5Md1zZa z0P`sDfJvQjuF-5TIkLy`WB7TKrKOZg=UIM@fSZ4oW_&I1A<_nUR2%`=^o2 z`z!gvQA&dI6aMe#I!%9 zX}R9@)TRomZZ+G~_q$?X{{EJL(VsJbgOb{*9g%v?P}1!Rk-1Nyi_XOuE4Jp{OgN6^ z9_T>lL6QE1q!kv^cz!K!4;?cWbo56YIBF)NTrs%>|NZJm>zx3_<>4E-BcOtSwq@7n z&5JwcEolNv)h6ptJ`f-NAVZg2^m2qw?v1{lTm)|{TeG>}wvI6T8b)AwW6nLIGHPHo z?!z>gw9bAbW%+G==^lriWlQB!D0?gyNRbv{q0MW}ZcQO$ zy;HI&c691H`@&QElR*(j6DL<_s8No;NN6{tz3Arfr;yAH2h`oI`$K@jquT+f`R#WE z{z+)~9{e{0;81ydjYcEvUAUO35T|_}llnHG*-YH5`xO3;o)S&_qdwbko@ zuUpGw=@VP%Q2XrL&(~Gcx4M@+dqYP$r3j%i{X6rQy6an^zJu)1?>^!moU|kPkcW4= zSdg{-b{Mbn8x8Eyj)|`#eNql~=Fp?@N2~Qqc^&-*4!jAfLTpGXmec;|_|qkvl6umq z**40bMi-~npJOgl1m;)Z_f1)9LviFOY?mMT_4k+vJ1fqxl|$qw%QTz&;lnvJdZK|# zx{(wLZ)N=+alEGW4v7x_bj5jV>YH~*m9g|4IdR-l?qT#s3tTg5{lWG;pqXSLTE7Hd zN*a3OX6Gf+l(omoH-HhUH8$;Q8GCXy?Ms0m!`X+mGoOdfa4+fW15$UU#Rtozy^usN z^D*$C6x;HE+Hc3ZrDN{NVz@V1zZXDCFLqn2=ovnFx^(#%CKb8HCXcJ)$gOIa6#Hrw zfj4?h@i(S6-e5_^v!6LTdx0p)VIVSt)VEuk#x0e*HHrN7c|RW>X6FNv%3`uY3l+ysdm(xh(lP>-oOR zV3qZVVAwK9Zud!}a|gAzK~%ofPVO^uf=iGaJQlyg_NJty5oR7G!oF10M=I*cNt{Sl z2up4dlN_(?=W@qdttj~QAKZKmy)SW|HPUC9mSFXQ(@XocH`t|KeX`K6gwM?jq(-U~ zGYp@w&;U2bj6d#xdcs}Ax|^HRmZg3HyE3pZ*U1e^%aNHPnwIBMi@HQXt-Z`>@bC<9J(&0O zI?)qXAL{G1UqPflt?Cnw)SkD7M2eKxup77TFwvWbiXLI>839w z?sk~aafoY~OeZCGsE5#M+ zeQ+Lb4QLh#nW9b{FUXL*pF{hA-qtlpyx-H&aEL69%pM}S&Qn+IFYX*P*;~N5(BQOl zS(0}D8Z8YwbFYF04*@DGGL%0tsDNiXm3CV)dyAA*^6>7umlv}vF4y*)JJGt7m!}nf z0-f9rhzESU^I%|J=&J5=!6*eQMG`Q4W@YiuN(RNvdQaoBp7r; zshibS)2>X%EaV?YU;W~v82uml{if~X+Cu@jR9Lu3aT*uSK!qqidOG0E{auPBG=TYy ziWY37tw-Yz9RF=fYUKH&0R@_Xr|*=|f81}|=%|6|QMX3f1shDPKW@CRF?+t}6FX}o z7qjOYkW9-gzdx#^eCmoFjE&}G7IAlBEq4tS>H4-2%EgQkAO&_b)k9|# zUs)1pu%YiaaA1{?+#YM2{6O}*v8Bt{iE_o|l%5=lW34xV7u>~+vmkBAW%5$2?n?hQ zmqVVeD@IsXxt>5eht(;kP`(>({k1;fKsJwcNcq2s`eK$`6{%J#e+P3YuuBi z-GJQV?7T?JSZ?qhn=K-lZtN7lV)RPZ(_j1JE4g#sIbiGWv=wjW`TK)R{hF<42(uFq z5JS8Tln=R+Gqb~~27NtJWi?;2SKYQLB`!sGFRf}8O7wqNxWfA61Lzr*wX`OgReM`STx&GxzwlUBY_Ctj! z_&Ky}kbo`ZXt7T@i#YLi$zhIplMnHmyC3ryMr}lJw#b0Tiz(8p|7hSe+g-b=I36)= z0B|aWtCK1xjwVmZOg*T`-br7(`QRnp8)}~jALr+^q5H{w`v_H~$Tfr6asLFn@g6b> z&PT!p=y~li%om~R?#-MKeZI491KyeY1?qsBE7HuyIqiNLXo4>%L`jy0`!-&42%XA# z?kKKSGqTyJJ>C(=Ijf?G%pW=srwmRPo11atYgu9O{}IUZCx%tqCK@`y;c{lK1EbGK z$Fmt0KHWy`Shls;Y%IEsHQBxzg#&l$zpAyMtLX}(Rjw~2RmHtr?@f#Os+r012arRD z@ovuXqxSCObARZ)yqb3a3q0W>(n5MKie8Ry=Q2e!bMAikrBbf2Dr~P(|9qGDMylhiVsr-;Vmz0uBal3>=E@@0{*h_MMqq zq7pAhkER|oe?_`F?Y}%XDo7de0Z{v0u?#srfrPR8GBWh|bvsry z>@M&-IC_MtCpP#Q8sdma#mP~Z6gtgqCAdon$gw)b$H^*^=?~3|;KntI;|ZBkqnf(% zjTz1``){VwD5?vSqU);r?|ZrqVVhy!BP3+*!%n&!CfmQ z&zh{giQPBO8VJaQpATEGYE@stgkt+)(V>C+a((hJ%2OCQOg)OSZRq!BNnNTd6kNAc z!0W|ek~OiMK5(7Rv3e{E2bMrT#!B#Svca^fvb2bfBhXm+zxH`7&9s+~UMp+S1pKic zJm;+!;RNhR?dt{%7A3PT1V4RsTm;Qnb&si5VC=`iHCr}6=)SV6&brL3EV*T`ZJqZ1 z#UIz>bSNQpo4CIktR)g%F9xc!X&7~*AFgcK)#73fxL=Q-^wA#}t+voMS3Kug7|^2W zPWT?J;Wk>7@zgRaS!5_|SOxQ@z`cla%QJ@`1A*%$zG*g$I!rB9E4Yi-j+1&6V;bu; zl*kQVaahkF8`Ez7ubk6P`}ak|jQinOo5;0&!$vDK6&FNtq4mw^`boZH{7{Qr-p+8s z7~;HOvc=jVxg5o%j_n#Et|(8TA?S6%r7M0pfkVeh~4V)`>P zu={c|Iqb^{&i**fP4xgx&eJXVTE0CVJGb>(nQ9Zbv$_op7?I~W??xB|KG_|&foTqw zjRpucjW3nRaJL)>9+P`*O>rL25EH^xX)h zNY4G)4GlKss2`(q3rmZfbsZDJkCr1Z6pG`jfVA%~SL{543|Yfw4A=KgDTXc&_bVnW zP{|iYHCI*lTlWtyj=&x(y#2@|+8a6I^~~Aw(;UQ*HUEDUgD0xjr6d)7xKM#)T90Ly19qN>WHy72S z>u)#OH&;t1C@3%9h-62K$|mU`1(s_9wg__?x-MTPJtKO_@lKJbZ~LD$Tu}6O?pbqn zbk|TVMs2DOzostDdvx8++Uv?>xX>3dB=NO0TzVZS1Do-GowIxv(B$ftJA zpz9ZP4=mn~WZABv`!XsjRcZ2b(j{RFhe2;|H8&B(!g$z4WUy;~^=PZifT9=|*$Tf; zwuxDy=ZR?q=fo!*Upy^5+bT^TXMelqFb}-^Rr3DuXLwDm=ndZ_p_Eto(<3eBR`kwy zis|r6du)TBfmSvQ#deiBnp4ZZt~MgKOdxbpirq6QDu7h<^A07YCwwoCW!~s8Wpckh zb|yxcM@MAxY46uK-PTEaDrEfPVZ@c|_nvQYUGfD|ANVNH@)TgVau0lNvzHATN^_pb zEZGZoI_Fe7sm!vZUujQ{ed9Up#@C_i{Q-WYP~b2gC#VkgF`lGU?2pfCI5#Iz`tF4T z8^oirw7p%_wuc}_LYskVx7CCKkw#(2a^z)IPN+~k?nIO$5YQP?z84Sf=zk~Co4F?qwRUfcI{~C-lY<+0CD`% zG~16l|7wey{uw`-5h*SfB4#BFWkSsiG(077^rnlwfXdHL?dA#3pfRgl_gGFMf9KPV z1C_^8i23T-IOnvMomR{RABkjwx3#KuZ5yMS|Q@4K<5f{^BlJ$`1C@a}y=Oc7#QP5!b@p25q z&Pgrd<02kg{ba1&9%|c*pd48n$2qmHA`DfDD#|(5KDx;%B}_qulKvx(cB{5bWvH9Y z!x1x0%(++6fX_+4y{fq@`b*}`V-`V#8%TDr+D-498B)Ei*JTQD!i8FXd<5 zo?iJ*46$i$8E~9DzyhN?=nHs%PyoCI;MN?R;JeR&2H;}5?FECRNS3QWH!OLWwa4S8 zz{wWhkF0HH?xSKLpj`gyh3fQ_b#aa*SxTX!vA@pmIqGB|N;q`ljCfz9G?rumt^E{^ zh0e{W8Z7eaU$@73w>#UhpOJwSsG&JDgZ@aDBdp?DgQ~0o1Lyn0gfG~m&v_+tsL*;j zei2wapl{F}0`k|@(E2u)&JfKu_R71KYkslv1r5i|8WG{yjtZ#1Ze);-5*T&xO&V~* z78^HiKk6VOQ^pG3_Dqo$srLO~@LE12v$w?&alg<@zy!Yh@m$1PN1L*_fRwid7@pn! zycRfJ9OH=3!N}TGj#Fg^tn5@mlC+cB9)2!k!b9@Ib)NxWn+#tCuN08eNJzjC#z(qCZ=(t2HeomYUh#3yOvkSLS2;K9TXi3iS4i0k~ zAfRw~W*YtOKw}!EKkO=#en?B#9dO$a8TA;g2Tu=vH~!u8{H0rLFQ(ZORkg!9Tx(CW z!XNxZ@xwZMa&Tk}cFxYDXCLH@4MeHDJeRG|j9{|+1_D+Eue(?bXLh$lwlFNU3Gn=bS9h&}U=6TZ|VRj#tYyWSIu2%-W?B&B?+G;Bs^_7^NC;3T4-unD>;}G#My0HVp;Vi*KycPl`_JDq(KbP6 zc%U_-d*`-o$R#tOK&`O*gZ;+S$<6lHIMG%Os$3VDeguz{8826j^fZr})#X-jh)D<3 zJ2t{HxxIhNvZd%Z-<;UixW`T_Ji@qGaR_ZC-aO=Fns(_F$4E3AUgo^0J9#8-M?3|8 zrk0L5nGF0N4?kT?fqfIQWlbfeDH<<)LvZW%JLNa|Ko)%3@Fe=Xa~qUtxA%;G&hJHY zwji|;dsHYc+)XL@j4T|s(Y^EKmoSn@8_~apD<)CPflm&XmR}9yXzr_r?az=1qeZjb zEIzktv+~DqP{er*;+-vG>at^^C&ev-(vTJ{i;~#SZM5wKb*9$0VsuH8xV6GP@K={u zZF~qEvayX;u{?3LtI`OlGTB$MQyvY*aC4Fu@F9u$> zU12+eV*HgFm(N95hXuPrXJ32P9^q5e)gM@OKk*CQ`;2)S4)0|-Y^;8WNZ5P^QV=JM zWTXe#HFyR~l${J&>wK*3NYgzYTghsI3cgQR^eufK99Bz61u7Dwbp}3&X4Ln2&YVyV zUuBVQcM9!f&?(^AskRCl^t&a0&H6FE44O5Q)28Hz{V~T>S-23Z1JO#u$Nl%Lj)(n- zorffa+~iit9_1A!a!4VRw@81f`vxe@g<%zQ$b~9IJ$TM;(|L9{T}%Az?UHwl zwU@TK{}^7WP8G9^?zm-1HO;U6xUBqmGavZ8B;#wPiSMnKkhb~M{S)~~z{Eg%dNZU4 zifsbkgW+?Ead*fqx3m%HRI|KNkj*h>xJm2j8dl_*ENW?aaBV|>4BB%BvLF5%@#C$y zPr9IR-i$FH54FK-hfAGj<){@RA^kJFY@D_QLxjqO8qHpexRGs6^O6nE(aBX)_u8k6 zZKUW1fS$Zgd+Z(As0J_NH+(^tQ5D!&YkjDAJ<~qMpdeTz&E?(D9M2y~+NR#Lk4C2L zlL0b`fxIQo8Ce#~K0eS|+P^VWrC%}1FvbR@j#vgQK4#_lhW<|Hdrpk?8OPW)HocB{ z#~+B+;E2}J(^D%&KY@#zhg^!vd(BnDO7f&B5VtzbifO4Y$aEP^^INa8w`p*j1>=GW zhEDoZYfLaZvA5{zL%Bj|T&Mj#24o)%%aL}&C}uMM5dPt076{Svg~7xU;$GtBQ$i!R zdI;;)vAT=4k2WK^fS1K|upVQ|MN2jqeh(%JUpPN#FHYC4f>tD}_FJ2jkF`!mtynjY zzOz!^9&(FF7_=@=4IO!}qmScMsB^ZC6?ITmCWZ88mLYEMN5g}g=5MT>A2rw6_`3Mi zl4b+jQtKWg=wed2S3{ktmzarkk}s!Sq~k!T z&m(-evXUk~!$Yxa4`t6RH6ei5fsmtypj^i3DA?W#_1Z*FW?KQsP7g)hs&ti{L-!EaK2*F#&bW#!GLlW3-xW8?E`F8>rQJnNx3)C4USdm7_YCvJMEkvkd9>C z`N;0^jggdgzMhy-BID%KS-#+_Gg5;zBc{XS*&&yu%KDhE+*2pBjwBWD`AL#Cnd%n) zSX+prew*AKlL56=VL`_izV{1`v&e703a=9pe$=KP5BY4e=BLZTe#U;k#?sT-o#4=* z#p*(+*Q&#&)zL*A!qUrfSw$G(aLzJGuWej=>k%U8i5&DQ|Fgb2F8zY1Ka1AYe=lpH z^3E@8+#<{5s&5HB%CkwXaV0K%;|qUe{AonS5$8~ok9(UY3$&j~#+%bkX@*-gdk$;K z)YRIAoqZ=eFNp_0%}v<0Hepp z=N5v4FE33GROyW`PY%xa3G=;jj3_k{ai0Al^NOY-`yGIp2fogMzeXM4omci`g zbxYkNy5!H)+s{s^_WksM0s*!uI2TTrm^KDmX%QweY;~P-gH)sKe}dEbAFzy4WK?hW zSh>c|6r5xtzqc)4GUz%aR(K`FrS<)`#5!~MDTL^1~BQWu~~Er zxM&jF*o)P_*x94#wp*5N=BFA=?L-<@5UE+n)jCo7svPO|?oT2(A@PqvV57q@?TfKq`%;f zUWCd9*BZqJ8mAXQn`bP0eiqr$>uO7K&=}Vcf|k~$6edDaLwjrXlaaP}WZq0Jqrq1$ z9XmX!7rNg3usFhE{zy9UYA>{Q2&^Uy0AK(n7(cit76KvvPeX?O-x3u5KX_982j^D@ zf~JkWIG|ONjvY>_(@kzp0cCSTLsDWB!6vp^4lZ5;O=bzas4!xJu(|X+W}(x8QlBBw zC(`Jti=$1QWXLD(a@p$^h6-OlvUgvU`;2^J=arPGwMwDa9HV3mdi%xk?qpJ07SDad zyxO8&s;24-?wYEXFm&d&d2~Ftul6k`SwzMJRvcYSt#U)8uh_f@ARw(lWIn)2GQQ|v z>Elr!f4)JWwfbM~@rv1XIF8e1X9?>-_%4++Wzc91+ym}z%y2FNGd;&JZMrvC==KBN z`J`uetp*F!&A_jt-B4>&%9WGTc~4S!a zL^y()h@=jw-N?`JsSDM$)PvSO@XRZnstI8(AGtwgECmR>%0sW7B7w{$RxATaSQKs{ z>58WWOX1RFf66=(F9xXAr$*7|5Jt}*di1-8?L2mcn6bG@m*iw`t@44Ickh5=V})?;>F2e5PiHj@TaT*W%)c#crlrWwA^= zfc#~JtKe%tG>7@u9$39lmBEv2%;jBW_3w>WNXFRCnM-{KR?OuPNXN)ba0nB}2&hZwDJzoN*J%@C?Gz#i!Q0kgZ;la)VV5yIWYWUP8ko2mlmdq* zF5)F#mm8VRpiiQ~I17y(dC!gCJ%}nWhh;wfK3*}x7{sqREYV@qPY*LzJ2+iD{rFb| zez&JQ`li+D*A{^XYm=~Dia{4q3r4HIu$F}-h$gxqYEI&bYm@!Cg}Xe_g{w1Ueb~&M zytuGGz7iw)@nNQ(yPCr^;$0uMGfyLVS+qqeXuZbC7DVR4$*s7k?s7bm488R{@$#B{ zf$>mST3}Fz6*Q3MDGQ=^tu=a5;2Yqm4@Blpp`$`XO`P`e-LI4e8>paXYZ-FQ@oqLf z!efaOQ`@c8Kne9S-A%$2&S~`xz8Ab_N5Y+j)7r;=DaUsLo;1F~1ZgmyKcY|JlRsaS z($u%bF=`U`$Z^x!f8Pm@6^$cpsF6-=OuO6^c~uUuqFM;n=dx{nJA5Ct(RJk4e4wp- z;qTW6kvu_L_tXjP%^@mPynmkVa5^N^QZqI%G!ipfoW#0j@3<$`lctg&E?DV@>$f7PpXGr+Hv-n`IE@5M)ct)YCQi&t z`pi_&8;}3mTeGk3Bi~2z3LerwM4_&P2~+;AX4R|}9aVxu-^)!(AIdeY7rX)Lu(uOm z&sWP^C_WC+aTaeH&JD>@=`gtq6nUIbX|91Y%z0yzEIPyeW;l41Qx%uzr^oZ6r-c5F z-|!sfIF?jIU|f4TK#Qi z6or{zJKZ^S>K)KkNW9yOJBJLAqAkEiS;l2m8sqzBwr0{Aeg4mhxQbZ)5K?&KNK>6J z;ze1Wll7ZP>tNxSr*dx6>bL)fXperZHc(L*$g9XiU4SixXrrG=+^KGrG}oxKRy4Hu z`S{kS#9=9$>jHcd;dw=-j0iOOM6hI$NX>7UJ0L0}S9%@;WU>VzH6$T118FI5FcRSH zm+bzDCKsiFldr;}FVoFX0G0C0l!d5L5HKugD}oqTt9rCR* zkHWuvZdGB*Ca7NCoP%KU!N7SKjJbBpo87Z1v<*YWAygW`=^iq}<}SD?_z1*&1mU5OLpC(Ap#Bl8xle)QZoz(}$o%$>utX)V` zJXvSMg6vOjUH32xHZnVrpSN;W9W-f&RCby^{}2?)Y<>7lkEH<_*dp3^-Y?#`ptyut zant9eG_6Cj9cG#OQ)QtCh{78{(d7-$Ts`iP@&*{L6M78jdKr^6;CW8-xnH}M9Xk}* za5~U_J)&_RfYnPWX@PBD&iygoIGhf+uo8Rd<3sKGU-r(^fL&@g)j48~rNZnDRKG9Z z_7@PL2&H~F$eODyHG=R<`js;53lWBO+(40^aGKDst4u;D$` z$qyU51sJ=_U*Y~>m**vp;u+UxKz>-^!R9|t$)a9O=8bXHbF32Lsy?oK1DwOw3NkNR zQ}x42nB&cvR2Ju4cj0D(v%S4uV6;q7&u2Cv72lpzo*SO-B`rr>vaAqN`3N5N)dhd`G0# z22RK#_!UqXi`GeY)+n#`_tQ?qO3-uJSU+iqOaG_BouR>#R5fYA9x|Nxq(QB_(BuKD z6WTx^lbgJhlo=y98G^#a=}uv%ESYxg`6*4>7AYvb*~(9_JT-@lF4d3KC<%F=D5{|8DfHR2Ak@Se_5~CDlXguX@qFQ@N;n_tP9^rN;Dy zqD;}>Az?_2_bSAYL4g?ey`YVOUaleLs+Hf*W47W+cmvujEMq&gH}oRX#in^=;v4AF$6Niro?+V|>X zxE93jK;^L=W)@;$r0H-@>Gc`9=Dv&FWt2 zLgV&18$FAP>;SQRWW!t1p#TfwZdacpXmpF0Kk*d)U%Of>jGM|E2z`0KA^a@&;hGA0 z3EiHFIX>`g@mR-I3Rh(J&2Io$OmFQ0dwcwm4GdF57Ltc7tF}O@K;BsGTHKnLu38ra z3!bISLFXy7Iqd~^5Y5AW$y5m3A<6>rS=rMYfNd_K`nJL~`VA2K3R`gU|IeF8F>ZB@ zI&B`kl#^3;Z3`v%?Dt^>WD|n{+amHY46bsF%v4Ez>zwAte1OL8h@-;OHivBW+mQJQr@SGd8#smt;e0 z*Sd)?xBbrmE=Fa2ET`<>^< zw`PUzk*fg?M~q=Ep%u48DAg52&XAE-(dJ-^kDYKW6XbJoqe`+U+|&xrAy$Vt?ml8E zPX5n)`JWL=0xEMpGqE?qs$N&6CM#Ig%f&x`aWT6kso*nev2%afWj4F~#Do99)(+*r zC3do|pXg!(9;oG>GZQYA&CXPfIJbORk{8^v1=Z7m&^h6pcJlaGfFN!ZmM=Njz9LHS z|J$A=a*Q^90}Cg!C@o8$BRjXOln=tw1JTW|*wG3~VpBJ@UF9WNO9}$(NUu;45i^N5 z6a0T-XR{R2;)xQGyG38;-3Q&=lwA&lLA+E27!pCH!wlvnr7* zm4DIa_;w)GWM0R^H$E2lI?Da$i2pseqNci)B7H$AV-SdzEE%6@UC}!n2~d1{*bJ*6 z93xZNJQb5;hbAIaEBxQ|9fGYvEWD5;G~C2Ood1#jR<*@%HdKWe4Or=Ru<$S9SH^V! zg&+L|U@r|ufc)=lkdiY{N5Kasxg4n#+JeEp&xX(Jo^}NeBy*o$w^rL63mN}^ zjg=+!WVlc#yU8GKg`Rcb?JSUW&d_N)6Ecc3nJ-^e)oVltGV@;Z8M5zihIlsb^e3Dt z3`FgAuIS$O4+!-C>Tfya{beCSGx2W%*-CXQ>T-xwDRK&Y+uwhZc4liKk%`poSJw85 za1KoP`AYsE)W>39N}&7Y&yRk?k9E|g5wlKCsm1Z7CnW_{m>^5^>#raxcIzv0G42~E zIe*x<>*mc1uT89XM0@X7Y)x32vSjPT2dSlNKlf@Uk0sSKr-V9K197tLFHq;%jM))N z#Y0bkRbe7UGBU#^P6KXOAD#qSa3{%ogKMQOm~AOFx4E;qX2)GDL)KSrDU3K~L2Pgq!qzn01cy7FTcr?U z(b(WjCiwQq46O_7q5IG_jqObg?A{3nNpi9{sR6)rMjK~K)^YcUg&Owk3%MYiZ}$dU zx24J#XOSLb6IMcwU!OdR3{L8IDYv7>aO&vbH!*Ynj_Oq4jV8lBugVHWQ>Ye{vXl*q zb<>(uriN9blMA@ED;wdn3J@J}4jSUE6G$$G#?{`V%i5FwWap||oitx1G3Re#la!KA z_@r#pTtF{e{PzkhEpXF%sC$3F{s6ya+VtJ%uk($dVN)uGq}dz|ADz||5%wZWQ0L;J zT_cqlDu96|Cb*2A{9bg_L8eRR@6;-mL2x6K_r%i}Bp4JO-$d2TxF7!p5MEtLAZoCp zt~frbXC?U~$@jT#U3}D&ZRq`WFunNh7tAAILE~?KQlu4gB-TI!%4`au4OwW4%dU}_5GH$XA>&!n{zE6|KKS+gwU2mcf#hx%It zMsqFRo)RKpI59T9(v02>;oM^*Cg~pbVT(UFf3rJd)T3oC#=~?f&AF7`)NL2k^kYb+ zSOyZ#+cdQ(>sX3ZQ&6g+O*>$f(j>+W&xQm>1a)ht`NWF}@05(&AA^G3o>bMOQ% z7DTBKVX4v7&PLiG0E>aS$AA*&<3jMU;R(gBfJj@(q`!0n?&_ZbLaD@pyTA_eYZZgh z%qF2IgT3e~s6YHQ3J}#>}Ik1yXLac}+ba-@IZY*5X28n#A0O z);yQo4unu|kL_g#&d~z9CODf&Ht$A0hv8j*nXARfP3H(h;~2mA%~+MgEYU*CZ`u7T&IFE?51ORs(uSI1%ot*tlU3oxw-E=JeE6h4v3j;ls6QZLi|%iy zhTzU3-?-~)R zo+>_?eB^Yyi;&f}{U+8|ax9C3CJf;W*zLsT#Vcrn(+vMJFT^`a0a)n1cgj#7552O2 z@7=dYVT?Wy1!sPr3Q24L(HBDv4p!ST^bi8OC4zematCiW^X;I#qx#ALjW==;?~glU zWJ3FX(06YJel_g*!ZpbirQeG^otB%UfefaZ-+dPj+&Ef98p=@UYi=TL_xJ=_*b0qX zwJhB$l(2McIuwb_!RGUS*PoQ=^@5f5Gqle`zFJITJ78YB_tRY}8ZVO)4 zwrDKm9=gcTgaZ>?odqTV8mcF{aY$k;xj)XARYhvmscW*W*zHani?h%DAa*T%um7d6 z*yqn9&EsJK$vFEt^n=y{v;{o~OLIUOG$qOJ z)K0b;Zc%-`)Dbr5#;NE$wNL0~vm=q`EB!iSbGY@qvTeY`={oBe0u9MlA|woIpFi<= z=290pTTK3S6$Il^7q`5@mncxcW)<$1#1H)nO zZ66#r`1^qmKl)4uEOAs>UgESw3^tGaq>WRfK{&HVW&4hmQ(BY6Qz6wq30bI?2`pWR z(&ov@{KLQ7DPJHjf8N-F``ymV8w*$=KJJ$!XxOcrb1i9B&!5L+{@!EQGZNjq)AM;F zSdzar<9q?J=*v6Vg{=O@+|0ctO~!pIa+a@{SD!?!o%ysaZgG5KJU>|0?!biMtI$r* zy^jqX zkZQ|s-sT#2s$ay8qMHw;C3TktR71QaMy&Y_Z6yf_15&uQFQZNk<&iVJnSDHzr4ne! zi^=Vz&8#sT3-wRH6I@cv-=kL7esz|^>o{92>}}CD;X}z-XyuUN;~E~sgKWeT3&_`i z8>#em@8qJgvEcp;LCz&P?yQ_~{9-;9qao1I-^KW~*f;U#cVe*c6p+Vnxc znsDU+TvxwxUaj*H*-mwD0gnTBs^T?#O^t#oTZst{ZCV|Vx|_@PsVWNr?lkaR)pD7S zxAPP_?*G1ZH#Ayx;N6s(iNeAJzMP#~(CMS2eMY^;<)!#;w0KhRRJ=-%m3U(G;>4Wy zo{>%0#Ejln3HB+$@`Xiop5bEbIU+`}blmbC@AZS{(=GnSu1C#@haEaSO&l^$^_dvM zuY8YV{<)~`5Uw-*$pxcI?h%qq?{B^&s%wlYsA=*nB6i}@DSqz-+rVMSK6=$>6e5nU zOwro&!P_D3OTYsbn@wpc;M_V_b(;^zYNg9PjUX(ffvRLD!uJL+oH>{`g(XPdtd_G9 z$vdNmA&(tJO)SPQthNrvVvV0&Kf0_9chDDngbUk<8zKKD9Xygaqh&PA9wmV-#k1b> zrFk?G(<^>Xqjp1ex)-+;yOgTpWHI#o7kNHZ^>a&3%!Xh; zVoE@c*SqpDTWj1Q-3=NOv5yuzG>2sRjZuQ#x~#3sZ6RhYAr>{Mx|6edpio0Qy#8P) zXHr%2`m8E4>EfNr5>u}p{V=4xe902Y@&h?Ccu+xK?R`j2+?poYt8OEWj+?Sz2l59# z#x@29$I7NvF$}!SW4wk%?Qef>t~dqt_y5(E+pEM1?<)e%wxviOy;i#P&$Lk*%OF$P zASqO}=(tAKGeF$Y{b`4zh9wCXw)o$Mehv7R>F zF>i_Cp9NZ*AvXwbQ(hxzAQ`I^lBqeQUhH-Ih?d_18kCJU)STXdl5L036j*7Y0Uj;p zx~yW_@6Mdn%8{A~rA9%7Jj_=b$|m9j(ODJBfIw#@|HK^oTsF$ioT@Vx13ps8-hIoO8<_UY9W;Pr87{b!BNRA85D+v z*)bWA$!HSbK@$aO_L)r+p0^Io>$e>0-wTuHR3p5sU zjgX>GY;*^>)WqgI8H>wO*D_WNps_c=1825E%1POgpH8;nxVNXXPGCxyl@w(~fn$L) z@MeOnvrBL$kgJaS$F4u{)D{n+{+E91+z+RvV(O99^thLNx#8Uq?m-ZuDlT%Ri`E3S zs?@Ely$p?MJ~S=w1?$IgQ`` ziCeV8s|C(dwxFV9aw?=J-e^ZgAhC-Vf_txVXHaYwb)5bo)MMB4b7Mtfome6Vb5KhK zU;QXwDqUX>0RFoyZQ{pEOWq?eADq_iTx#J@LsDz68G>>MX}G2ninJS=sEoNSpam;F zkrB3Sq_XfWme(;mqO;F!ycTz4WSObYg#~9UXDcJ$w7Zhs-a}38%I3TMcPy=mty0<4 zqo$X1TU^}0hqE-RBwmOHBW0wI_LpyWsm9G$v;DOZg;g^S4PDmSQ0=A!>)>5Je}?nIOWyQ!bm-N`C%gl11{e_kMeJLzAOirHk$=9F>Ws2Is~d{~I|{m85!Xyhb?q_7UJIU2?Q(e7i~5OLXb^A=6j zl#-qRYWk>nj}|_eTFoCnuOW`EuN0FMq~ndcd$l@pXL{j+u8cfdGZN0kgGZ%KuE~QK zFykhW^skU>B?d3s9JM0*zWIh>R9UQ$UqTiM;hxgtC00vGZJ5K7uPiMs_i40PsZt z@-@|?{cl>B0zb1T=*;d|`p&YsyA3H68BOk(a2SvBAVjvC08%#uod{YRo4?^gO-hRA4$_Y5af}) z$V((5kdBy^A#3wWAa*#YmBJ+Ecj*4LZ#M>q00Dc2DPBe@zza{u(b)vAK!eBQtW>_^)W8QXGxFJh!SCgQ z0CP`xe__N-|0iYN6Ckf(`Wt{?;PUk>u8NY=S(^UPoHU6!dSDuA@AAr;Y2Q?su+Wo# z=Or{a*W7K8+g&610l>8udw3VuVpsK3afO&0PUVLNa`^@C!@=HNSy+>AwcaJd>9dDR z)uGnJ&B@Wt+P#qTA$kz2&tZrpw0PVn%4I<&5Mid{O!)cfG5hlRtaa5&xBU&!b$wfw zVIg_>{UFr~EtIOZetFIc<5t0E;r``jN>WqmU@B3;VeJ-6?}gs*y6Q^5#9zV7Q|p03 z4)(F~Zrps)$S4B-caEFtT&JJrSzmyWKOC?Bw_njtbH^-4axrOVOXm--i)T&qG|r53 z_c4P$b2@ZW-x}b~s(PItpU10S8wsOPU!gn8Hgi7SuV;Ea=atPH2uT6wi+vVVTMK91 zb```J$XZKEF2zSRf=o#T_$66cJqJr&#RdoWXH$8#`DZ`(y}b3h^meMnR-1n*7)-=_ z`}i1k>0O?*MzBp+GGII9Uau8T&vo8Bx#)acr@`R0@5~ynj#IjyH(!1ceo+7S;JD(& z-JQ@iaPjDcB=OO}{6^vG?&jeQa3GW%>6m!!&)WshvvhvyoeixyW~5A_mO@2EgcpEq z*u1i5e2vs)n{+AoCHcfAH5FsX}b=d8lUv@r%sp|mj8(~ zm&hl|5526_cb&Yflt-bm&`|gSc_-DqUjMEs6nmkCu_qM-G5d4PW364iymq|$@Gctx z*n0r8e#GVu)=t|F5 zpPlEf<~d?xU?anWctN(#?cBJ>>@6Jq9opF^sqaz@R5TdJ0Z&gBy4k$rcyLQRGv%ve zv-u_b<9>>-v1>dve%{hA&)aa?>%}Ma#rYjsqj35zpmj-W`Pb@zf9|HM(yF5` z8HU5oHuv3JsE&=@cM|nu3ZZ$PS3MoH`_KKMQ_$;&yQj7{0GC6VL+d0(VFj z-8wBnZO$KWE983;^f~9tns&G?N(I+!!u|R_2etT7S7ijLj~>g<43;oWayoa-P&akz zdB2FPy(S#IOTwpWw9$z%Jh+*g_R%ZqRAu9%U44Y>aF6mves~e=Qpgk|`wrt|JG(q# zjZD0F8#$k7I*4vrF;gDPD?80TS~+zDRlHJv=nxe-lwY6hqUaD=RMkR(C{JN23N-Eu zkv%@QAsp$@=QBH%f+=7T>*icrBF}PWFc~2!IiO z&Rv{bz5xg?O^1RkoIqoVxH6^@Q(=oyasMpTWf1J5Yh;Elh&8I*Et&$ek3gk`i3v-R zduz{aN69sLMc2Vca>l5A;{_}K%j!m6C~>cA+)4nCP(o|n_w^n-xV(cHZ``H>d=-R;e@Pru+< zTBSOtuUOD`SeNgn|NO+tL9%4>+0SK^5_rLSaG%3Ep$w=IIYr?^#JNIl=2}%2$91`T zy@ENvx{et=G?%1;MIVr*BS@wuY>Dts^SHl-ci5kTb~cQH=yda4W5;6!X78D~z#z?S zsV{a89_Al_zW@rgeoU`Q?txO9R-dU|64`?UtzcJKjfXcNpn5>pzx|BhH(PCn3J85z zIH?kEid0O@mlz%uFp-CGipuwSP}15r4U95pwk`GeMcIQ{xhS?)A|j7 zK(JAv={Vx_x)#v?*NV-dt+8&~(*NOSntL6c&M#&Z7p%riDC3TOZJT|)&ZLdzBoV8O zR@$Eqg*(XXuC0yo%RjMDPizylW%-*l)M+SSXBqFuv=3lWtdtBB)tZe_C|- zE?u|G<_6*nnT!EuIP;zk{}B6}8S+lks?1JGSTp@TF>1GgQxX61;YMrB)HH|b=IXGf zu-i2rWmjHklzzE3BAg8fl^Dp35MV@r$|?fAcc?_>r>tJ}8^daMJ}DYN2x55oPLc|3 zM_GFnxa|6og(CI8I%fK6Th~=FK)-B$Vvukvq8@YRR9lb<3!%c9BG^u$sc}cah)%N6 zm#HQ*Nq&OqfUVpz%QLNrBDn9rvUs^SkXpGm#ydzwaFA|ue`0(!p~FjRfByL^j>;aZ zEw}$8_+NwUa?tT+FB=ot8-TO7mRW&sv}(f2E>9!{7gW*Xds+BWXx}eRt25jCE%rP1 zXi=_I;*$;jfB{f6g;5Rb9xJ2yj>y=0Sg!l$V%~^J^OCV-2=^K~^lR9(x+Ts=U)ENF zUf@9d6kS1sA~sq;a2?LyS6A_&%V1k<(w`@=9Oqi9{vAi}bNAFO=$LhGG}>i-eV}fzhyMM@Azp?lS@!#YC-mm#0G^_9%By z`W`drFU(vV4YvbW%EVGdeMeFhjy7x9U+>nQ7}qUQKcy4Qu|<6r!LwGADf)=zlg$yHKq<;$b%bLk}ALbhFJv#Kf}pFVHPohi=5my1ME8k?m#pzCz7;B1OrdEi z=XYd5g}?7Q5+-d;2fS2yB5S?t8G_kJ1e;Zyh5?9{&5)LDF?P${Ahgz{5g$nNu6wwF zXXenHFnFO}CIRU{X=7`4B$T~)I5vc=W5jK17l^ZTMf?L@qEtMPg<}|H%Kh#_#d^s+ zjW$%>Gy4Sf>Lk*Se0eG@41T%p!|XmLzt<`FQF#v7zm*thN~^`2nc$Ja%_ld%?p5^Y zA4g>xoARmeP&6cUJ11_JuYrhy@x!z#E}YP1gyO<8(h3f#JEYkAV`vq1J7 z41JC>!2WPW)`58|c(wGvsPUVPZznEnT#sIeTl^v3=yPw&`}9vFEWCv%2X0cLD!&(# z$oR;?c!>aaI4&R?*A2qi?CZ<^qc;U>XK3l`x1KkEnL(8Aq!|Z&$=y^Z4eQ*QP{=rR zg~KBijq(xRi|0`O#icsa>GHj1M(dCG?p8=|ODM$Sz}Tv~v(5RWcHUQ^Qf??5JC>^> ziR|{FbEa;P=G! zO0l;hR*gMTuKI_`wk}EBjkebp?Cj5H8t~j8A^jyT_7&OCHmA5@9ee%YU~2xFtV3v| z%e~ZlKl_<=*<5{7is2iUwH1=}yX>q5&EOaXZToms`lhEB36(Q@^|^5*0* zqY&)nv=hF@2wFWLUc3$_7yDtj$z)B_l>09Q&pl~MkoFo#NyBt&0Sz@ydq*|?JQux} zg2(maFN@2^XMs48<|0{B=z!Ij5bF9-sOI~u$3rm@nWOipqAHwR@vI-dr_k9M574#j zoLfGp4Ad(uvRO`=Fus#C7mKECzE}t?xulW{4{BfhH(bS*Smj$6n3xzT;97j4v++dO z$3kFPH|CMRN7<-lL(r}$6+rr}^_#1_!9ed<2W86y!V$CP%Xn=!av7OR=h%1&%TDUA zXBo|VJURn>=~3ICtqG=WMo;f#O_c458>Z`)8auwy9i2uG8>ARvxf&NiateZ)@~142 z7RDKLww6yPYmWLB=Pa6$gZ?z>8l+zSI~sJ_5x1KE+ePqM(=l0@N(ZQjqUr8ze#U0M zMs=Ur)gQfGn$U{~wWu3V@wIA7_oxP9q(R7)MaZagS(v#SnTks&AJJMe?r}R^eFm7* zVyJX0aZih^vc#HdDN~Qa1o;wXKk){BiHLM!AD*`t29ui2X(~lSR3H*2@ou_mssr<| zYDUV-Au*d!t-{kus$_~d8u=C|jMRSJ{20FlQKj-wm$D_4@$(|KwB2ke+>2qEL(e8k zFmIH$Uu~1Clt!`J6+5}^;#e^%-OUm&C~W{@au?*vmP>Y)*^aR^i<2ug>e|WLFBmtJ zDrLLVIFJ2BceDNi-?39Fr`54x!yG1lUD!}L4rJ1VkaBj#RWNJS0hQB}a7+xtQg1px zh|sO30MyU-MU=HXJw-sR1yn#lxub;2y_V2m%~yH3hLt2LoaCj6&B0W;!qSK%rr475 zC6+DtmH zNJ>B$V5Z+@Q)j0te$TJzcuudf|~9y>ebQ->#F*~MPd zir>uuMvGM9X&lSV&60wsB+O}=MSzfRjgNDm6dKv=TeaB+2}zPA1mqjqGR#n-P%5$T zQ9vHujucGbR^CJ*KV-?-lEC!1Bs2SO|b1qBp3I7iA2sza@u z!TEA#RyD~}lWsYRLH@WSnZ|k*frQ*DxQbZag#R2RXpE53ES$|hap4*jCx?|tYJSH= zEUAR^E{ukHf<0ocF`Nm2 zxO!m&Av==+gD8U1ZWxL&3K^(&hw@oCd^Y(r-`Io>&d8`2K|^iu-7f|QQokLsKJaxR z6a4R_Ka4V;x7z4LABl_wWYn5GViODCmP^4vu;dsRVNd%;p`K`jl@0E_q7}x&c!4EM zao#YYwAQy%h$L)LK*-a`U`F)mr|J2s77V+Vbm9e0j^H{|Os|sUUN6jTEMpd{N!C(& zujyU=g|iUfb0R+Z_r!q`l6gug_@J@Uba(Q$G60hC-z1jnM?q)Go@uf4qfU zCgq2;+t}%vc{#CFmIS3~=>F<(QzRX_;A;%#9Eg|f?_vL0rz;G>D;oA!3n;{pwRNb` zXo9lY^~eIOqiOm#8uBu}bFT+N3=}9pp zYw>5YrPyS03xm;G33>R$2AICz}BdCyp+|2KoY7OgO^P4eOBcOUf)42x(|hIMd?`Ul`i zQJqZJ)DM(DE3(C2$WDtfD~;NIax+Z%5O#7wM8-lu|5=dCrYUKo%JlTx%go+3`KK=m zA;p?iu5=)}gJi==GU(!^%yJi$9jXP+gpdp4{#>}&Dfyx$5`D}RfgVUO6~&`&yfGKn zA~bLjVBcxMex^lW%WB@LHO!CNZ6O60l!WEQ&@7oS3UNy*~2i z*VozK6{OcNp30r0id1 zo|viEBMnJ%UY>J{u@9Yar(@X%p*R*S?LHgGD4W{clLlK-wH4SPbnc`E11KzmddZ7b zHtuuNHLL)-r1BJPvwL`AG3lCn?sMU0_WO;IdRI{1p5->Cq+6Xv&jsoveHUsUzN7R| zm8}l<2&kp%?_X>2{VN1ByQWm-9gK_*#+}^!-uM691)~4>=n)yXNT%$H^!(tn2(hWl zc`$c&T9m1$Z_NtIMY$}h(X}nGeEhiF_KkKU1lcXuu35L9KlcLuBxzi0`V8uk`Ma<< zIV1zJYRzZv`hel73H~sv{hd*L$aRl!qh@OZCLtF4GG6HVutH84(#PU@ema(P-$}Pm zVHr%{e;kK=TYa1v*Fvn_UV-&&Ldi`!Ekl#`KEkYJ+qp%$PK54?;E-#%jDG8>KO1ye zBxc)9Yk~{9pGh%qUaV~lB^bi>rv1{bN-7x#WQ&JPsQA`E5yEK-f{5PtAcx5A^@7qV z2T$Bo8%GH6jb!4M-0i8huZjk>ghjCxi*~EniLG|3WX7IqmCZ0(!PnNx(fps*mm{~* z1Yy_^KQGlCrUbIPGn`}RzB@*l5tRt=vL-J{aq#y#}WloaXT}$Kygm16|C0s4yN5dXXW=dSU%f~0$%gkJ@GoZNO za3~IuwD}>$5OtuQ`X3LeJpL&*C=5hXF_O!MnmA-JlHd!&O8{jFDdEj5d0=C%gO01} zevk2P0{1f!4n4SN%U_y2NfF3ufpE-df@>+v4w`|Y#;-bNnWe`)YZtLT>{;r!rFWZ@ zwUn^P>e5oT5=aVHeJwjyhp69sCFB{`OEjgmcepcY6J;<*Wq) zwJo`hTy^+Uns#(mp=Y6&SVN`OThKyKfP}D+cD|K$ z>aAe;70Zeho83*q<7vqxV-3&Cx)@sOb2+)HkCC+6*< z%Y%=qQ|`XDo<3u;I-D*jm6VjJYU)yfJ^R(jczMR1XC}F3cXkxG`1A>L(%O;%3w5_k zOIl8}vIuZ>Qd*Ay0jgAIw7EFgnodgQ155X+Z^KbfoUQ@1)#sfOsPwFrhm2IgXWi2#@Z{Ij(hGiRPfoo5053{+q`j z?5<0@sneQpFj8^S88j|v4UTn81 zQ6)IagDFbd2uMkeqPH36qrSA_=#-qd}_J<5<%quEh3%=M5GW>;ix;dzi{2Vp<;JTVxO~eHA~SkcFuPr9=AR-&I3+IW9|s$cR!3!?pyH zuU~|>C%D$Y-u(u>K4!Cvo^e#q5Ij4`cor*^F4bd;cW%vYnQ>AiCgRwyXA zXWiUFe64gTzWeDlhrTMpE;tPCRL1U#M5nBfU$>Xidr57O_)DFRiC0ndI`r+m*UEoq zo}5YPrwH_c{7;DC=Hr+?Efj->M~9ON><0wYT9L7 z)cX9j$B?-TQ3z5L!-W71{-bJA^{_Rh{{7VJ%D9F55wfQyT;XzdNO^B0b-fk2K}4;m z78b6xHcphDv856UeRW>EKHsoRnbkay4m zM#i=4SvY%Ng21+S#hH$`I!3!IoaYvt?hYC>wx|G2068n-T*)3mabudo1VyBVh7cl! zuB@b%65>J2WSc6|Z4OCxQj;1iw^Cwuso6ZJauz1|MuTpuTn z_1^1pi!y5?p%!(#S_}9t!85Ed|Ii3RcG{$6Wm;|Fn*$)D;l6E9z+Rsi?We>!* z7D62M-fs3=Zkn`_lvjw|P29uAITs(W{zGKFF*d+dQ}$iXawE%1eM+G``p{&hzeO|W zEkG#%Be~s?WETsS;!5Vc+JFP4u+ka{OFJ4!OFs!Dgl$?RkVz!&ucyRjQ;FrgoseU3 zZz_vo#H{v+wy0>9(~(yp^-cGwNm|1E_gwdOLHN&PgTD1;85h}%7g24LnMn#?SC+jH zG)Bk@DH`wCCtjaU-2NLR`1}?YOlx~5O4iEQg_p6C2D&jRpbXhabeTa7amvExfaMNr zTQF>G?BjJ8+rx<@_0v(X9(`>}Htwc{&T(&IJVmnFN%Ga^zLhn!sC1C6DJOg1aZ{!r zb!{nbBH3|-b)>7Q(o>>$+;yFep8FLqLx!hp>^4=?_N3Li6tS`yj#2<8;UpAu@F6K| z2GWHjDEF+XWmjGt!t%Q&9m-N+zsc8ZQZ?M;Q6+iEwcUQmOteJG)|HncE<}Wa)E=mB zs65gVttfBh{+N0z#Boq?s5o=yk~yb5LGHxj-84}bIqr2(WXzPwiFG%$d@N%aBaSm* z>hZ1?%zhpSusXQChI!^Avtc^Xsx}UTdH{eeThAF``7Tv)x5LG3cRS>$a@NzH;T`9g zX_fhEWlzo5xJN=&=KN2--$>b6)~hi76L~GXLopn0boRx24I~Ad(ar+L z;$4Mg&z}|E*nWdz%7v)Le#fhBs9=c+E-dqMzS$s&$qVa@JeeKN>V(slA=d#pV@vEZ$({+^izsU9ui03aJ*ZF@C-!3nC3x@#bf2-n6ZT{Iir&u zBw@fg?~k+1BN-QY+W3qfBL!cGF~OPEVlKHgBQXsY(lWwjg(f-A>_Jx)Wa{fm}~;{`aE7Y4?E&IHw!qOj+Do41PO>Q$N>!{!m5_O5)j@g!)Anj?fASob6S6gc&sx? zItpu!pALazmXxJ^L492-qdTafy$$?le!iOif2DV;sVsn$1M^YWuhZ`T0Mb_f01MY2 zhUedQpZgxEAO(aS2A?pY=t=(oWAp9X=l&0scF>LMr6g~+&b}MjI^WO$K>5^7k)zU6 z`*qv#>^1Y(^L)j7ngd-wQMdH>cr7!NO;5JEj}gw7!z`$gr0?+^iTa)Fe7!5amRHz$ zdp)<(NZ;e42kay6ucnki*JJng@8~{5=k@v2L7}h+Q5qZ9uc7qU*ZFH^D1-I{Y(}Gd zA3qIs<+8x;%xHe2;(S-bF_*tW3EyISkLUZUB-BR~b`TGy_D09;?fx-7z zEp+z6qrrc$2=D3G0ycepbPw#_zI7CC+VB9W2fuUseq&ym`48~aMVA}|bfqMGv^0Oq zV1L(9BuIFtT`AH**wFR$9lycbWA|5L7&LCVNzk^n;5h5p4#pEJ$GFQ@Y$LBut0X?)=+fyn|ABuENr!0aBVi{ z$^)nX;L*Ka&OG<Y4PrUs8UA=3%h%e{>7oqbi*Yp1Xm(HdZUu__K z1ng{qtq$GvN>2Sgzby{c$|P?^E$Jkk5mkAd(%e(sbTYA|xxFr^X?`4(2tj>aDJl3z&u7=yzpnN7{cEtBWBo*cNc-OMPfoBnv0s*~sHMd_6ij~!GcoLO1 zR8%$Fu-W~iUHf();jT4Xq-<@z-k%na?gW0{yVja{XVpG6J*asGx=U9gJf&2qkRvvU6eLF7ykge(4CFx>g!;8 zLY2PGaYac8(F3}*uKl&W5Ayl%*1E31PyiJ79-j|w{Ex_Q=jsmqM{1efl$8QfJB0+2 z2dMfVU&pcgw_Rf(fQtJzUDUpluz$z@0AKC>^$&5M;WPm~wW2l;uW#A=f10Ot zA)(Zx)zYryYqrm(k56&?M`K+R8akD-JNGAjx4*wb(_V*lq-lgx3tD|(b~m{u@5_6k z$yzoLb$d;NTlj6=@UJx_JjbXbr?}R%HbFkR1NlCNp{~bJ1QeYgnDyVMuDg%fQ@ilm zUdnZ_PtSgv{C->i0I$-Zk4FhA0PoZR+xGteTk;==-`cP~5G*yCJ4*lo^SbIl>)=jK z@bDY@-zwkUr03(PB$W+=`k(>a{rHEDF#HId|pZ)5~R*lR2box8L8x zO$?&@1Z!Y=@4tUv^8Wy@)}aqaCwfk{cdt(0&)es|+o{3UI+S|Gx9Th$R)m6YeQW!4^1Y}jeQkF;ACJ?2e#h7FsC$j+M121MpXvTD z+-g4JI(MT5mXg!67jUS&QuZX%nD`l>#JhwHr z-3o!J2WRE=2kTw?Skun@kCr$46hFvU%Z=#w9ll?;`G3pnT-NcY-2VXRXMcW_of_rw zKz{{^H>N?Rzn#C+rpsm`3))9j7^Nv*A#WvZk@-Cs<2Mcn=ewg!Om5@)PgT8%@yEP2 zNWmKZ)ZVs=_7LQ}n0O8 z$gVP_u`0~$977fl8$4KY;yH4X+*X8%aXOvcZjus>l^n48L)TV0Y_$^3tGU@ojgwmz#tKl>1QcEWtCSDTmVfoNW#?US)4QmXe()3!cF9ei89M z!LxBmH>kGBc3WFZyV)c=8pm|RuU3bn%DC<`s);$5lI&%~i1oR(yz>scO3HePx16Vt zEx1L;HESWrEtiX{d1cZ&>bs&t7ij|D8UwJyJrfy+1usaSEtj2o6OOcoSSojG1kAi+ z;cZV^?EIpOg`15z3hi>8mBLFm2-4Sbv)!HZUB~k)T*W;QM!ZCPs@Wk?V@ZcKi0m!* z!^>^WwYIBl)~NTk?a=4lk?yIvyB=M! zeq8tNaD~AErzS^I)|XPiSq$_yTX?^aaYzcyxZ?J4MN8xB#4Or9pC(f{ozbGX@vn(? zT-om?H0YLTZgs+rxP~0aF&K6uEw=MzJ#6yTh@pHH2lk-~^3x-06 zlE}lHW(=n;m&fibw`{Sg!v;Od;u&r~x*oSuRO?CeHVQ>{@a4gGtZwx?mouzc+(PR8 zrMqS?k4v^)@+9KcFhj4Kh04ebUCnxPGIu8zu?FeWfcB^K26P@F`+oA`>zTEF0v z?szSd@Q19{Tu`{)MSII&*{x2p<$mP#4BkFk-@SY8%)a6h$gO$QaD~#2GVf zwk7^)$eATMG^y~}xZGWim(c31(fs7y*k~urX$>YjPKP>DP!rWtuN&l%ZkbKXD!kn$ z-Wi-Kd6JweR)&W{c;f5pn4EW<$FVz1 zJ9pZ5#K>GJB{5dtyfYbhH3am&!r0`jY8kaL5CV(r||Tzq_*Fk-1cnuD_pn|kI{Etl0FJsBb^@)lQmyou?(coO zbRV4&QvM{2q+xMeT2i8klG0Ac%}t-L-ROURdZ}JO{{UnE0L0Xz#f+sG!9oVoi-eKs zzRK3VUsV49A*zezAHV+qfj{G-C_j*%{{RZV%jw(ss>OUzc$k;M*UKfMJ(6wO1jzto zCIzDBbCYPdFS2gIxN%=pmfK%aR~jgKO1HGKpf)lDwiHqP8k-7I2pwA*UXG7r@;|fg zs|oQXiRMvqyIx&tk&imsAgcW1I&bjX<+qeped?a*9{I!e=XlH)$+!!mr7wY1V%aYt|vTbc7L_O*boj91jOUZ?Qt@f6Vy5S1jrOD~)JP9n8oFECDnqP@s+gu%aZ(v_LoPOe zgCW#{(uhz|7CrDwzlY8%!j~OZG-T2;i!m~sx%uSS?-1W{G8Z?>R@^VP^ku`0$aMBl%*FX`Zj+M633u*I%D%t4(kx<979A@W_ zKN|l4X}LR+#J7kx%ZpP~%SK8bQqeARNs`*+p@xb>4LH-Q3T*(OcGRVIKI^7jTBJVf zEbaD?^H2M-)2A4a+sSDzXo9q;1$J2-UA0rTNs8Qr_snl}eSoGNT#qQ0JlSY%Y4TlZ zXbVEt=xwBhDC?vWPp@|6Zbn5C)f&Yev44()H6S4>eGlnMNJ@58q!f@!km8(C>K=em z7tSAs-Y#~lmRV%AJ$A6zT}*k8yUSj>LytRpK*9F~YhKw_0hijW}FMNb@=gC+D%- zxnPGa_Mll{+on!_va@NAafdC-8bT41Ex6(E zC&Z8)5>(S6!iC355S8oO)NI1($Z6K5SIFV z;Dslthe;?=(MVEzNf*nN?k~AUW@FE=+!F;Y#!*8njHl!{8(JIbF0ZZU6qGs&lBINj z28C2B&CFe{6Ea4Y%)4x-jE{9Gb(i0m)4^y;(EE-)r%QEex(qpU?L~a^DOJyiRT~gQ8)LrUWDbS)SIDoir02=FC=}KIV@hNbD zvQ+9-qR~fRWgQY#t&viGdj>R7Vv!ZKA*qpP5LV)p;*`Yq8dzEih-jsKKqLh1B`PDh zHD2V}S#Zz0-I#D(Cb(9xl!ja_yQ1nDXr!SnvRZ6~r5*g;0xFVn(TqPf8-&7`h?rB{ zXe^SYsrd}Jo<0rwAeVOss_C%@O}s;f0ga)0}IJO2P>zwY?XUMWgf9c!g2N+MNq@bFznBMA%!{pt=cmt3K#N0mQcQ)Zcyer8@7btHVo3 zaRH=LiJ}8egqzZUvQ^@gK_e$7gd|E&K_7nf*GgBD&Ym}Nen_<5c=j|_r(?G(WP2(d zd@T>KA%y|%$Vqr9w%v7PA!Q`>>?iET)qwmlVMYZMg?v z1fgk4l-No5(mo@jx>*6crKay5=B7)IM0uCg6hnx430jMV#3+=w6cyYlBpqymK&t1Q zK(|YcXujH!@c1aLpOf4rnBWqf6GTcvDnG7i<+%0HY32XvmwT9aN?n`8ji$9)Z0xo&>bl!sDtrr??STpdhpeK z9^PBFs!L5xze_@v%mzswV%yjr@bN~pp0oj=O1sp#;iH2m^Oqu8u=tXhwn2Qh zyQKG25{BUdRHhI>SNdW`i%leeN|b@L`i+3`&M=L|7KpfIt`cM0;Ii0;>qtv$anvLw z^*p5Ll{`?{Dp69D00ig?_($z)_K}bDJB6GVqnKu6IJPO>ljfd7*zm}o8FO(n1~|(; zB!T<5i^$61H1Ern7kOyJ+(JSme1{Il4BC)h;YYnkau0g6H-25bUlPSQy8?xdSet36 zcFSrb$a!c&UXZp~30v+F+$l;OMFm7BXG$P$u}hrt5abSggO9YzXIn9AlxfL_B_xZI zAUKDmh`S{#S{rTBln^?CI(7-&Uk`|mQG>_)e|NiGCZ-DAZBW@#_EzM#Qm%%ZZ2?N^ zQc$ouQ_wU)*QH=4Zz*J#4Du#jXnIAmMZR-y!&6Hwy5ns+l}PGwrFQqEhkDZLhr;1W zA1d;H_;B<~6~rgwo|$nF>1LO;!$FCkLhMBKCMs6g%I9j<+rov^0R;CSSw}i9^TEl$?AOSld79HEcr3b!c@28AN$=L-B%SZS#jjF6wfb9z@czz6&AiQ#?TOk3kOM(; zfx9+bM~=Mp^?sasZrEdQ2f_Auj@~F;j4`+d#_ZY4dqx{zEy`UPtq5t>!mNBb$I$U> zl5(lp1O*mjNryJe2r1WQMt!Fcw%%KTq_js~)B+s?Ub^p5jJG4lvFH~lIAyjqk-MAD zKPlD}7a`{t*p<1^l6Bjp05$UTs$2G}@!w4AXmKsb32sv@l26C*?IAl!2Vf`?I}`QQ zaNgl6CAI6t;Yo!FJ9Po%gt+2IeKye^V$aBwq@@Jn4=HvBU|l4%*GuemGZs)os#@0STXik6wV-VJZ!a7Z!-Q<`!JX`UZ!8Y%!HI_Q zmjj@A9hqq&!;gaX{(|5;e0!Q)433Mkk2q`Ho?_9O-fG%A!pR_k%}cSe3;sojVOYG| zyLOunJ#dCmy)$J zuZR^871QcLC)AGhdR`~&oO6y~c!i?{xx~KAx=d1YHz33iAGghBYLV_RN?KrueJDeT zd9sk8>F=;=$+8$AjCuFETyJY-n52gsP;9hPpbqq*)ukxtpRb{;xNJl?xgf?x9I&|=^R;LsTI2y2=iPx679ymTF9%%j0C zHs$%QbI6YzayQ$LUGo;~(q+D6F$uTBO|tW?#8@uAQli>Yo{|!vNcE}b8{#f@;+Pl9 z6_XQb=@k z#|y=+c6-yla{416yEW;OnR8nxS5hv z>6UTtPQ;GDU%WV_2y6XTh&tEIZ}a~E8vUmFSH*a2d;^N`o(se{TrlwQ4j;0bCd%j! z3;zHQiO-7`9{E2Higqpr$l_%fV~EJ%6T;Ovi1B!KLh|^@ad{ECol%!G*syZ4Kn;Ug zoG(%qFFCiy+KvtY?sKw+X6#?>Yn09C6ikmlpb@t!q+=8z|}lBYz`TALH`^#;?{`{KJf%`NWv3WyC4F!flXO_H%xF4kasX2yQ1@5wKJ`dw1_?oJDSfisE^D_Sudu(*3SL zj`QvY-zsem6V$+2aVbewk^)FN-F}z-n{XdR@ISN3c(t6u9`_mLc_v4?nAUW#coF{qr+g*yFd5!l>$VTWNgqAC?f_|L zdeYV5a5MP#97oGDr-KtNk*$W+V208<%M{Ks!iV6Wl^$qBEsVgSVEZB2p z7iXj!&HXP0&SiE`C&;Ub!-jQD7gKPlU!xp*D6KH#YkF|UwPh!)~642GEYKwq6q0o z&^zCydYlIfe+|UVKNEf@5cau^#=bb6D0I04qlMCTvBwx0pDcmpz`jDvngX=(Shn!4 z5XQ>MOq0m=i6dp3aT%QFp6g4!T~5BuFH*yau2wHIAHB_qe#CE0xjxTyY4+7B1I~h6 zL+rTVDEPuj($4k>6+n1@A(g}g~3fedTETfH@foHvF&KhZe@o!HOy%P z)uLCFe-<%zXA1-5oumk#h%J(w;wW})EoqIXyhFp>1<|@$S+jU>$9zO$7VP@=!}y(v zP}e?HwT(!XFUi?y0ZL4CB`>%U@q(Zdq5?=FvGK{+KOS(mh&*ad{hPJ3kIQgho~u&{ za^Tc9-YvSMG#+gam7%iY`kilONAazJZMb}x9Di)U@m3p)e0CfdQS5UjM~L%mG|Nen zT8}a2pHpZkS5nz*f(KT}*{5?An@09D+IAt5DXwBEs4=t%mN5CHZ0o%iqi>ft;?aZSU+Pc&t`SMTO_B{pNP zJ_`_}m zl0Ps5`9L6LuOKeD9rsZMY9r8Kx6+TZZOk_672FpeDwLuJrutvk;3_>Pb~@Fy>67Lb zTxGK7mX`J$0B9kyR6aeAm;=AAy(F8A)_&EWA#Wa+A9Mu#Ne#5nEg?%*f_hSfCqxoJ z9jch+`5GrOVp{(IgfY*U>cI_%!0plw((3;JS70b;lcT?%N--qZh%oWS=|e|-?E@oP z>vcTstBIk1%1s>>yzetllp2Yy%f9(?ix;sMuk|Z0-p-tB0pz^0UP*M4>?pi;!f!7* zaDg4NnW|b^h8uj$Uv#|kOKK?UDoRH{*&?v`rSQ()ZIxVksy0q7*Jk9IX8S3`2+|vj zL1HcCDtV>GLZh~Z8ZOy4pDM-3}vngC^+Ryxmn8A;M)e z%%JSWttm@MXd%=R2Ud}?I;R06%tg{LIxu2z4QXUx(H32$kkBX_y1gxXEC&yYOaoz> z0^HzAWNX|U=LZ2}O{3J85mjJasQdTadh3-*v*HjHm1GKN+pgCDN^v`Bbd)&vzH~Vv zYe-No^cbzy>is1It#fzTbTgh{k$wa7Fyd{URm{Ci}T9V;Xn{Y~K1f{?j zSOkz1PV2G8FJjoj?a=WHbVXaT%Qv{LZ<4o|4y7tgg)<>dsUB)n=H3NiN!oi5YSk?~ zOU!p{?N?hNOEH$YHw#tW`F&92MMJ3`?5>5a##Xer0Yj~}3Gdvb8rG6bBU?Xfi5xm6 z$6{wZMYBp9h%3(WF8vrOk24K3{e8K@U_d7RKM zYapuG2E~=*e4`(mTK@pCn0y{lgN!?BY!xwYw@+Je6EU<7fFVEUr3xzO2}`>>^sB}3 zQNS$n4kL)<&bXA_Tgk1~D?FIW1*!0_ZnFOXlI!U`NDceRG0&Ew7PXCN4b5I3xO*ez zzA5BRIJ`i%jLvj-?nXS^E@bL{0c3OP)4b){}C^(mb!DSr#&I9zuU z^v{R-MA6}0+b;DI zIv6rK(sZPhsj`i2V=JB@GLH=wd#s6XTAbpHy~twSYmWj&#fYwl0YY1wjaq!zWw-kW zACnX)FQ;eXHHi-rR(q|F!|x+vYPtzn?XK~UX7;!ZxBixgBP7dgZ9cfFQWE2B$Tx>w zak3B#izP|w^!Lo-FmVaEL~nEO_^3Fn6TSlTAg4&yFg8|z9LI+O;8?)d2Mo1Nc9#x| z3);eP%_2ri11An}c`g${AbkxRVUN&Q1YVZ06-rXRhP|p+R#DOteu^jg5BRIKU5K`s z*N4#-13~9T+$k@S!6CehZuLE#kL=e^G~wjO1eQpn_7tnzss=uyVtykHcD~2HCi2f4x8)G z$92nja2nV=ugn9#-#%;gliC->A@l7f_wJL_kEKKeWF`zjK_(D;5ZPKtNv zH{a>>{{SD|5@?jD4FD9KkWZo6><8@m_pX#pjGUtM8fjZmv8Ud)w4U-#gppPE_t&XW zrc}|6pegDYhVrQ?>+l^_tEe78idDaxRoDLl$QuWNmoeFB&Vx- z8yh~JhPzCxC1EOezR~cZdMZiL@3IGd==bb)HM;4-#@j=z-2edBT{rgY?e^%+DcQAF z1)Z+Jz4=%48mWRfKqF(f*Qc@d?XCR2e?I-bbsAwQKN5D6ubM#Zzg=&C`bve$_awZ} z+3R>mC`QbZgcZ5~E))bUU%~Yrl&Jpzu|F|f;nnE{DRCW1O1e~+4_@`Yj@lr0(ckT^ zoKdxaQE#rf$MYcD z9)Ce=T9{iZ4Yt@(StHE~>Q6~3NJ%}8zLm7u8|yKrM&@+_NNs_78WcviMyT^zZZ9Ai zDWH*A%US_7UFQ+hbKk{x!9!<6r1fkh1N-;e)6;tI-?ew!X-hy}x=;?fx`71xp5D6k z^(W>k63w#w>XciwTwY9Ol_kK4PN~5iJzJ1KMv}^UR11sg9}2tr8s%^82P3+qGa*d{ z1cfD+OKq>zp=AjvO1@enXV3yc)@I7ajIK5fjM=-okaC-M-!%>-I1uKw$1bFczUx5& zq}YDVaM8pN2|B2-Z6yAvR=aQYbUt0X{YRKg7l8u=G@kiC|W;2J9G_(0ej>d@Z)f+4kEAlfdJ{gm(N`}DDp>eN`V99M|1Vj z`ki;*V_!v-_fmFtLD#7x@zkf1M`OOX_54BK+-u;aYXNFI0FZW1SIhqZ zN&ZHmQf5q!Ru;6C9R&GmLy0|JgcKD4uy>>1zKGa0^uw=78*MrzNj z8`vYc?c7#i_dGT-NsX731Fpb=2-3Ox&y;4frMS~g(IZ-`vA&|g_Ul*OQ%ead&>=^9 z(BIGbKM&cX-nb6I?f^RU^&8f`J9=yRK9$7$huICS#J<{AqLe92xRfn^q#_-;#quZCy2?lc^+DfpumSu%b^H7O03{V9 zgruom0ZCB`NC&MWebS-y@3ZOCuTftT50tjT-)rJPa0I%SJ1=?!q^JdY>~CY}D;7|6 z%r);>HL2G^eigr($_YEPiX8o#c%6Lxi>~)@LXMB9{{Szie#7^AS53k?PpKeiXlM__ zZ};oB-L2Q#ggCXHu&@Ct(FB39>G1)s{{SQS*HpNJq@h|+{!o?TKIcw;IcK((11&MRbRW--p5f}QsF%TB_)3mBS7A~E{{S8T05|xm7`FAJ*S+izK?+X3qg}!O0Kwk0 zb}LEnwXUnzowucHuh-nRSu=?N!kzrh-iQO8bKgVyseV8J=_6qroVn_ zS$O~wb)cej2DCqT-u`t9b)=+hsP%lln*04ezlo`uL~fDWrjW-P~UK%o$sY2orA6S1GdhNiPvLXnILOJb)(BSbbvZ|>+xH0$z&SSpE^HH_p$R* z^R1-pl@Cu&p4uOF&c46RQ0GuK06j-!0n?&=J$w3h?efv6)WR7WT1iky{Z#A{um}MB z`yage`c$<#U+@l=66zjEB?q8r=zPfoM}2|b$J8CyD-zgRNhBK7hL9X~y0Ps&bQ_h? zh3NYei}|G^Yo97>PDx?UEpF*<6@5NqN9Uv4S{u;%)aeZ+4lN*Qw$tUHckU8E-|~L) zb@Q(;kCE@f;_Md3kQU|@m}h4`qvNRytk$uPfHd2Um(c`S8&&Q(?_GCM3R0Eh*4kFk z{>1osllGE8`1qpUpyHFQj*E*j+|xYKF(S5f96DpK^K z6RX8-8SEwHyS@$K)1I*_J+kF?lbCXfA$z1aDxA?Xu^eUylD0}z(~E+HxgkkdJqQU| z(so8zeR1SgSbcHiRYT{FJ;ZYkyQQl27FGUkOC6hp8BLRMvO$Kl`}O*C{{S%|=@)YH zSd7vb_>HZ7Ye?xP>AyYjeiIhPY%_1taO;)Th!=?pjN9_>^R1C(BF@bA)hvifQ@_Ku zrk5qR8*OXYC`wm0vEbRfn&&$Ao7~&R@T)z=>!h{9c0&@|izqgv#=6{~pvjW@UPOhq zzT zd2t+9l;ReZuH$?hWV~0yT#K1;{&|_Do80SbPU4qFr@tD>k>dAgswB>k+MA3ncG(G$ z9q2NOU1=e=5QQc1r4db${6k5XpYt9|mtolR`0Eoa7P~%H%!M&HA&Y~vA}U0AF&p<4 zzT6_yuSa@kl;Y6XY0&3dE}I7NSk66a%X6rf3!TR^no;AYr zA=t|{s0d3lCP;^DdvK#Tvf5Ng7>&F;2Oyuz67M&3-?2M(Lyz0>OJ(wH)h|r7%6j8E z-;gCtq1UIjhV8yfk4oG7n1ZzEsHZ1f0m>NnF7h@#bi{K>7^W@5m^Hf=w8;@4oZKw> z>1>Z5C6{Vua|9oA6K>Y#pITc|f_fvBrA_RLv|;$2*M_ls>hgU0EBP)^Q;_DevBgY_ znOp8Rc@M~2B21=H?{@2KHAZcy4I#53#5|Qg$XM0LS-B5@=i9KAPE*!tEFx^Ij($NA z;eU~Gwl^IXwVQEuO}>fhmnb9|NkYO@uCdZVqx|ci#fyUC*QW25YxZz%^5Hu_BD6tz zb7``=+_7?c^)54oxUa{O(f40^BdIPj(?v)q6mA}UeDQ6OMOn9EHam7A)H@S7eTq|d z#awSpGb{Zpr{<#G^w#1bw>-8agxkAd#_DSi9huL7Z z1(MJqMd`N2kkIOIPm;?hUBRR@wpo2|HkaN?LyD{z_bQZ~RajJS+^$I_1nC}n=mDi$ z8fNH_7($Sc8er%U1f-D~x>R6>kdPc2X@*w1yG!ZR|HrrY(LUTK>ug#GDH&B zr7R1z)Zt@_(QXulyVqP_hp)%kW-QJM+0Cm&=x<}du zYNQR_azc3yj3z7NC~BvhUFB05iO-7?2VqoguMw})xn?DI*_HqZV6UK0?NMX?pgz1M z0l=_z*NU-EQWgjcoiQg5ce#_Dy)B-j#8==~ux~69!`>vLYR9S?@_b`2nRDxOm zQ;&yLEViX433|BWXfejIq8vUP>s=Fe zQO%m99`(GliDWEwNF$2>ZmO~1l;ngen2@O_kKf?);zhRXXD^g23?T*(la5D2 zJRY~BaTnH6O2LmW#`|n0jOE(*ZJRhV2f13E3NdAF7a2oP!&vyX#DuTj1VTD$Y?R#3 z&C|Hd^jtBTSZ5S&^fVxq)J5px@4zgQ;@0*^xUYP0@Z|a|hh8d?kBaR=Q!TpqSxvxNM0$rrf31NbpqT-A~W4LF3oILJqe2QzP=}#vQCc0L}zXds7U);U_%M+Ig|A=LZ zlGr2n@(2tL{DDd8_@;|^HTPxdRo`QLuNuE)Cjr2|SNEgs_H)tI`&Pei>D$4NWarDr zr-~}BdR8tJ4vQ+Ep2(}=+;`${$7=muIfM+#kCEk)=0yFVUko|%03=FHs2DBaG z)7yC99gc$jmM3`M$BYZwE^`ZghD2VT2nhQC$|Ha2bMQu|>~IzcKR4xY5{q5YTXQWT zZ6r3&BqaaRZ+yT$mg}m0>MO;QwmG6AsP6DS>NrkmVK1xG$cUDP<^b1)HJ&I0Sdk}( zX~z3>Nc4j(LN}f!c&^dTi1zq8nwE|B$sCU6AW7r4+8(jFjlCvk5wu*kS+#v>sFE@- z>Ug0$%FevFlJ(nL0F>b%*ab?_kk(v{yz~Z?Ab*$IxBnh_5dY;@dnUy6ZI|?>Z%znU zeV;nBI3+R679mEGP55GFvRi4YLCt2g`81HkjMT3;BT+t3ZpRfY1^6OdJM?4CRW=3g zJMReHY|>}D8A4?J?s{ImFmo8oW4h0G;utqjM{Il5&+KWxEwV#@n$E{*#H%B^4*vl{ z${MA?o+5c!$qBg*%SKs$X>0GwU+i<}$Vz#B6>DjLRpA`?$O+W}WU zCWncBgDv~{s+yBpQ2D1#$eBenA8}=YYdU8De+v1}g*4n;B@AxI!TIfQoY2d>zVA2O zv{9gDd1_n%c=^Z5M$neB(W?*5r~!bWH1RndOl!a}S17Vf+Wl?#E>rFkD+%A~Ei`{+ zE+;LM)h^DW)F88=KKuM}LPd2tNIxRrlV}+X#P@V4Ku(cwL6N#?ITO~Vk^Zv2ZP_~iNX;1T$nyF$`I%$bT&e;%08GsWYepp4d=k1GA ztTxSTc_u3>_Dn3kmfRuaeEat076cL5WRdX@fj)-Y<;dz;R`<9X>mCSE z%60ZqMVrDK_(%6|m{=&(zN#6^h|5bk{s5C%4>Zf7m%{*pT9Me-%41CkB(g1IXbIj= zl$OrDu4G(U9P_oh%Cv82rrwTnN4oS^Z;-_oR>`PBiuc6;uz83<#@f7BWj95>H-B*D zeGW&l^o#Y~0eIh{8>-(FsQHP~%YZ?7_=wOP3|Z7*PzDk%e5D`n zIrVv)Z{O-45DQ%Ke?x)S_=#`UTj&3+mG$vLCZSG7nU9v!#?&8Ik8LuMq}R@6f{)H+ zckT~IjsM;;qdk7~`rSUi+6&pe=dl7@1xD1#Wplt13PS?T@1%Hq-oGbf3qf4)vDdcXyaSvGdtsoHz}?rE}Jf%V|X`yIP;Sg z17C2oUIjSrB?fI z^Z<^B|5pMh$pFXvFwC)T^c#GlCTcYwZOLc~^y3*UkQg6G#LEW0C0$PX$^!iYa_2gZ zXd9xTDyQxe6weP=akBYYk0bF1<;Lo=_oVgEgm&2fyAm6|t#ABj%iaR#ys&9PSvJL6 zja8+z1LA}-SiVn*W%~CNYTZ+LsT4=IZP$zqliO+goK|xz*xoh2W~G*fmkLg1VIKmg z^P~YNCv7~CDyxYv`^-CF;gkdZ%r=rZ^4N9MkL&XYX21(C9xJrNEKzFB+dzGOlz5)Q z+&VtV98Vw1Ws1C`z;ZU!SjD_6ulu3w_ljB46c(OnM6J0rM=j!e0*AG`qC%+OXQy#7 zuUOvrq4y>8pCSu27z{GfSU{a0K4d+8>$4~40bDa1piFBfT!Z>Z#6XM(4nRV+!$lo2 zH@SK4hfF=xE8hpT(#qi+kovrixZDhFkd=6I*VyxZ}^GLW)s?LzmLJV|E}ep8+x zDQ*m2J=(&e2CZc@2Kt$)p{ek6+T}^M(r-AaQ1sfIRaZsKCd%drLn`Fz8LGLo>Eoll zT{E-)+5)2NXFWk9+knP(f;@FgyK|D}Xu9MMLgkJtk9Pr_T8{_(Rj5!+_1tGbno(+yCW{t3>Ojq@cScS1+E0<3t6NvfN zTGee{MZ<3DNDI9q@W$GkB4Q^xSmKAEiq9V5Mafi^zI;_9Sy$>D9!-ycZAGW?tS0qn z&gfV!|M?aKhog6G+;Do!7fQS+gw-WMe5|tN{(0+79=64D zEvqy2MSSrcCQ|d`Aw2+J8af@-wh{)qjhz?(tA#B$@0<(LD|>RwunoMPFqjhcwMp~) zM}iLB*T4<#Jh&gPG|HXsSbGyNPvon~ug?#S)#o!AFFpE<2+H^S#WC5y*LpvVHjrE9 zhg!{RT5_gNcvoWAE&c)g2!^fE?QK0rsBdrwhhL{8hvUX`U`;Y$ah1Qs?En+=zvJeh z&%*Hx70Bx^#V(sI`Fya#IsfxK9@bYSQ^wCrgs%w+`-$g*zQR#F3Hkea{tqqIcFo(a zE&h6!DSrI^cdgau+Fa60#xs;``~i_{+O%Y!zel9AsjIj$l&vt`EK-jB_RE%HBGhRl zDSpf$W(|4_zza7fqH+zAIfZvWNM5ocQ2^FUhK@wV?SFFh{yyykF+F4UG@4WVKDBh3 z9?;MlF9*rwhz~@@(ewxS$vB~A3-^9I@DorUGJ}Z)Ka+I!naYbzWDc)|IpxyQpI&pD zBrl)m$yj1LKa}o>_eQeZNXI0S$e&8pZg?DJHP^mu@+l*AhGD zG_4dBRJZ_#Q1o=GIlDL9 zPow?N3Kb$n{k(DNUT&Vj{WkxTwF%qAtEf;z41dJ^T2OyG5zs4GC4Ssa%g>7#u>&<7 zHS&2{rOrf@MoQ}K8|2-9_2r@(&4PEwiLSC%p8fs_+hSU*PI*Q<6PN3uf3nEVYK;VuE}uHmE=RZ-jbZsQIbtufXA{4x`jMZYR4Xm>*5D9#Zq|1k`N-B4y~7e+$L#fZJmAlg=^+fk2?>L}o3v$3Oc z30&(1HsLu?~z#C@qj{D}(M{_J%Qrx^MyF*hv&15;p$bOw{+Ht;^q`AuH- zhOh?T(UQXxGInVm%QA|27m5=<8ib$PNcXq`S(6)pI&CNtUTdIAGWL?h!w6H5H>Rm@ zIH=ctGo#+N8Gl$%u$Cc2GiAkkapJHG9~2O47%3O53YKA*`NrLE!Q!?)>JUnx7?Gr;E99WzuhhYP@~4 znRjZ9P;T_usz2Y5eU4=}Fu-qvyJxaS)@Y8JPn(P-)0`7@zMS zIZ~det-*)&>m_-SY5m4bx?hio2KjObWDROGEXQlGlnUDVF)<0&SQ#W_j{d+_V{2?%p)+jKI*?7?~l#zYW3g?a9c?{y))9!pjq<~tq}h^|?tg%?Q_ z{#N)m;1^96LL^-q9vjM`$4mRCbXqFE-Xh}y;=gcgj||X7*awY~WBbx#&6?}*IaQyC z4`j3~+Vns~{|b%p(70cXVfQudaAgUvFA3QVw>*|)0zmbl?@^K5r9x$0)798mSAcIh z)(GRQIskOK!ATW>m&}OU63W0*-f_qMW2TmO6NlKB6g*I;WSgt(zj!pIG5$Zdabz$`)t6vZ#NWSbAA+#tfzz6F8q=^ij*FB^NmwRqoIVADp8Jf#ebJ8^U z*1!g8hu)|(OisrbaX&XOVRoiTE~Fp;U%P#Yz}@p9*|PH9UIBQ%Vp)&ZdHFUf!^*){ zKsqNUhqrm27> z#4{JwO%n{>m%x2e;dY?2xr^&X%B#`g603B_gfxqdR^|&cbZ{xz7D0s$qB_dTXG`*@ z&2w6Fh~8#CNfQ+o1FQ4>C;y%8(prOxMBQCH)`f{J6Mz6eN`s?}h|nXF!(Q3$nmIqg z?oC$-$Xa&m-@C-+v!K4Dm+jCmu}O0$CdTRbWr;5~OuvxNGCG#^=Rq}LC<0z{TTPko z*!$0kH^{pd)>t@iN^^RsD!i0g_zBp#8?MYKJOAz|sut(!SW5X9Axk}KG`|P3PtrNq zpdDs{wtPAFL|@wue25g#yNUj@kaazcN$|_MH(6|Jga(&{$%U*yk~gg-*VOJN zd<*@I|HIn!ihw(|t^C}ar@2`6bff?YBdU8fy(rBU&Q{!qo(VIlIJ`L@BEU+I#H!km z7;j5R(~gbl9@-JqG3(DZ*HSyTG+?o?$B7y;S#|Uu&=BMP{ z{9@8F#bOF8p@?R(O@A)^K$botW5U9_51;}>BEG5{m!rDj{uNeF2|uNZd?BSUmVd&0 z&&=uPLT5d*K>ss?Ok)S_Z}AdNkR6xSTy>0RnJl`=k0fu87TX@`VtWelv8=E=6Kgm! z{>#twMXFg)AyAW6f;}m$4}Dl!2@;^DF0gz>gs6pR=;`B8irLZzG0v3EXtDshLoR)% z{AYvesxl^YyWhaN58)HodavkA^dDqiYhH20Etb5IViC)TJ+%zJ*r0LHAjWKq7iR3r zryswuE7UNo;zyRvByT7obPncEt{D@Sx5wn3RcS?j6Cn?C5Vez-MAk=?mvV9dh6&q>l#Xf-R;5hUQA43KYunxG}0m){rSXQ#q`V!7+SJtJ>H1?GI9Z%eN5YRasv<=<&7X zYc<*D$TCpcp6`>gQIrH@+FbW<;PpBX#--phV2lgU;MaiU&1z_zklrOG!5ml9h%H9s z7unaNHB{8NHe<43Fr5gcyn8*I&2)gHWTRfzH)D)a7#>f_#s>VA27C#ZF{&rK_zP{H z#a=;<#=<|w@H9w&J=ShR4a7A=Y~0I&KEJE;aCVb9f1RsqAhgTlS9q_687U)8=RmE& z8N+&PfVYYa8{Ec=01w+wRqGKU5lmJ?ucb6zMKach67$KZeh^=dDY=Wba1_phH~q2H zvd$d-#LM@HpHHpRzdB`VVI=VRE`j;U*0f%%7g?!L$=0+7W4M@us4qQtE_~qlqV+NQ z%zW0hdz3{!LDmvbubcICM$tW>p+Ouo{5cLbB|Jfa^Wh^jtzdc<+3&w7JF>pZ*GbKM z^(XjGg{MkJEX=qUIH2cdo&J=G1Vu=}=mB(obzsfcaPu2yvngs+c1e2O3%51RPRB_lqMsM#;kKOXOk$gHUT=rQvrD z8x{nu^7X-hlMyY$Xj|T6T^QQoJ zIfwEN;{;na%lYBct`y(n-TX{t3{v;7||{LC(f@(%W_qJbNzWwPN{MlhlRjSHSn%*czxl19tUi_W(Du%q`7!JKEgHm7|9( zOsc&1EUM*eS?Y>;K`oT9)`zG`8aMpMiSDR43Cn6lxQFwrDd_|z~dw=>sK7uLw zW*UBjTohaE;Kp0O ztM>)D)RF7?TN{BWuFt<#qY=FJLS9{Lm7nBJ9DmzCK~F9Oz9eRNF!PFE>F=~yFMaN{ znr4}Q5N$x>ZEA_)eWPPF$d7t?Z5x#~e4=iUR-Z1qk)yxp{I8jlvS`&RdUv6<;pL{} znXu+ZJv-o*lIH3Ewu|$7;)r^xsLB=6C1xmBYg3n4Agk*i{)RAq30}OZIiwnQg-e}? z&ivx)6w7ke$($_v>5NpHDNzJY^ICqO2%JLV1&e|S599{b!TP_rXIf$D6-8yL8j5BR zqBI3!6XPqLp-^Vj8@aowx7)V+pNnC@{gKLDXiTW=ASnuk}e9Vc@ z-Cd-vDl3{lZJtR`DNF};lTS4~8Y#($&^~p@P+xW1xraXQ^#T(G+^XK$@i$Jp7~`U_ zDJ<W-KHcRh9x9IFW47 z8zo4HjmqI0>HV71Xi3a&^P1>%^$UcWp67um>q9`Ldf!W*C;fB7d78s-a@C?G>zU1} zn)QED>s#AOk#tj3rRX8P1dxYwLfmA#?_DP1;YLVFJ!zw_)!_&%y(7i4m0#{o(VWGX zA7Tl!K?D--PV`^18bFe4F$I*fa^y8>v&95QhOZ4k=_Dilq=2gM@=&^ce0UMfI zr?$c4t!VV;Nt=1$Jow(;IaLQjU5O~;N$WJJ>OW#i7((E?YZ5Hiyh$*UzW=bac?Dl2qkx(1bDZTP^L+aN z86vuQ&QG>Wvkhx0$uwbUViovRoH&ZC%z=QJ&-|p8oWkPRjCoAJpxuD`4_j~(A#1)GtOPcfreWm3unDMWNHv1PSo+@^@7Qd9TCBl&Ca)5&7~f#!n&Gl$ablX zy8OJ>2ssek6U&#)(Wv>(b~c2C2=;@zYam__(<%g5E1pYUL{fH|GN zFPXVce)8O|FOx}>4Ik(xjV7F|#1Hs%(Kxw8J|j;S7~(^ge!@Z4I24OND+IH+OXwXu z&l3>c<8^{Rq@|JevV{cY>gr6$gypDfGP#jh*QJTnxGd~-dc|X0DgZeXq=mPprk-0K zPi$4%B-^kTn!6>OUb@8K_lE!Vl92&Zmw4{VkJ9(W3#elTo{E^i7-7wsQ?Ap8YeQ+CPV{ul@RHiiMul; zQ3B1;LdpZx4{luyplL$oYbT+Mbye@?iACX6JdiCIrjQIa(fum3+yi}J+xE>1p>MZ3 zHu#7zC^4AYR!nlq=x&LF12UfKwnpOpR$!jD%`oG95NC_5qPcpPa57?QKELCo zPlpoHw_`MVRuVF;1fS)`~I4Hs197@KaY+xirO3ej5zb;T7exCe0-0Oh32o z`$)_3+?Fnw^BA5M0(0j9>=`g&a1tA&Mt|9*B++TOBD#vEC97w(*8SOq0Nz91-zV zU9*rq*QYN%Q#vEi;B|rA=?Cg{(mzADm$byJVK(s92FaCmUe$Rbd!*3nI#tU;8p?0c z{>zNU&A3U=JIzn7wFC2_9U^omOB5rsL@2a|z5KX;{?Q|9 zM6aK3<8CLth-fh&3YJ*OSUS2HZRsI0azV{(rpWR3?HM&JD81H7Rwh`<<{*s{Z zSR_l{SxT=I1X!8bG;Ix~EwAjAYt6p#>3wo%g8$Unjh|4MiYDR?pI5;#tSoLsVrLK1+zmw(mShYA(gEfGl0kq9$ddEo5Q#$-;uA$N0~T4?-}>}QMtO;m(|Wq0Yk7cJ zuDi=jNc`K@*eTQ77XyYpK{jb}cQhH&;<8uXEJgj+!qI+%I5N&rf?gS8+v44tvG5%; zpOpw8pPoUkhC$+x;!JMdpoJRy{((P%r(A zFb1&GDhhK5ReVsne?X}tHQkvaybq=7ws&!L#1~CV`Kc1*kKmdIj*$G8wD+=bd4Xjw zDmdy^wO6Cx^3+h%4&?42lAr+hYE@U7`32(Oa%it42^naA7L9Z0k;yDTC%hUMz%F8w zNwJBNKG?(OEaRCw4%CnrDzUF&Tb&3+h7i|0THm=WsUpsH=S2ARK4pM8r-8-KN9;>P} zjGk?%c&Np~ps(2u3mJJyF~r~nqVAcU*6%@c%gQwZQaPKv82TbC+7(B(f|Y5?fz)nn_s?V(h@!J@vTa#tL!o}GOB=rm(<6f8F(Z?Nf?K|UJOCZSf98({}8N~ z6{iGe#sK#nIzuN`kC8QQUxdVs_wpmvXld9 z5)HB~TJK6phQ-*>G)Ote0V}hwrHE3#mJ+xUtQaPWPb{B9EE()Vzr>fC?^ycR_FLYK z$6Ek38ZgPx^C8OGm$I8D==(}El6<@Hg+Y#zy7AP?y}PpFT!yq6!x~F!wjjxKm&fF2 z9@L6(E^7`vmr^)PHG$c|B`5K4c&E0`4`tfST2!=7FN}ZdT3a8kV+;ABNqA1LOl)_H ztN@lIH&UeU^MY^El!g$6dg`Wg+%?p{nk`?0Fs?xr{ww>8g9dZ1Ut0<$sLI$N`m3yK;A)X?_MN-my^o&U>?K`5};r>igEJLCv4lq@=L({EHFFIa*T7y9!T7dYVrqulQr zWn}5WT~a>GXPowa`uVzOeu8(Bl27|VJ@dLqE^iW7Nz10fv&y<*ty$>V8Gki&Zazms z>dtR5Q!V-6B(<#C_3-QC<~;YtM|h(^SUrP?1||Nvc#10juv&jkp8z+c{og@GmHw6p ziKgKpBUq8pz0AhNr!Sl$WlCKsStsni<2GkwC@(qk_M3tJ>;zdt0r*@Kl%LK#h|l+E z=6!M@p=aLqE=S2^x8*f@VS<9Hu;|q10wHHmkG#9m{qFY9otl1}*KuaQ_yTX*Y3%kr z+A@Y%lig)mEtI{~IIokDWKi0wT}y6TI+clS7moLg@OVi9msDk( zMIXK;Q^~XRx+|Yh4{x{%biP%q!cL&E@UHD?IDV(xD5&W`5u`ETXWoKyE%t|@3TlwP zJ`3iz`C_Za`cJcOsrH{#{%GfUshRz;v3dMN-s|kSvaVwy1W9uP+A0ddCo|C=xkZh6@7N?MdP_=Lxo|6cTb6ngtF!0ycIa$z7abY5dW*F>oXPZs)7Uc?#=2}FG?CJ zG*VDmXOINgh(j@fMjJa{RIC65=Z(yD`K>c&Pv9WhdDx=$w&5(k8A{M|Fp+xBJ$jp?x^3vG$;1?&f?TN*Foh9#D$Vux){$h5 z@hQS|)I9aT>WD8hhd@PaZowt;E`3>;=kmtiU`&AUdEZ<^+$y(#<^GJpKkdv$w|EfE zyR=D=h+BiH`J`vJd0OL^c^a0T)I_+zJGFQ6W3{oaP?3kI^N(E-+h;qwt&L(?WQ9*@ z9#{{5S=#H8O(tLhIGi|~e?9dBcC{tE?H2GlGgn1SVvIkzT~CTU%l6B5X)d%-MHcQd zgNoN(eb<&EdBN(MI;geC=kbBt(4XVbL(!C(uSlNtDWoByzM`bi5Z z9nUfvSq{L^Xhdp#s!6%+pVtFSLT`11XgC|pyhMNa|F9x}Z7Zo5(LU5ChE3T~@n0Iv znOMa=En}~Gc#~vn^B(St-s!tlS^^Ho_3AY4^cm18yO?9w7SHL!tTZcu_17Lg{EgZg z+b+}JSumzU3uBWEM)6dwpb`rHsI8!iY@6yv1}E2V0hv^6K_Cnv(SaaqzH^S31pk^m zjeo~@^3uB=%dT<}@ixfP2HDan_40l1Ob)(^nvJ|ZlnMn?WfkyS5x*4@ck`o>%00an z>KUq)xiWOeSP$}omd$E4(~9I*nT7KMc-(l?q9A1JkL~Tl-!1GT5>G9vl&Q6(l6S?t zGri#@`Dr~G$x&atB^N&G&)F!}W~yf#Oceb>l{w4km-~I{o^@*$vkg@E)Q1fE`)jIx zBirnV!A97~Xf8SGflo3l%X@guqHyD+WApfM}}zKJ9+tsA;nVGqa#A& z(^f|#cL%{ld>ESx9=G!A)%;JR9l1TP$)+6`QTx%Jt4n5q}JnpfCc1 zVN1UK=V4QoN94LD7RAG_ID4wDxooSp1#K%$zkXA)6fgZ|-`z~97x@7nc!6Sj%sk^5 z<-6`J5r4NpsZFPY8rUF=>0%{R3+krlni}{GfRYL}RfrBodfG6mX}esthY=w+^D;rq zuHfa+mIl0kX&yc;sidSJ$HsHXv8PytNhB+j#$vq9C%>G-?HI2LE#z_aN1>Ae`j4kM z@KIlb3+Z|pEQzzfmkT6Gmah3ppYI(H8#gUi1s-Dlxt~rV0i4db;(z^RI*JMh(; z-N$$|ybW_^Ig`}+J>%b-*wQ2geO%=a^?@POcg5I{sIWPXsfRvtgDQFV0*Nby8-34% zUwC9jmVh7diw8brfbwIcc}*3P_34doZi=XZ#BWE_Sq#PR?^0Ov z=kirB06O%(C+^-DK7NE^+Wd%w+n2#A8-&kj zkT3$}vblW^=1bepynlw@e|kIXS7&QtuDUbJ+U}L=Vpt2BHFfRK+Hr$vYBe;4cUu~e zHR7t2;)zotXSB~9PL7r;-FsAWgRCyd#qC(5vz%U0v;a}|Kh`q7QW#Mq5mXYD41~kC zGeS-Y9(A^!?%BmYUM?uf4yluVp`79#YYQYOo983_gy{Lc=7FX?AsVk>7RgQxsTcvWHb27>ujH^7g*`#-GM#a(vG z>G%c7onz;^g$Qm`-N4*~Lxng$<%{>bwAhO(x_;@tFs*6}^TumIzo*6<8D%OLDUOMr zfEHX6GlWgeyC2FSd zSE6a%4dEG$L;$!7YTQF9Zzf0Z$$-KfwJkm8`&EHVzVj5z#k|gp;%X62B53JOXZ ze%%OQy#zpi9!7%yRT4=BBnE=N9lmSEG@3d{qfx5i-FDp}>*NuvOSC+pN5X}tkQ_U- zbmjzAN+k}Jvr*K^EB>mn(W-T?YfM3klUA>~r%E?QOM-*5-){^gPapJ7{c*?vVbAo~ zh39ff_&A&y@_KPG1~O5SO$n#7Roih78oKR0rBGV&abzH5*cT({-&Ty7F=JV=LYqFr zO7c~*NV71oBD9}IjZ4;UAWEc}bs^n>{TMNQn15we*SnTi>?0%1ie;{J2o{5_fFW02 z*NnD&>83#gqjdno0mI5z>G8o_eB_wO3&IepJpsaF@IMmc+pv<1Mex4fp*@(?JCYMn z)GVK-I++o5lB7bj@5Ra6^lz6+!@mQOk;YL^v4M^#>qSZ@@9GoYI0J%Ux{G}+b|!@@ zM{czSD171UPhc;U&eNxm{GVVYL}XN_&YdFX=gKINpG%K!wVk)-c^1Ec)tP!{bN=;u zk?G=QK)dfq)OZAC-NHB3E4kWB$829$$ruT-0SDP#GK04|PCR=idE)5>1LI1mNC0Q3 zuVY-UiUVhEMemBe^sqjA<*qJtW@}3n9cbop=ZF@;0@rix5|k&aD^3{HRlE#7DSDRAXkW{LJeo+aX1CG!x>;Ws^a z)1-QGF+zORocQNFSIN|k!td5?Tj9W>Cf}QvqA?Qkkx_&bzLo>Tj=xVJ zar9!E9nQ)tsAO$wHJ?fN1;B?EJxIktH#a0o*4gIC9jxpGSNR~6yRB|8U4nhT4dcx1 zGQ54rJYewH@Y8#~K~OB^Z%t?XLiS~3vT?W?R1Ly#f zOPSoUDK2^)tm`jk^EH?iAQUAZ9#j0#ivO^lM37XJ1w(lumpW%vis+Rc6osJn;*s%n z;9Dgp#a#-6Ni~ zRdX5yHfdVSogil|1qy0q{+gdBMWMUXI%+8*O6;(!MrYYm?J_n)%<^GotSCbK9qK)` z^g=R;N}QnUnWzJe-hdx*@K&z`jTEnRIT>9D2Wh(ZSN zwGz|Egi=m?QgeqDuqeaV->Xy2bU#M$&a)?U)*0EYi;R4N>O~i89ZjF4dt#(yu+TJf z%}Zv=Ly)OM{cK^TL{0m#=;s;=9>^1%3D5$=~BrDWvT2ZHM`I#Jh3S>@U0y3?H?G$pl%nx818gO^f zWUv{ovgKbfGhFoPKNF|=(<_?38WVo}Z#ec=JDU3{zNog1El}}-N#;&j##AUmImgsw z+}gV)r6(-58O}CIwqarCW-Rte$SaG15V$4I7Py#{b(d6RK^bPv9UrrKs)u>;tFBJ+ zlgi8f2dzX``+4H5hnj}5REsOdCYtBZanIN*DCADdKmM7QQuSgeDNyX zTXr$V%bFZTbNSuCA&Hi#|3q9?@HYOUP=a+E9sl61wofQ9Xk*BdxFMdL1e|u_k%kzB zV&Su+Li8U~f&ZTL&9?>gN7QS2?re}DGpDLoHOu5?=yXF05oAPB<8UL>RXckN^ZrR@ z$Va+=I*0lh#;7O`beY?&N*QDy#)ifDc^Qjj4;y;JW79VtxnXL9ka8gDeUd?T3!U(i zZV0NYf70BW)2=I2`Xg2>PlmE*vsX3#BYA{(Q-F05k9hct?cja^!=6N@KVIe{!RGz4^LS?+4KLT>|~C9|?w z5~>yC5UJ6yU-aoVuZ<^6ex#>kd|Y@5`HmqS0{CUrL6=d(wsGyHSyCdO%z>=R-?h3duvDI2>xTOiY|Q7!b~a1lhM%B z=F4Ta`K#|2g>?iWUlA1`JMpQFx++pNi4}blv@Hnk6n3#Z3Cs-2bZWe>NE2fgupg7R zh{mlo6XZk@{vVKm1Xj^7x;OD3NYRIH5|;+32dh$T7tR9QEsqnQN=FcS+OhV+2GY`blV$T#W^1-EoIW6EMnp-xoDNUV(ES63nYz<{R|!DaL?B8m8e3>Z_(n&!=`RC3D$% zg?5wbCca_vT=Qr;(qV}j9XNdz8+tVs&XarNu=dVt)3wr`^$ugow>2wjV zbZ1xed6?i!#`Qe{zC(>$-+?-L{9EV4oK-#1Mpn(!{_l0YV>;1rVW@3hQ-J&{P2wh_wOj8 zH|FuP-Bp*js}tYVgUP!8JTB5Q6N%)#FPrk{`c?NfZE`6ekA^w@4R5%t;~)RLxp=Gl zm;p8Pp*<=O@4w2nMcrO?#U^Sj?4nI^=a>|S&KST{VoszZ#NUn@8Z@c?@ww8pDQDGT zt{Q>TStiCzzFGrJhKV}<)kptA4`MYT!2?stmGAi&H2{&^8xrom7RyqV+0jhy0K{~^ zMa*bf=LGdc)jDgq_xvhY|8Ik&|Ie*Gi_DQ*pH1e(sIOL4O3J~YsQ24wb1<(GMbg?b z=mD#w3~W63yQQ>EEX$_NzCU%%(k8yxJhe=foNjVo$zXzPE-httyFMJnocgOhwbteO zd(ofGKmVfdL`S?OlTIl+n~WqYn7WE6nf}B2*b~BAw@vTW$L*B0M_lSdy!ZZvVhndnyZ14;@Taq5Bb7R>_8o!S8IRrs;-{{mkEy zZt|pUWE2-)$WNB>2Ndr}*U@)o;#>-A(%p)<rS6VvKH_8@M#U|r_f<}-G*&697r!Eq19l@<3G zkkgG2q&myZE3LJmKq(~z5Pp^T#&Hft6&o0xZGzp>6X;Y+yA7qOjx?1Lxbjq;dEW^j zD5UlPgRa#lJ2Ak$m1!UkfM^mowRhKTr1vM&TRp4K$Axa46LLQPw;_CNd!2R1e3Bm6S3a8^Bju%CVV)vXGoK0X!%jt zQu4DY>f`Y_EDJT5w-3(lZSI@ti(*m znEO)G*|fRTURvhw@XjF%FxcqZ9D@}Qbg?%rwnid2N4D4&T% z#V92qw5*b^peCnySxU2P2IIolscqw#&MqE-WSHAwv|ZUl>mnkYNK1v+`jmt=;(An- zC#01SDyVpMyy5m&B1X@ySF08!B1;ZAeYVF^;-SXY9&QcMh; zQ3k7X6_1WZ_U+3&XN_5|Ps4`rW%iW0G#!&1+(oLLn`;IG415D7Z!+0|SN( zVsQ+@Hy4VG!90yGFv$rI?Mz^15fTF#Zl^uXX)SAug@YqqHSoa;c|8p@UvbWt3Yr6?s`D&-g(rpX(YTwHZ89fv9tCoL8MbBC z`0P)&-m%D`Nego3^Nq`l>|{o1O(e1wTU&tWY#RWH%lUl!h_t;pqEwsmE)2WU8F{6$ z?$VN^gp{=5Njg6*4d@MI8g5T##>LaXmK`Fxt^J=?$W_JUrDO zE<&U0Nq>E{y7ZotNyWbaB;qh(CBn%x;v(5Q2{3PSVrCY0$_8%4W|y^y*n1i*XE~R$ z;wEW0ClQ=MhmbQ73_+4T&3E9Q(Hn8mInJhHBgGiym5ia-InnppU7Fc^O!FL0Ps*Yy z)#jqbVu-7BFy}{@^QciwhtLqU1cz7VB_NQc1@$NeD1mF!<+#&cQWOyLsY;Yrw2;=% zmfA`5Qob&s*moNzb(ju8@NLDsp<{b3;;XD%9mX?Cd_jpdsdnXrg5pyqGs}KUDp(0F zEUC4nMN0)ejbd+;u&eF^l1aw$7_GN+V@{~Fu<9LPsD>V8MQcbx`V>IfSUUvz0kwtU zabTjt;hK9)EG%myl(;#vxRBuD;0A2hZaZFLC{ptiaOpUB21c>We$utTvHA^JwRNLr z_vWR%b0b_!<+I+p#lnx3SjlUBcs*x0^aqC~h|KFNxwgGNG1 zQt)L-QkJH=(xfcy00L5=6ruNOcjVWgz_3rS+j0pv$k5?Ij*owr=E{j3ySxRp8BD1` z4J{=IK2t3rNkU0b=?bPl0LEkZJMfU0CVayYG~){-h4@&rHSn_+U6voVCH<>OGD#Fh zF_p3${22#W)6xM3F|&D%|%N!z^k@1=czfdWCHCr7O^`(pMXo2kqf2W=qU< ztvzUMYCOshzC7P_ArTtbS#!*pD|R8x5&SZHXuvanhr%uP}rXGzQaA zEUz`Y=W|%SK*OpVI5=bN@_TOrzu~=}aE-58V9{URU zeRL1<`VD&2c05%$`D!Z*c}|{`G=!;O6ZEpg>zds6Vo`%BTyjf9Fiiy-6&OR6WCswyiERk*teHxGJTCY>I7BdCR;vc9E; z&sulXN{YIQK?)=A?pvm>xrNjIh z-a}M{u&>i7MRtcKE4fD1$j{y8K-*kP9Wq%`620{-mDW`%1ZX zQzsxc$xmDD*4Z%NLbI$wiri&LF&l0Wl`RNuzNt8{IGXTC{0N&BKERBtBU z;Nk<@mbJ=BCdQu4CU&JUX|>L^CJZZs(%z2xZVrT(BBe-AS&pSAePHY;r9)*P1G<1e z9Z1vY>02EmPb3A9phnQ8_gGT4J0ukLB%Z>#;zBOkmh-caiEX@FZVK8f9_o~#NPo`h zBTHJGNd(@KsKO2-uJJrfb^~S-q7KjJ7ahyt|V1} zGVRN+O47AhLtUFLnS=Y+m}Vd~pw@tRR)|EQ0^c3uDG4$_Xss$r9(SQhOI}*iN5`b~ zAcL|985@agMCNdDb272w?X^cxbiR5EuVSO3dmhOcB&J)Q7|=t`jSbgh)N|AgY$#He zl904CQ?&uJqw##VqEFYlv<|8y$XG%g3trL`rJy6Mtc~n^JJL7ZBeJECfNl^fwuDNZkPG($&WDDc6Ak{)$;%ifb!U=IZG|hX*omMG;w!%qxp62 z?aRlaov`BF++Gl;ZLUgc-14Haa8jCaA*49s{m7Qok2rk7*56UkNm12zFw64YxGNo$ zEwSQVTw*-CjL7Y&R`#28g}l>&3w5=0tc4c8gkF>IoeHHMbhS!(*zJPjic&+-Hk9aW z5>VEZLwVC$M9fkha{}xS>qOxn2^(3>mOO!j^xAOO7N+ZH*;eyNwwZ z^T-@eYr^Bm#&PR&w|kAwL+XPm*It)uN*+;ZAeNTNWl2I*geB9`m4H$`D$jB3Y3zyx z%Kav3VhyEDD&X^OG=~FjkN`_+9d4+sq-nIMC+0g>L00L8PrO=Nc3r$UqQm3`^`Rh< z<^(6D3m^}LKx~nr+yj~!-WwZO_vVfHi`?eELc&0BlQ1tJ=m&U=x{ytgql@9#Y(^#W zOP&)Ti}x|g`0&gzhZePhGT8e<=aT9Td7+ukZM9WtJc)Cid3yQhKLX%@*of>0%wZqO zDkumoDF@*vW7DU(9=odHV(~M|EZbRU^R`r!Mh-}E%j>UBjV;E~QkI)-Uh)2zA!-3d z17oXd2SD1*%O1SiVa0Oq7)4z4`_xr=trpbKI_guV+cTN~dqD)akZUtYcAHtctu#mQtSsyh z%LH$08s@d7f!Rd>0MD3rk9&J_x4u zySv-WNo<0ZE3w|HaLoBa)1O-`HjHl^wBau$w-~yyZ!JQ#PA(7|bquhCB&-ggwXKk# zNC2Oe0CBmw49|T_W^2T@#+gGX1Ry1}k1ezyX<;eupmtB+UCD56wri^v3zdcTr@Drh z5t7o3vF``?651O9N(&?59V8M?s}%T_c7j=-mQ1HBiSJ8gpuNs^r$IxPw^L{^QMIg% zk(!44I9f|v6J4ntn@d_6+p}WPB_iK9!2Wl#qH~BmiTesLd25ehOyOa_;pmM5J^J3)N8;eiIuOmu!(^byue*rPl9lkYz)TfZ#C0T|q$$>q3^bbgX$`boV;_M<-*J`1cgq z?1nV=R26TQ9b3*sbsd|KOoh50Q;G>%n{X{4>~sOzq-%}R(iu$lE-%TrTu|!fL@hLg z#gs}E6i8(~VPL3y1f@May=ziNvUz}e^EsifFV677Ts3Q1Vl9qrQWx~I4WZ3UGxM~hltp5OL#NUk;HTJ`DwA^kUd}KtlzJQy6Q@Scq zY1V`?*gHv5JVjFRNIsd47RjjRVGdF>^)e8)-)%S(v(N(%H+dU|VGa_sDMa}Zm+kq-lf>IE#;S;ihumAv!tLsv{8>P`y-?) zzWp_^Uyt1%UtdDs^7?{;I@S_UNj)k+Aq7N|djLkx_qzR;cWG{JJ3i`-Aav}n_(TEJ zvQ|~DqBa3K+4S_Oj#&eYjbLNi(M$xi>O+ZT8yQbqHO|$9a%BvdwozU4Kn$jU?FG0w z#BXDnwyBlOlPR3Vt%88dsCn07%SzafDP}_44k_s!XmKEwE8%rXI?}bG4MSXqk-}7a z>y4?z8EQ;-)gie7#l|Udz`Z!404{|EC%)u z%r$n>($i;7oty6?VgFxx}EOLY!*jHFk?m@ClJuqvL?BWYgjaJ8L5m0>Y5H+bQ+UD zq?wM(c()cpg3rL(8gaz3QUX?lge6*0&?P;Uz4qOAt~S__o3^djlHj_;FSzP_!0cyI z5!S<~QWB(|rGPXD_(&go{{V31^hv+2@o2Extc)$l?+7uYA=uvZmAG7WUiasorGkQ_ zm3e1CgnHFWyIkhqt@fJ?YZe(X_iXXch|V~bsS47YQ)gNv1KfP7Ub?VRIx_b2o=Z)u zNmwXSk@8Sdl1hly`yZcuRaoRHa<5m5Y^e*kKdW7yIvlsGwH*z!4l8J9pgG;t1Oz+k ze9*5>!l~rw$L&PFe~+ApEolu6yGzW_uRrv-z)3TSBOs*R5AD zx7gyr7?8IxY_4?@b+(EIhMHcj1ZoO|Tw_l6##b0PhL9U8Na-DEcV%Z?U)FhP8$ax{ zK6Uu>Jqiobfgpti5!js{K%zcd`uF-8nxw{gQihpEkk?YS>WYd~K}hNA^c^R-9f|pF zpce%6I1{GE`{^Y`UAuQGQSJLZb@%U|#$fFDZYkQ5SkUdMq&b9qblj}?X-tzq0IJP( z%nk1R^saZll@FyUUpZa0t)0D$T)BktcY`iiz^xAFqYkZm0SdVhRu=o=1 ziCR#Yu$#5E+mE>@$!<~-*(|oQ5-V7m*q7Ne;z^z-@SV%NaTf%)hu0oOWvj%GAy|1kD8a6F ztm0DaR$OjHzGgRzu{5&!n-HX18@%6NY7}SIR{g-tg#4EqZ>^<#MMqIA&z!BiZ;$a4Ke?!f6r^hTBquQHu1~Kq9EN3*r}xGKiLp zNMTng&p4cctKG8SjXP&19-mtI?|N>(=3+RrBdalR|X z{9lR9`gq5tMR$2ImBh7p$ zaJF4wxm+OKa_h7UrelyrZS+Q$dAQ|ly&%{vD0DR7c{eMXAVgA9*h>jY!Ab{gxj^BL zImViuGTn+;W85W7m2{JFMY0;^Jw8IZpOmRjwBz1%f=ZlMnere6?1N@JlYilSbCPVA ztnSxfxgm+sSbOaRr1>T@$T8dqQqtKG-fg0h(s}|1V1P75joa6SE>Y$TE>3C4-Y;*Q zHOje^b>1u-Q*6rHwc9Lq<)pQ2Vr8+rtX3z?ayv~r`^jPV`qoFpM|(ryPHFMM;MbMY z;q#TUH1byva=qF{HxDbBa(dw9s&&BmjLu}tPoC3q*^J^GTRTW0BV-=e8SN_YmBCpr zB-nZDBk@${9b-{pH-DLDvtrhU>D~LXygvVJ)y33PI{gJw~u&;m46Y zNply9jAqSh$*=kTCu6?3c(nbw9jWf3!)RGyiN0I0z7_d0fcTODE`=#QKoCW|4~H%) z$EOd}u~q*7CPb_Kf^F^) z_d9i(-!=F!pKYw6um@P*7TncgdlPxy( zRRvKWQA4P8OvNm={CLlW5U!VX(p9n3L{@dlSnbcmzDT5Hmqv0Wx{20HZMx3$g)yOZ zB`I<;8A?n`doBd^rDB%f<+u0D}M0lXu)ut%G(Q&Z?mr{Ml#%WMvnX&Bqvoq z6j*CBC5Eh_8e_9G>r|&oQV}KC>2fnlLK{de zCQ6-8N$PAJRPg5?&Aj7Wfo6+;MYi)|#!aHfbC&d_HyY(S(tx|MoiWwv#!)FQu)j1V zryEL!v`twCRqWvQfAUTSyG7>J$C+ks8Fo%hltj;)nUpZoF_WDC1evY{_Zq&?BkXm)l}ih4lN)u4@scAO(C1Q!TjC zR6z3xAe*~N0q^?T@ATK>-}aNeR)E|Of!aJH`&77d7x1lizcFRL<`OaNn;MIJUFy|v zvY67Gko1+8r8MAW*vzRVZk~hsy*ux}q}lLoP+q#-CC0Zyj^A9lQ)D$7BLkzU!uzf% z2taJ_dr%r5yo;BhZ}|Y;$v<~}k^G;2sG`$aD_d>#c1c@_KvDkyVv?l*RsR5j30K|g zR%w>_y|l~7qF}r=k4@xEp62@DM!PCBxR^|NAcT^;_e^N>6w;Opjl;N=^z`g)Ni|FP zM~v=Mtt!mCQFF}9VVIp$VOa58nY#r7$22ywQ-1h80 zmtK{Q>_@~WDdTrDdd}6u83q?{Lr%-cu)L`%xQL4iTT&uh6_82uipI*@Of{`NfC=7= z?R@b|ZOiR*-ohuvx7uR3^Ac{;B*tnQ?20JXYAV;DAZ+X9-m%5Ve$s29}mO;H9#}Z82%bfr|)1P?dQ(knpJ|xVp48^qP zSxH01yMmZ#k3|K9^y@@=n#ZN0CmUpbCi0x~2$y2Yps_ zAAJZTsiG6Kk-p=;=!(DaFOF#bIPZB=c)u!g9y6NT?bl0YVaJMs)u$JW37H9ow`{Ao zTwU`dwI|38OmZxDOJ#o&O2|1UvR!!Rh-~V>5(t~Zvow)hEGaYS1E_}B3f%`$vLa&{dS=PLP*IhC!e=081A?sk^o!CR&(u7$Uy zB16%V`}%OU@#$8Sv7jo^_{+p)`;9p&?R{$Xa#SmdCaHE*t@dp9DAJOq35wgLw!?i7 zm9XMk0O=(3_N<+7Oz|ec;wzi7&L+h2>)rT!t&0`+#;zBJR+QXB*F><1u1u2Lpr!(r zLx>A#_uXQJ*p?1Ziz)_HX?G0F?9$zD9J9x1sBOChIC|`o!9aO0LrPm^vK(z{Qhp+! zPROTO9FZsAZnw;O-|n|rskyGp4d^J9A%)%6`oqRiO4wH4lBXMAo>qcD(CO_d+%K2x znbzT5u2LTsTYC)6b~J6>z2WC%I$J^-*eym<)ONR1zca8s^%urhCKZlFvc-zsuCi@6 zsM4pgDY=io5*4M^UAVCxa|IgsTRi-9V=l= z^4deKvZTpKDG6JEE2X}!zLE$KPE^Zo@~*bm=d@!xHb|Xk@}fM<6|5m&!?yf{A34a$ zbIWlaYJo0=bnI+iPlxVciI?GDV9Un@%-bKmThm~;N41%axgD7-H*!?A(95kRETwzX zGTwb5w6yDvHn!H3EhJWzXUp>Je>a1WAz#jKa=cxvh_^zJiEpLYs8i9S1?f@ax3nc_SOrOsrh{oobQPmxdEUr) z>dPk~xg6^2y2T3qY4O^n-Md-~sjImXd!zb52Nn2}pd$hQWh!+F%DwxXd+WdUHT zv5Rm%2g#qL8&%&PiDkDrX%Dk7D4CDWn&4as7NoN0LfB~x$Zgi5wo=e}1!zevnj+VE z410{_s{LxZF3mb8TaleD#*`e8q&ULaX%8Pgk9bpM)dg?L>sLuYCa25A@%7^P<{@W^ zA-sM>wl1FIB&khIaWhoPn{n-_EgrbUGtyL)k`@$|6#-d1P2rIE!t#qGJJh-Ns~^2W zO}gi3Tww~_OOZ;TNVr8q)7nZ^)~OFYC?Pv-m9{z(Egcf_C=E83rMzv?ezyZlYYMAtfm3@|LLN zAhgc(g$|?zJVJYw3bS5Tix%B{V$~Qw)o)=;$S zoy>CYYnb$jAxjfr-MU+E-&}(7;=Ix%`HiVfEk&0CR@iAkDCmQtB##lu#ox;;_pDJ5 zM}?5%@ow^}B9;($+T%Z2lPX7SeZ~V?ZY~g>1@A4n_HC>yZFVrXvDN5g2 zQk10;SBg@V$6D!1Qizq$7UKP?RB5rnQ;o=FB?EuLHn$R&TRI?+w#t%6&vn6Fn2tkf z^f@%8ETKtKRI${QzJ;Z6%7@Jb%>-+GDLusd*ot?PJ@!?V*1 zE0*2G0qlDI>rw78nj@u#(X_EKv=9K0(OaDaXoM+$7jP9_EEgM}8FJ1!liHSBHe2RFsl^|9oUQju%r6JJ@m6z|@t#c))ZH=b zyt{L5^p@19>_||9DYCwxpt#;k;DnD}$LoszL63gQd4}Z{+i;V5mp>y^7NnL`R3$jz zX)LV)Zh56BNy^V5G7dc;Ian-Uv6ESjfbm&mqcKtF7scE>kL?r=M@KPKr z_jgp{9R!UjDLo@qX)$CtkcSVOImuzPD5tEghl8M@d)k2ou$1dSY?JBOR$KQQUhd18 zeaK!uP?Y4mh5AeCQj}kCsnj~90$p+4l9AL@RGy+eYm4oiQr&A^pAL$ru8y`6CMl$) zYEV~5UdGkbt!)JM2`TP1q4pdM_)Ia&n%C2GZPpwLfJ`cv${&n6o1PcU^=5{ zVJ*;=zv?mRNQScBF#qJ-S-zP})a+ zrR@IzS_t`7pUB5D79`jp-R1dD+>`GrDsgIkgeiLJc!c*(hR~j)uKNSEb#vmoTUOXj z?RN1A_K-ABobrnvV#z!B`od44`H@;tV6hxZc$taCTv?Xx&4zev5f_wgH^EyQf%yeuTL8B6@^_My3dJepFTaRE6+JG(!(fQ zhzH6*LSv|>tz8F2XInnCQ}7;Jk$=Y-$kA$x7DdMLsPGwE%aYp=nNvwxPYH># z82eILC=YzEnb+B{+bdz1x~-Npq78-guSUHL;LuHm7_J!HY?4IBv+@@gZV;rmqp}Gg>=F8)YO0qByloaqN|in|HNS03i)Wy=neN9etqrfC zB$ccqy4Eywd!6pOm<$|P9uX9A0@Co;0k5nibKG|XP<~TH6K8H-j~9!9{Al5#jgVn5 z(aQ`_F|1@Ph{qIcjjas;G=>v;iU4?;k>PH~${da25@pv0LhX32C2?(6NNq6U?aaW6 z@0;#wYpf2p2U`5aD5xpl)jIE*?qv3!;yxbjQJ~|t>6s@dTcaUH%O&R{*{uj^DQqrc z+X`{Q+kwM|MTUYG5hLFGrRH#23$5nMobpcM>2>%%GZP06#$Y&LdB;rou{en) zji(J1MZ|_-$#D&~>y!a+?`RDxV&tz9e7WLln=$T5%WZa%GckcFxXQ<^(xNuQifhen zhSu!aWo4DNG(wVklf3~~35N32+&)WquQ0UL$~(nxA~eTzr85;1L7DFg~=IMkfgF&^|MVkP!B?hO;w6sB1>(k2%CRHXDg>N%7N8C&fiD zmoNjhh{qcXqd7}lTIYv>m==vJ-ioPlcQ@c->w-Rp+hWOVwqDnJL$ivRbxuCVxW-7@ zNLUGQ?5L6mA1?C8imcm?G7baCF0wDrJC@s#8gyu`OGAvj{{Whw^n_M|QsZqUwKx<) z)O3IpK6Q%=1~q5$9**7mY!{RneK17FW2E`1bEJg@2LeGs_)^i)J-52HtHx=Sv3|bV zWn5(4nPu5<7;WdCeXl7_5K2Sdsz^ZTQAj#E`Paz!Fa91IhV+|-`b~uNj~9n$3ddpb z<`>|@XLLAxT$qo;zF9oKF@(5h zI}kL|txGu`Pa5{~7l{!Q4r9d)G|LWYq=aZC)hTHz>e%W&o7e8|!}Gk@u>_y=?56$U zr`G8SZ2At9{>MOVI{^I1^{QueY4k~s)V7p>hc<*LXnA|;I{9|_ef#b7pRx4Yq+H=V z>rOn9*kPv0P)jS{`liUyJ0tg3>pY?H2bZ?8ijjuM0D{(sjoZkNBckgvM?N>buMQ3% z0vronZUDOZJ6jzGk-WipVZY(+afE9(A}z_5JH%-%INEv^lOj40wj9#Xpp=IIQP*>` z-`1!GYQ>i^W0Wlxq1PJA7Ciexno1mPreHf2U@KY)DUQ(xT_dY~`&S&9;*c!peVTQrU)Z4v~y5b^$)QUfWCJ#r8oi1~3T zO4jRVq!jF{tK$0UK-DqiTt&QQe0S99mm*0nI2r*k)YHEbTw{GoN zlDM$hbT54fQh`c79VjKe00+~)+b;<*npZr=8IngxKn`dC8ZOXvt0d^%>3lLQu;-1^ z0T|B5IoF(!K{<4)zI$|5^I)BiM8%)BJ7Z>L{_e==g26&y_~BxhCAZ5>(q! zUTl>mx1Nkt5TIKf2h1fQXG5z)?>5hpS|79JSLPKNK?$DgFH}*+(g-AW?dUbsxY*mZ%S(b<5hty(>?JqmC9gE8 zNhGC6Jw1-bf%N;T>upHF@9Pt$OoWTYu?*WKs$_5ak@lfLQzcGYqt!n9j3A(&>&(D@z< zohuv*NR_QFr&~+&w1b;4HY;7sEbvc*7=ObTVjkA#l!9ExTXH@+Ia&zL2yxWa6GR(% z7;BK3Bl%9!kC&!XtM1 zX-Nt9pFJs%TmXXF5|lVfI=u-g?XprTlwNY|w~(!EWfse2vL&iqk9KXwt|>B$o^Fs` zLfdU9__VbE;o3XvNbVBWvA?aRVrPVYy4e7ZGz@=(misRo#9n z2Vij6M@GtpxngT$4|_{NC-;ubS_vk=?%uR$D}K&+>l4PW&fHMkwyrO}x7k|?4h90& zRC-Q;Lt$lFNee!@R=ZTWk8*q~GF!AiK3uybCg-V*I)oOaKGK7JSSWL(hX5r%*8oT< z*LlZ~vE!6Own2XK>`RnqB&@vGnY*kdd0{bD29O+O7S0^-}s zmhJWmhrLFgv!yKx=?=zEQuf;RN!U*IHNR2b#$a1N4RbLpX~V)A?2?AMEro<7lEDg42^wp?TyG+BPBWgj7TtZrA9E&^Z(%Lm#GZ*!r96Y! z6CFXugNo})2nR~(3i;J2k!O@8I~K|j2}@zJ7T8NlZac8j!=X+B50;`YwXG2%U$9Cgor)a~YK787ITS4UG$h??6*VmsKay zl&ouME1ng(%eBVeZ)ukwT&XE*p}iG`)IWuBDI?-jZS^Q8V`O(a-Eda}rfY7-lL)wl zu<$B%wFDOY&|6PZ`jS!f<* zV@;IWT|fcG9BF8FS?eoELrGavP4p~~ch|JlJ-MxKw!v^kTZxQ@_Q>p{rA{fuJmOQb zqE}f3jR`lOrj&UxPiCr(V}M|}$}I{Q+J z!T>@DK?qPlQnaD^+CLybKYeurB8|?-1Bo4_mX-oZ1PcUom-k*&_zBS|a0+oO;i9n=a*aFBH9VZbD-Q;JB2hs4&=Q zgpjkZF@27^$gM|i;9Zu(@!3*jB`n(8X-=%AK}0Alx}Sltop(Vh>g;q4Lv3&z^9x1x z(4K`lOX^!lT2Zp3Y$aP#hixC{sp|%Xk4nj)M#-+u*)g>^TD#n$vFAvJ%35(vsxnHN z>TtRegYmbdsGaNA9fn7oHb*p#V`vV{K>(Hl>$}#R06XTZRBUlI<*YemFU}-6yP`=m zJ+22kNOP>yTVR~A(4adw-O~AQmkpL+DNvzJmX%x>Wii%=C&!Q+Y|3>K8%jWZWhpI_ z(v*NfAwn^K$ShXYXWFp1*CthWh8W4*nJK9Tbda}PA%NUgJd^_ONKoGXF2m(0WVuVM z$-HBhJKWiL!mjVT5!oszT$Up|;}E7uX)ZXQGu^@t`qn@vMC*0dvOu47Q_b4o%$&9( zu0)rivgCl<2ns8Z;Ry;uW2JpcBcud(R)*>!Y-Qc7&vRNFUI74TLqN9b1A~Fso2myz zmSWf&*~Z4cLfHFPHNoxeStFbt*$HUui=JM}7GBVGpu0)r45r%xJ+;>)Hq3V$b`VOC z5Qkd|0l=>>^^%p4cfV39O}gFX+~&!VElxcqTKxA9Zl_#d#-(;1s@Yek$of@caJ3jA*1sd7q|^Np7_l2h0zzoGBnu|A}F?4PemO-5Iolyt3O)wGqJ0=Ckk zlq`>tQgle}JN?xdw=~77G=n*o91@UJ!EkIOXpW?kI}@?n?)CK42XA5GFzrDgQ3O@) z2)*c#Ys4*=1`z!Noj?IZmkSz?S1*9CFc)eZyJ{ocBC6*u0BxPL(_^iv&V?if-cM3M z0X<3w;{8#eHBj4bMJC>d8fDiK+o~#gkP3#5zBH?-Xy^`*5;dScrl5|Wb{g;L^sC1;JGEt4|i17tX!P z&4I3Dk+?m~c2WbwNMQgT;VrRl7B?V@X#(12+g@uT9roLHSQ6N_JllwaaSN3dLdepP2>g3h(b>75jB*=$wruB*Eg}rtk(u_ToVf}3LBQbhSPL=`8tQN;g(XA; z0996L77Oo74l>(*Q9Jm^0QdSH+XLmlr_Q2Fj_Mx`hS0W(w6>B`4$sVVllPCuu@$o< z_^ehJ@N!E30KvNw+)v@y^2qyU&=1HQGK$`XkaaanE)yL-0%wRG%+f)Kf$o+lrEg_C zLMLSsG_Xj~PmWO0BySm-=8!72=a$I0t>JIr^U-m}?kzVdhvTG`!$YMmGY(3^)E6Q) z(uJ)lXegt70G(GJFOufs<0mGz!MNRI!HAYj#$g%$?6&%gWn`sA9R%$?_6O6k5pE#A zV>TGnitYW;u_}UrpsAM{C|ij=J4g-`l=eRf(H+4W z^c4#u%JA#kRw%Yh3$`D0f+Jkpi_z0EN;(godK85hR08%2dxPuhDk8)>Aq;Q1+FApo zCnKbI)V;;M%?UKn;ivNQQ;21~%!Ek`US3)o9)8H>0ioH;U3q{lIc@GB4b+xxk_OY~ zM_Qaqis?x|50-WD{6rNmTTX|m9<}<570LFUaki1s9fe47N?H9x^=&%s)Zqy|17mwn z*JrbJkz;n)!fg;PG4FBaxT(?~D)Ltdd7e;dlCGq<31v$?BYi3(e?Qn(=O?BxfpS~2 zrbBm%R5?ab|YlsD{XlXl(-0RFLJ+6zq z_q;gR4Rn3x?D3caa?qw6;ktq<#+@WsFONJUMTTTN8bg7$3YN8{(o%#aAQHC;1QdWe zdv`k;@7}RImmOSIZ8cgOC!FPQigrz`|5~U?0N4O`xy(&Hfj%V#(W8JJa zd&R~>DO1qB#vHd^1+tyH<(8B1q>Y6(rKBJ0hWpKVo%=P)Cr6u|qVX}uG$(c4`1ok|r7Lk>6xN_rGL z!mrg*LV(`(G(q(qr1q$r_`~S3L%cUKGtNl3ISI>h0))8AWi2Qz-;97AF6;F`{twr= ztDnlE4C>cunJVKJ%@)sXd6?7RisBI^$T3#@Yidgkxz_uxB@KlV2G$fvsw+C*bjO>8 zZn^uK<4Y*MB~ctpk0rINv{I$0AfR-x6j&aGTR*K^Q%LuM(9r3a&|c_Wad+C@%8z!W zy9A5qv@uwWGh}mc&yq;*D?b$&!{S~etesU@lyB6pK|;E_q-B5sX&6G9VUQY{A*2*! z1{gXOC8cxdW(etSP`bNQx|`$mrLi->$@V6gW) z%qteJ2b7^S3RPrMtN=M)2<=iqp2DZifJQU>q*x(YEzVo~^}ioIMnHX@A-FTL_k5Rv zS|~&rq;-L-SF$K+8HSII=mh|xauv`a?>2BK>&z~}TPn5sqy<$QpLh7P_y)s+W3)3#cu zI<~?(`UUZ67^1(m{f4U=ln_XPc@96_s3geT{V~VwzA&M<1CjSDaYsuVSxi8o6quOi z4c|^-wdy)HWS3K0;c7_q8^08_9YZjZ!_Fv<;UFcW~@!LDDQ8gVa z+c&giyro1p8+V9uPR_CnkR+$_D<{w?|B|D(5wd1EsXVoE*ZgQW#V)+b>cB}Gn%Gb} zZf(Q+=3Rl$hj3r4fVZXVMc29cxfS-or`VhbR=B>t!&8f}L! z%L^>t!I!=HlhrnV;NWQUbs@E*9QSiAhRnC?+4#=ms0GHP_}mC}oZ~hBKvD1G{k*&e z!2zA0sC{(=A>cJWj@l4BP!#ip7}m<2eup2N6fjA7;V+vwvxs%Mbh>g;8p>4X;%ka3 zEUf)qkoSG!S1|nc$^|u@ReCY7 z{>nry%wbS0FVw$ff0!f9QOUqU^ec~jt85YW6Rnha|ldTw`C^k`<8A9gtiF2-EVyvIxJjp$vQCd+hb_?UwL8ATC^>ZNuSz z!%>cOi6`S1(HROYi2z`k<18>S6OYR!G7K?uMu(9}T(5+e&n$5D=pU6h+BY&rcyw-Dbu6A zAFOl%fv$h6{b`2POE?^K8*E)f< zGUj-WL@6&MQ-{1$XY;s*u=Hn^#^h+Wah=rNcj#dJw+^hW_}c&utsB+CR7Lx zEoGxz)hLPUER@@Zc;h^gQn_0CB!~>fT2BWhE#3!dwd|ZN*RBT9H%H!}5cy99YoN*0 zqDfUx?q8FR4cB3MP}$ZAz)SGS_t7eKKggC zNC$-!Gq%#n<$#|nLtNjf0`U&0R9cLpV|;cxpKnW}4Eoz$qi)Ucv_~`31m#kHr7Mbp zBbjlQ1b`5T`wLr%)kAah27?QniB5s5PEPHJH!DF#v72?5wu^`<2J^lUE^UaOrshZX zX?+d|tLRGJY)n&J=eD}k%J#Vu%q*{#WPv$}Ll)yc+{bvy!;=_IUA z$&AP=20QRVD6~o+5a%RTEC^L2`63NEd zLHG?Q*Q;XQ`fo_{EMPy)N8)93UhJ47ece(aKi5EBzkU+EQ_XBWHFYTwOXa3QZFYRe zJh{*KY22QKVJ)m-?6KSfbj_zhO5MI=4=k^g##AoFDPkVjWOsJYBim_oMLfv45;TSUfxhlFs!N0t%#Gj%m;q|qkEs<=MVbjw#0l45?ngJ zFdXNKR0|H57#+Uj7M@NM7^KM3hsikLxPtGDW{*q|EFXM_`|+ye<^=8Km~HCTe%WhY z?7g6-{5aB~(912kx&!vvS1$DeP%=3whfZ#pUg^xJ^EhB|k~y;a-~9AS3&=M*C-wOc z;{`8H6@zADcV_LB+?!;HSW(khp`{|pnC9J>UMGzxN&G{AY@ZQnbe^3rwwmFjlayEe$JIaQ6#x)6|p~nx!A?{W+ScvofoG(AK~1KM&F3 zZFp}Y!SeHVFm=_4F_vp7g)m!nI!&z=+)jglI0*VU9GpGwO24KZ4zDKv*%U9}PrLO# z%JopD*x}t)djeLiD{Mc5V~s8~h5nK`;9>Z3@^L~mjSrhoJ`n2Z_{nkXIQq4C`Jc3{ z$ZU8Tal$q{#JhUSldw#@-C?noLyVpM@1lAN?@lqauq4OdiqJ#Z-Tk&oN}K*4p;Y9L zGp9_qL_Mb@+m@$T{yB|Wra{MoiH?Z|7@2x5`g6flEeYfBFCy?m<;6$Gp_UDj6_s<5 z_OdJA-DflJKX~*j?G7UMgu^c~5Cht%9)}rq?Y9(Jj`}K3M!yJWM-|jqlBr89kvDT~ zZ$JOcxblbqm>G;*PvSawm5TUsfOyQ6ppkTwko_?fv;QZ za`l!+hvYSZk`$O9L!27(_8r#g+eMJ71_M&sSgS9s7imPO^=fCLVFE$|DL#nsQ?}$v z`4sNAE|M3PiNClA*&v+oNPMQ^{)JLWN==ylHbS*Q*+bwKc+&lcKAyriGICXz zTrYb;H%nA4KmJnVZZCi3$w_cl#b1h+ojsbxIGF!@qHuW4VGv(M(mvq02iH#jwdlh9 z;fMX3p+)86BnO>jMDX^3kN6!2-LWZS#7v|eFi@`$Bfi1_wEZTCqIP`Hh44Cxkgsm! zYHuU2VrUXAhErYT9mQgvd()|te8{D)-J28JWz7rPbBi~**l92C|F!+03EeAkTUf;l z(uvbAS|JGx=Jb4e))I_A-+F|*=+fFpDn^LDpPOXOPQ@GbE#y-Mz36I+-g=gr&`9y>X|`T`B)X+BqUO_q(j2SICO6F1~c?+|6At zlc~SPTkjg)yFRJmOYyC3QIqIHhr?sl1f7pC*&jFTYmfb1FRYY$wi}Dr`fD=!_9mOk zPuFKh#4m)1{eDB@mcr5HG*mDbzMJn{AhF`!(XNwKBetIOq~%L-AABR}=>7$qbk51inbsJ3S3O znuEYLR=i_qqy{8GqLsaYfdsmV0l@XZ$|5HbN)V&VafvWvnui#kSZG3xae;6C57ua1CJc z>YTWuGrY|{_cnv>-&1wLEQj*V5(jC@=U056u;Gmveqcm%!e0J5*=iW=tg$C(2OFb< zDl1~(#moDzBL*1?9k!ln3z54;w2KQP!Dns3p)zREiS|0#%@$kXl_7x&&F0$N{5W-t zhyoYs+q?2wW-Z$cIRgQMy$!Jzf*4i{cE5rx8%L$5@a^S0h2xmz->d&&yr}>jtRH=Q zXW~V@PX{yRN}t}n6Kqg=wQC$X6rRQtb9`1KHdNWfbsJ|M_uCK!4F&=mw#_5*Apy%yI+8TV&vjn7RU7R=93%E=W^tA_v}M zSBj$1P&zw1h51xnTlc&Gd|-KqJ?ocb3cRwD9i3`AaT57~%9JwAJZ(M}6pFJ)^T&_` z4*%*lk6%OvvgD+T4JbGQ`JJK8dvx>JZ8UXZ-DZe;ud&!=zM%C)0-+PP;&yTvptic6 z&}bNU83z+u)#LS2_kpOJDsWTxx+0rT?C5Uw$2yN=jG3(;h+SvDLSeQV@4#don*c&j zA0stI)Y9b!6nw;t9r4fmF7^^Gf!%iLv7c5+chmSg{B*^CFJygsRH-DWZ7l@pwg|W8+LJx#suvJW zcvO$xEeDX?oQhU~E|h|IXw6}}lXiD8Wn=Scs6#r#1*rGOj5KPYlRsuDN9>hAZoaUl|HzFpdmnfJOb>O zRvap(>?IGhq@-kTFLzH@vLW+o=QNQr-Yzo;@w>`I2&9svx#dT1y*Og`>)>C8&1ezv$PgfJ*SnTJRSs}#!u{a79PlSe&`d4Ul#M*jN^q; z`qmXqCW+Zz%?P&|==9cX>u&FnSAU>K2NpxIBhjBt7`z-oz{T|?iL;Q$Q_3yrN)8e9 zu}vsSi&%h?vevdV#lP)yBr`9fHxF2v)IJS6gn;gL(G|?h&-i`SQy=?!@y=4Xl0dh~ z3r!izzMl_q7zmaWc-MbmJGO^pO35x6-b@%ngM~*-)zvlLNSflaCazNbj&?pYB0Ce~ zhF`;;1bhy6>u(18aw!z`6Xy%z=q`~>%>JbM@&huGn34scD%ly$J3{ono|Aj$QC!ii zg)qttVXwo047m~g$%qM-9s8eXclG}bw7Zd^4Xy@(t@AcAIO|!#v?ZKR-fR8q^~1u4 zVhfpu7VlBGi!SlQp;@oYwf$%sNu`Y5xnSJ1Uj+{W4Y2teE;~>`)DbAMJFyrfpgpnl zBR_IC6K*N=}TG&BwdRFMxce(8>m?8|OPC!(H;wyyoBBuV!_WV!P%8Z3Yx{Js{VI zV0EoW;-|^a;e8lTr(o~$?E`WXr+t(?C%6)OKVm|C!x|UXPAeGYs}@ zWJjTV z4K93Dhyy^JE14cYFW#?~zAFjX+i^DH<7yzZ*3%V$4?od0o|E{kjuoBbzC%$Lk7-rhrO52c-%sYOfdN{VO2Vf@zr+cVtt)A;je z8f+8z2EETA#rv05lf_hjUVG@WA!J#EO(FGYYMQy+tOKoVK(4i}{P@rLzhx&neXv)vpVA3l$cYbSuaUVs5m%P%+29%W7 zS8T=|t?N$b?tA~lEs~dHn~wu~Tt9kSy2mE5ztE?&87S+n67YqXvaP)rv-Kzx(2p!b zs7Lv=AL;z|jC)F+a0V4DUf(_ttPpm;XBuhUd<7g=v1~T02?IjelZr=(!7+?1?{x&C zDR74-aT^|g{S7y1!Ae|gf1(?02rxgm7WT#e0mO_rBEYb8;%Go_cIS6EWiCmX!+#h= zDQ#jMD*;8X5G@t`j_b>i1Y;!!j2Fl5&tk@#R+PHXJ^~`)@T^R(-70$$H?7S90G6Tx zMl{Iv*Y!PJVYgL{tLo$xhj2spGShcw1%>bNox$gR$0hsW&d-fHx%Zix$eLk$-vsxG z$K7858j8mibK7#1^}CKR@~p}RVn}U0j4WU`dkUONZ&Arb^vPf(lBJe z=NTj+>5nZ}m5PURz^wE+hRhYb>w%{HsYJTI&u-+>i0ZcbikzH;Qe2tyf|0qt`nkBn z3LF9bO3`!&zb+uRty*K7NoGuD-GmzUVeg46e|aBjXij}(47y-)Vxbj_j%tKCf|=Xx zLQuy=o~W(qo>Ir4(BTE2iRbYlcB!Z9xOTBSQ-#K;cym#=dD}3ShMZz2?iMYx zwWCV7c26ct@$ZJ{dsc4eI$8#R$${DfO=i7CW=Qgg-$~G1CUs6$uf?g~(OxW}9o(dZ z;-n#)znULj#H5RhkQ@dJ{a6uJTP(4b;y77B2`MkWvv#8(9rZ4xb9lAr*+Up*QaUBR zdYd9HKID#px!k6-U33I%T~}2?THP@s!pAC@G3(K+q`|=z@(vaqw|;*fXYXQO?f;E- zcukm4lq9aRXqcekpw6Iv;#G0S+#zScXXSm-HI$C^`p}yVx{lyo_~n2j|FQJJp0oOv;TufT+GKjJoo~B7o;9 zri`onTxNl)8l+CbgH@9+{~{p&*95If+n(_bnjE0sjON8ENh21TU8%SxIjhbMdD#ll zPrPCjv*5B#V>l6qFOPAnR4#q&Bknw-GlFpS=^^L1rQFQJpcJ{!KZVWCQvNWAFnCOU zZaJhqNv|fY>oOIKDL1&WM4>TwM)M_e!XlG7Ku_`jR29T)g z!b|eDl1ddo%bz(cxV|J9>GzxVClHomIV@MbmHUJQ09z8Z@ZG=!OPi;zK*+r&u~W zzJ*b5dRJ=DAZnrc;CAtzcqj5!LIEIW)v$)S7Ec-;fh-Py`L}}l8Sg5HN79e0H#2t- z7o08#7@Vg#?YnjFsi45B=koqT1LDW`OQX+Ak(0LsI7JUHFbwzPxiNa~*{fPp69vIN zH{^ip%Bc?V)WkGYmx^<&&7Sy*KOp)9(maJMzkh$&Leo8PdLP6iKCHO~j*$9Y^)-?U z&E;|OjpN9IjiE^tA@n`EfzaXS}=JL4|V64K}-~@NCy2w6Zdv3wxw_g zGB=4z^wKw>>9bZ?zqZZ`1%qkxsU79uwqaq(p;j)MxiQD}#IqAN*|;#>+_?PB`y}6M z%IN?PRUnOj=q{ayl3?$bt=}{WuuC&jnfXvW7&aY$Fq`0`ZE<92jk?!l(j|y zC08`qk>69T8OeB0pE^+RtUYD+k&17lpZI-AeP^6 z-T8Sz8tNs#5W6lE<%c-~dGGW#8q)^vB(T+3kfS{Lrc5q9|vhDMkLPm zB$x-(mlftYg-%m|ag?mlkK&px+c%>9wz3cTOg~ciu{8H95yd3O;lFcR z-XFft-0!{pOy>?~E;6irHNAxeK6e=N#pCo7(p8~iSCi`B;#5LBTiqMnf*Evu)2bI# zv0o_LEwmoe8M0xF{@mB%jJ%c{ZJAmq#vpDDZblQk3pyO~GA?nZC)GR-4zU>g6`G!{ z0_rfCQb5h|SVP+5e>wl;dW{EqIZKdJ*U4uCQi;C)M*U^;y{o^>XWd&^8k1yFuMbhm zbJQlD_YFcF02Pg&hP3J;B-e%DDyY%lb8HJrm0+X-#K)X(=>!9OADhek&m)YMTY|FN z&b`Xr`6-e3^`fEH6WLnN@0PCA;HG)BcJPH(BhuQ%GB~-Wi8HoGD5OSq!s$KPRjO>1 zn}$5lrOGrkH=!tlMCDzzZFNq^@|1U&OSMKs$p|buk3@iP^m#-E3?e5#Jq?O=$1Lce zhdy!LKKqXp*erU)AHdCX>~?yZR1$^eVTz1SN}D_y(;$7tB>82Sv5IAk<^J}d z;+IvOcw&wcHeS723oCp+*3X@}_~%F36CT3Ia+wVLk#m!VPOmmgIGJVEI}wVRnoJEm z)13hyKIc45vS94MCyjDN_6pMqOvlETLCJam?iZ0Um;};a>&ePixG}kSSk26Pheb=== zkq9CDf?5AwC&gLQ`yWusn(FkLO1-94a%rBs%Pm{Q<|cOefVi0my-S$&t3w`j)$Ps{ zY0O#N$B04Y4Kj%;vOlt(h)hqhSU~^u*!tTB(QVJROrF@r?D~4c2Jx*`4?bk=(RU20 z;6Sf;Or06E7wRU!vCt_Ianxvwd||?@z1y-9;Qf31Lu4q7*GLCS5IALo7^O<|lnk#4 z>qFv2 zFGm9)K+&o&=y6o*jNKXl2clscQzf{iZP=k`uo^&$+%UK^D|a3T6F?Oc>Pw0j3Ukku zCV%^W96oTV#eGfe8#UeqcP1oZj}olLJ3Hg|?b)8`b1~i(!dDe7$-YhTLMtQ{C$3_q%fX!*wUKCZer8CV^LuO!QCVUel8SrQ%X++pN*8hS1;ihl34@~tv@ z@zm?Y6YD>WZt*KGqr3{3eIN&?@B;u6pw-l2)F9wEqNE|p85&v30Q-p(NnIt}0Aqs3 zsEsacoGo|HXp*`u_dlO}Y1UVuq#RVuX(WKRy9x{a)poP}qG`RnJ;kqHy;gaM&CITT zM&~74uR4SSpGEJhuk^X<>+d{yDv*2qXCXK{GN1osD`)&u0htdUBM|@z!l*DNpIz4S z`oa#j0Kx-49e@HCqt^H59g1l&!-hETGtN%r*u-{2s=E_x*oFwXDAtz0WZ#d3gVcUO z5Fojtw}Y-SDa2d#(9N}R!zZ2L7*StF8y2OJAqU+tw}=b1QLtrGN5rKNthwhh7uU+` z8oA~jFBkUjE-&kUK`}AldsBOX_;5HZD^|(m<8;56%X%_QvkTO5vD z5s-WfHWVaqMcf%tuv2RX3<-A4U-18{HU^WFsmnGv$GVWRiG`_*?Wu;2J=NuH(c}pw zJ--rcp6WuL2SH-ellPC>4&Ey6)p8FoA>I;m4uAX` zJ3f#T&#qj*6E*&1%c~!bQt8H5j2usrsBfD^$MTnWUdQ$oN!9TbDcs#)V z(foN?P97_h;ZJ(ZUS0v)q2XG|bZ95Q4}i@-*#GS3izfy+mc>5F)gTBJL0ys2AO!Ol zyJ9*CDuu5uYFeL^xV)|D*MYvGjADBN~gm11ZX z>5(}*SEPLJB(d8%T7dScc(BAIIL(#&k}p{=S+%xoHVD;kL4SznRUlA|m^b6q{+`8C zYJ2YY^3a6UvB<3;`NYr89qwSmBT8!WUAP|sk7UqYJq#ja^>+paqlBg7JJ9rmN~?4| zRu>I<m`*$=W|@@ z5?g>Zg87?fL!Oa@LG>0-{>c7#eoGLG_iNi#J$MD~m;W~T3D8r)Uk*Rs3wRxOyx>7R zbn6(9eZ4!r`t=v=O_E> z_87*oSjoaY&PuD>av2+a3;&xu#4&C>dJf90n0%(jQ9Ajd{4W7FWcW*l8Z?~wCRZE$ zkh5`kcs(bdm>?3jZRqb1$#1x8x|VRYplWJbG^E(W1_)3;`iEZ`R8cHn)m5^ZYAYq6 zSNfSA-$5IwuFhqL$IHdDG${+1+_3iOoTK>a+)-Pc)g+1r+Vi-mDL}im8WRFAKxYJ= z9v$%(zG`NkZ=#yS&M+v5r}BJRvcy)Z3{HWv3o2IGpHMI>QGWt|S#kpR{M*@ZSjKyrY4tBX z$m-E>-(4em&|?ATk`ajB-hHreab8s@I@9OWmCjaRb%a z&FCMcohb4mB$e={$3fP=;xMkFI9QAo5*o4375Bo>K5Y{Zd zn=q^OVrs!HhV#DtxMA~53NaE#r2riq@iKX_r!G5%lx*`f0*nzoiINJ9j5Hf~E;hfV z5o=GUITwnV%XcT7=+k`QbRvZCwC z%Ak&0x#U8?*$JK5poXiOV)}eT#dBC2hzu<4T*0yHp7QinkGm}_Mq^K@F3XQ z?R4el0>GvU8~?*l$_3WeGyhRiAym8hwkRuq_k8zJ%w<6c5}J$-3H}e`+i}@jGIvpl z)felkcRF)2CP>pmU2J&iYk?i&8~Yqf-N%n9aQ%&*da)_x`NqOp5$3g`@%0Hl4i`a= z;&hWP^7Q@DA-7$`o|R4d>9;EpgnTQOim#d7=lv&b9AUi3G4q#6D=R}(Wf}uJHxsVa zcRSrJ+9mtaqzVXZ5ZYr8jYujeSTFRHU0-e(vMZ_J>Y!x?Htoh?aPk$p$ImRrFUD`| z8)5x)Ouw&+crmiJJ0dp!5VP}2rN#Jf<%iR$%1!524NYS94%0;msM)>G%#-3D7`Fr^ zH_h5OGS7*vY{>i94)l;OvRks$VLT*H$}7f0e7{jeKa%hMM~iQiCnvbc@1Y0uNt@im zKj)u`fL0Ya!=eS1Dw1Uk>5#7mdSiqJ!nN-HNmV7V?}LSR+}>I_7T6;k6TW@}9qM~; z_n&Z+IBe4 zqz$iKHvQh3heoOMv6z|qonbuF@T~OYcU$ooh%tNnxw?BZ~Ot{pJ8i5A$NEVdv;6p2>nE9f zE{MWoCEoO%35@k)+UnV@j-yC8B7^e-pSX@->S7Em6eq?u;VqF>3r(*|ecf$SN2$}} z+8|2j@#FzL0Yp=rYd9QgC1HRk7sh(RPF*TQ3SUc2H&1Q&R8u4+6pEsQV*`E3Fqj{s zJ+DhMD5D-V<*o!P8k1y#I}Zp*1Oo)H3*C>=<6m$mtSTZ(W39>eH(Nr+w^J0GhU%8g z(iql&4Dnv>fzt)Oa(*^tEiQ+IvaEra*BfEPfngYjc0JOs97sM|n=%~{S4(B9e!bc+ zj_BXo>{BD}rCYY7G@YE@ry0HPX|P8U3w%WP>M3vsipAbBUCx^-007W#s>%V_A!k<1 zesMrU=lYj6lyU4>;w-&j6TN&nmx5fF1GBk0Ck4r493Ixi>8g#m2__y~W{H>cV`~DM z@=wO;%FOM;CrVSY>+f6id>;5f@vA;GoS7o1b0_HfwY*%XPlZD-ZdlA{j_D9a0-zv3 zwU@=e`-bARs6c#8+bdB_<}H4#nUs3>1~KY>gJIDH4l_MK?Xo8-X$wYt9hj7qC;uh* ztXPLa# zrrNd?+KT$!zh~uHy%s)IRay7Oy0+E-3BPmrxEI`MBQpv6U}sykeTd*vhJTS*CH6ml zyC?jeFfu$zy1p3o!E}IPt=?`;biNo;m%lGok;v+nc@W4?Vx#$N8i!`F>9DK`#E_f! zie?57*IZ%##M0H@N@zZc0cqVTAd9-|JubXnYiGG%ivEXzg+C|0#q7bR6;fV?Hh&48 zT~b_BRBV*E`srpKw+PXiDK4RO zQl3gX_~#w-gw2)!N9&zfFUD}bDfkv%x(k3j2;Y2#ar@CaHD=_nAY(8+L*$N~lee>^ z49;fk1A0IK*<=0QM76!)M9s9q>)W^EDP-GE@Y_vBV*oF8MHc+U2}iBk*h5YXvdq^h ze>d8--!g5ts)vME)SvjDRNZ&IJ|N9(n&xW7dFYxTb{^kZh@o)+a_9dY0Rkqo4(qS( zLkDQMbv+xb66*Yqu2&;P5-l15uKdWi8baxzjgdeRXBe9XLQ`Vr&=<67xgxc-!qO`v z&>i`?uX$C3=$m3L+v91zqcH100B_PxKT8UIk*P=ScPEB)HV4F9MFn;Oe_hliqqx-8 z`(9P>xKT=EtFog2pYrUucf2ywl*jq;H0Q6NFMC>^W#xUvDrDg7iv2Z(yr)cam4MvrbmQ`O9Dpvv z;)!-?X|d$)KlvSpQ?o0ggl)glXP3iEQU14 ztmi+cSxn{7ZbSXL!a#`z|1hnl4bu~a*F(w%LAae#p2-9=F8%_bEDePg0&NIY1Lj_C zvu84fyovgAG~hS!L`tUEAg$_hGQBhbV(=V|HSDLAx0mT4@6PX08;HjRSdaC3LkTypA|Yl?+$5D8KKARFPW%gdSrYk}UZb$~}@c z1ZsdOkI|?(0Di(_2(5IaYk`{89J&L5DimWC*UA4ppUf1+$K8FOE*AE@A(e)alTgW! zkLo>5_=Po{5VkRLHeV-;sx~-ivC2lRJs9&_AKQ+s1 zU}5;v#H>G%7568(loxzO^%!~b1SBgTh%B-bzM8$*ZQQbmjN?{lGEHG+rz#z5Z0eS& zF9kf!*qb2w6&2n1*oj7Ln{_e8uAit}V>Ht?oPe;c#Y)$caAbEe=ZJ!0R&(odhlX1f zf|HZ{AYc3AJlDY9!2sV|`w-W7Twh^7fBO_==B^hNAMDkg{jeV(Iyy!=vT7geRT8v` z*2(ME{VzCTekRm3-|lRuV4K%sZFsn*A6cf?IY#> zjayP^z24S~zUl<^8F(iAF)UAsd@~;$R?ugMNTd8os}41TFv;>k!nrWK4$GtbY&vp1 z)9B6w9k(+ZsqRcVinP>Yqy%JqglxMJ{fY1{lNWl>teIH8!Y*pfWbuN!%6scu=}X%; zBE7xsw#M6L@85ZPo;LDj3}~AOM1$^>PM96f0mPH%h<*xwGWpV(;`909(m?KkpLWlmznpLV3h9*fFVj&Yx2ljWeHvX;e zLME(|RDF8o=0D3BCFN7DG80og&2x>pzP}Wx;Uv8P4 ziC_0PlqLVrZmaKl)u(s$jG7tKl^GL&K``M(dHD&)_4Xy)k#xf4_)PhlyW*MR%?8`jrrVNC0MOlcnsM!s?x(OUh=A9##(BDE?@ezSb5MxflDE8n!pfm zN_Os2vSpwybEGFs%r@;Mh3!b~Xw!Ie5z}HXo?BiMTldaO#)M>ceLFN4wqzKiG2;@0 zShnB?=lz$&R)#AJEQ_n=5tqr`U3S&)xRrFD#n(_Mdz#>aa%3SI-~7}@bnqYMYcDf9 z-YvMSS_&=b(Cz^+;<77FN^SPM&?n4^m-&2+`_AXlq@7KKb|$T)SzA78+rNsXL}#zS_G~FFG(~9I@txu zPqAVz`01kve%a9!mG?KmswAY99gu$-nbc5^A@B!hE&|i|3JQT2OMip+nX3FZH5fCC z6EDLUcdAw^me%oU%x`5FMP`CSRyl`L>s}07>EtL_D(5b*65`!|rLGby>Tfm#z|Gc- z?HaL0@Ie|HvRwEtClmB>+dQw!|2*PKEpVoI?U0Z*yr0h`*u+ zz)(-brp|_uM1g%|)X9U;{K&T{;Y?Ek{KX^eEw$zMoBu|TO*S&oeV%aFA?HCLe0c2S zek3l@Da_|iTMlX^nL@RqP#iSI%VW$10bl-}&U&XUy%R|*Wp`)ruDvhrCk>M!qxH<& z4`zl4`VyN7(pA~hc}&Z8APy!freT%kCOnGbm{x3u#i?#f3UuZ@+IC$c^lG;{qdAz# zRWHhdK_iB`CtQCTs|*v&;;L8k)XPwc^!XSh$d(D$c8KrNY1*&mtuXm`F)Ca76E!~> z7$_b!A1n_;BpZ9od6#s+?6sdfzdDUpKVqT&Kn9+_9Qe8Ncki!#iwN7!asgVqk6F>C zA2@d?Fe;2sT)_tP=>Qy*&U=wu-o9FYdnOVE!+s=tvotfABn5AlPxviCmZz@g=xO#| zCRy|gRY)`ROEM=9bft>LlFO!m?Xz`GkVT^11zt8Sd!xz2jvE58lF!1@5p`*Yue0&8H(l6O(Eg|vw{5Qd+sEmLxV~T{rN-$2h{5NF4 zmftea`1-NopMYCzi~EN;-ox0u7HyNWU`F@Ln-;>j%24Zv)4Ur=JX7ewg6$VR^Boj_ zT3=A)uTeFEPMFju{6-%MAB#s`=w+H7x{G~rmg_4iM`=9(*w{!wvBdF#V5!vNF!c%J zOLOmfS>i=MQ>0yEc2Q(bw5mg=WVenLC-Q*9g_jm5GyldN`U7?wcUsut2Pd6@%Z5Fb0NGDJ4u0L&ws!aWJeL~* zni!z8U+jB&u%9}{5ZBuzZmH-?bD<+`5^O^acWW|iF0xgk@sp1m46k$cnoo*Rsh4&S-|ifJ&9#5QWptSwlsAu4$vq`X zTm{Y>*bWgIabfft0Q(fq5ZOr-G%2Q7-Lwr}s-m1V-#vfdV)t((+|J0mmf=TyH1{}7 z8Z)V_$U6Knd1C^P1)4C>-WZyp?a}~G@6L&8eqx`hq8>Of6}b#x?vO28D=;1_ClDH~s<9xHZKBe{?%CCg0_W$vv8%pA@ga zs5su&tYFP|2b?`mKC65d%u!c-?wEFtw3TKXFt;PBpD-OF zZel7!7h)}9?(v~nF+g9hxhof_8ePzLIFHU;5`1E3>l>|Lnk9ky_iqAbX)=lH53Zg; zL&6BA<<)h|&9oy_4)!SMIe*w({MxGF?@n#Dpt9ZxeWQ^+PaZlxB>h}+fN=zKP-9)* z0f7{v`!m%i_fm9*7OyD2Trk0`mt@*<4>|j6i?H*9pk1?h9h23oskY z5ko|=CHAxb1Fb+(zf@4eic+>al##MXsXjIpi)68{S1guw#x=?+zRt8nfWlskr(5eC za-A=-8$)FvkPxzb&fc3f7l?A3PBe>*k2O-P_S}%m_jjA$nGFzPr8*@bEz*SiVI+kU zv#@tnMq7Af9wX0h@U3lIB3z`&k!F(orkuDw`kU%Yky%^If6ERnV208V2piUge9MaQ z{6_`i65+6%KZr#RD;vVFxIA72!5qD|GZGyp9!}klkY$aH4~`g~=fY+g;|Du=Sht)M z(ZI+iEuD-4NW(_1e|*^`&C8*nE5}f*pE0`JEzsgw?9x5n*vm~NE-=3G99nu@dEy8u zSUU;|-*q1K6v1p3`??a{!LKwrq{WLVKy9f#LB*+RJK9`#sROw?J;%zZl*w|GQy;DJ zkWz&Q7NvD9L?4Y?BdcKiL-(DX++PzZxnfQ`C^*EHn?%TKQSP;&q&^gdY^hpUD(NJT zQS#CH^tfS%!8wDyBa1}RFaaaTG)#nQPOPzcL9)?Ws_~{vgy8swU~~67GWUa0Iiie` z0zaIR3DY)&1q{F7?83Np1SzGpqo&i+m($d*gJZB!@3M9PBfV5^B(Tj^ZEJcYc4Y?G zdHHtzFUVyn2pt7}SqgE@llq{1B=6Fvo7K6w_S8@4J#47Qln#eW?SZmJ`z2^nPskE} zpwo!CDh3CXh9JkwozM%#U> z&5bBzv+=8Y&CItd45$slw#R8+Tj>wBW~>ycDNt%3yddw;An)l?kBA!7Rb$|h|Qg=_1;`}%E3rTBxs%0+}m2iE!l=ObB#Vd!r5iSmv&m&2qIkD zn+gsq;Q*x?mbDv9ZJ5hj%4HK6k}Z+GBI4509E}~8l8x!KKFZu9 zKq$CL?sr~agOiHmY>o)slM96HM5VpZ{6x7LH5sq7Yh(n2mpQsN2kQPkh>r=tBa%rP zMlN=+G2{lx6iw|j0@IbubDkq0UFET48<%mJ^Ddc#uUXj$$B}XSI^Ng{4LtiDO)b=b zl9D=I2diGXA3D2;I4$dp9&%1EHBzGF3?ZBQa+*`Io0%yA)g-5C?1#0`qLI^~>FHF@ ziENgQS4Le>0YNDDVA{t8KKOu$Jrg~09iIYQZ9+H)qMS& zTZHgtVWs03)?sGFT)1{LIK8is0PgDnB06hY!r%aHf^A=yh4-K1%iP%O#NSR^niWff zJ@(p60}&v_D@(ykDB75DX-`Q#B|89}R#sx<(=cmW4n%b^@|iH4lAzEII+l_URpqji z<~W|E4(dx__dEGkx*QqfuKi@m;C;1?H*|)Cyv2eg!D)RdoZ?g>w1-yWooSHzle8sC zK?IEud+Z(H`xUv|avXOm@4|F($RH0OK3q>hjXsn zE+fOF61G_$(8?A>fyM4^4$J|tX71(UyZkHC4jqod$nGBI;`+*8mJ%Zp1+DJfD{@gAM% z?!LXDcup%jg?MK-vCp(X$ufI0?_R9Tx)~E6VKO5sa|lX1b%p{+Qc_f;l23ZMj~;ew zPZZ{NUMPPYMnbIaR4LecSdNn--0WL~Sqq&VL?ul&xPn3)4X6cq+9~b`#K$|?GHlm2 zx;K*C?h$c5H?rETw!3h7;+7^mZH+N;SVKiY3RCjQbSp%audCG7?gj|4a$o>!Fyju~ zOqtrtbJ{Y3;L>%@UD^2MHEUalXE+>RWcNM{Y`8}DVZcVf_~epCxPesD8zel(cIwDD zXS=9Cp6^nfZOHOpNlEk{9*R;nH?jfVhQZ#xl{zKO*%(ZhP|GZpp#Y_q zRnm0f^y=(KeVz9D*S#-eqlxC4ExB_OMA&VcsweW%>NQ_54KU(y4U%5xeum8&hJZEH z*2hDqU83A?F`Hev_ZfzddVUm?$|X7uFk4=h2_Gc)*WId>uZoi<+irMuxc>mZzS(U6 z61G;h{IEeODoS>tqpf`VZ&NLiiSVpUUfe@cEl9WdFD5dZFE#>7RvrL_AtglY9>pi` z@63#)^O3Hm z$?SFa_c)$hY#WB6TpVdEH+q{Q;`>WVT3XR?eb<%}kA!rjJ@EQ~2Veq4aWO8PxC~rn zzx`e{t&G=v#txwJJ8xT6*6Zp#BL@>W)*Bh0y@uhDmo$2jyCY-P{{WfBgMb#&+R%kZ zd`a_2uui#qjm)!fpAqY7)bdF~jmS!or!-VcU@Y(9KR?2;sei|>_IBXSzxi%ib`0#UTDxzLcpR*}?{X*&R@4~Ms7TiiD+ z5W-_Dwz&QxH?)}xTH0WbO-LtEy=*%Cx6?i=FBjuahB*V{!OJEX#gMhl?*jLLX~gDW ztfHMjf!1uoGG^@%-kl35EVmsbHxs7UtZOP9TBI1>#=_Yo6d_h=X0=*BNqJzQw>U= z4tRNK2yvjKvgcpIG*pnFTPP|i2_SWYv(=$La=#haa7nhzg432NLT_(6!|^1&(J>29 zQW$mS)vl&OOJ!aP@R1Q5|HLqvAUYqL{zvqiugzj&n` z#<5$eob9G_w&r2YX)_+0L#(;yGPJD;>MdxM0;KGbqI>tNk-|}pz>PjN%rhuSki>MK zIVHxB%WV~?t7NS^N{RXO?0`?gdW>DIFJr>!^z|V=V~9N~PW$|nqBakv-WE2yJ6`v; zOYb{148pCTcU*6K+e*+9y0}PtSzv=RM9w)_=RMg5-Q7ilT8>(DT-?{?STFA1ushLm z+U}#>?F*1dWzeORgr|MH#Hf^jm3L2a4|<`aByvs0H19QDTS*EHC$MxK0NPWtrTku> zx}dIIpE|r$r;M)~k5u$s-+Rnb|nlERr|2!+-}zG-!6a(@+ZQKZZ0pf zq-l<-Lx=$(O*0}KRP94SPN%=cR#(Z_%*OF&$e~?7Iga6OX9ML+L`!RpyAL^$Xc7>G5M#y5Sds5C9qts8;|YjU)S#IQLUafk zVP}!^W1lM;3*~5Ij)VyV&Xz~{bOysf7P{O=4Fp_22^Jb_5MiFz)sY}(N^Y9~Tb_=M zakE-06QwCzYs)1@tPsTOtT?0vdD0ngq>uu-))GM{uml|)`tG2$IAD?Ovhu4ed%Kbv zw!dIMU5%I8^KXBN2L9<^ItYI`Nc&5D**`wEA!wwKcT3ND?t zuAma5q6I_7?NW0YGi*12rL>!4E0=R|w%2yFS*;9)qFZCBZlNx=WP6)VHXlZtN|unK zj+0dIw(+?y9N$Q$zwnbyH3~ z!gBIlVXW(jpx?5{L6;`vi(SDYU9C3v5~<3S7v9qHA}ty{m&2>U@Gws%Ckc;qxLjKr+(R)Pj(!%*E*A}m z#7gXDSsgTTi6V*7KOkUmaV-ZZS96^5cNW3$d2ZtPd|LqItXg0-KP>avc1l#_lt^NF znrL9wNC#9) z#HRz=Bw&|LQR91>@*EP?Ig2}YLhD9Ad3|psw<0vZJR~8+6*yT5_<$<)*JlmIq{PSA zV;>_i0}f&|*0r&+7d69BS}!OrcXu^3<}_&W{UyO;B;wM`ERMzDqQwcKk*{>{gO16S z&;kIC0`eUTTO42)wT3pn);yzUSxb=FwYz({s?{BZGRziaINNO}uu2l&p-NITp|1Y` zKfkK&{_f;R(dEjT;|NlRLy}!nDr6Ft)8%Vl!499iba(H%sW|Qdamz38>^3VD*RnZm zwr$qLp;6wHNDj%4{UE<8C9Z}Tz4$xL}NM4vbZ62z6FEAc=CFyav6P_xvhJyJjl>}o9cA>7_VnGvH# zL)}Hkf>K{7Bxxn@^+T@v+V*$qYmC@|o>?WHJiVZNQ3&!fJEC*Ct@0OL=|HxqhdRBq zyid_-crOmXN9eTCW1^lH@s2$o9mEVz62itO5@B$dNpSAMU|TkLBx&sDk~tyFBn_5E z3bBaZaLjIeo3z_azIENy&$qhNruyP0H0Ga%W(K^a4>g26?=6o|v?PTBH&qtN##kh* zM%|LxxhHwN%lw$?#1{~It9JD%rNw=@3X-0mFu1e^%LAk!4xzfVWMs_s^gNc8)KACK zlA@#7bsYkA{Qm%c$%w<~4|PQuk&a_t*1g5A zBi1(Nos{31!OU>%u{VpIopBv-EvT+Iloq8l2qnhK6re|Bl6!iAt=BZ0hCy+}t`pvJ z*oLJLl|?SBOX+>&A2kW`NbE=lYX_vAY*afe<;&h)#g+xoB78i3pLdF)*Guj*+hNDL zQsastBjZ|>q3ODpZkc2on~<4{YZitWVV0#M%~FvqZG`l=cC`fTq$ulM&vEHTV`hxZ zTZn6ntt91ak;k3+b7U6q*2ddlE`7P8$OM#@80DOP9|Y)OWX^UNO)OZ6W5PirorY}g zVF=pVcL-rDUe=L7F3k0N$>Fl?18_C0!Pph>s4~p?kO6_YC>)g z%Dc&$;%*EELPHEC4VK;rE_`Y#D;>&vk5k&IRJ*$*GK8(QZNa1@Y!EvQk~OZwd+*$j z$FP@F(#)p7L(4}>76?k)J@iL$qBeiW_2=SoQL->Yi?RmT%t#nBv%ii7qz7oozhYHPDo+sYkD{P)~8b{ed4!^V4yFK_*babn7OD z!^f3ZmfX*|XQH_B*l^^G{Na%90L;?6M5b1ETSHi8c4o>SsCvRFUQlsbg*q zqBc)s-2VWEuCpuVj7rnS7JD}zTVKa>7YmH#FC*e+NNlIceTAt@4LR|;6qd>cf_{Rm zvh$G5p5)0BT~qSpzGS&2(vs(1$sjAa(Ne!xdO9DCZ^gu642i6MXOMq*4YMo2*c%|8 zdRRINUOW5~6@{M{BjSoUn0nQGM16nOQwA~V=30Bu(hZGb@3Mn9tl6tqL zYkm6>zWe>UuGPyKzhlwuw+oC#Ly350Pr!UU+P&qZl9el=Dk@KX_D}|)_ZzCEgem2f zEjmupN(W=`>?Do+zFq!izFfRZyDltyjOj124L(d{JnN4lIuv#i$`(|VB$NGahW`M4 zZnAdB8X8O79MeRA&@5&onhghSNYwSEiSW$FG`x(B&R;^@S^y(2EZ4SR@P94Shs*@c zdEneY`?6ND8bp4l99bu4qzx-$_WaLnRP&ET$Zyv{XO|`Yt+!huWP+b8u!XD3K4BhS ziaL4^eT{nT)TwWoBOJKHx>~2Dsd1LCEnz7xBd7$E(m_tgrq9#&MeO}~E>hDoG89_{ z)@!azyGdDTG1l)}t8|0{QaaCml%4PC>Y8rMIKL@l8V$AGqu6{J1)3vp4!e>9mIpcT zmbIb89EL`hSvD=fz%{jEh&tZ8DVqI(NwVYLoHFJ>Kt#nvZa=&SCOT5Eo}{#t9+VED zr4{!ZI@lFY4Y|ikl$L|ZUv!Y6-9(KI6rWGWe{uQo zlr*3c^GR6*C4IJZHaEJ2MY~Fdq`282A<(jw9oX{M4uCHcsE zgR+rrt;N-Q&Xly>g~Y~PZVq;Ldd%$GLWm6_^t;MB8@&}yvB$ecwg#K>nt4y2kt2PZ z0YriDpIr^>g86JfdGxUF=-=z7RS-9>9Iz{xt=&UzK&YA=i-|4>sut z3Md~Ba!L6T6TNSBGFoF=W=m!oU#yLHe`O|ecE+>Ai; zl3P+5aJkddt!YFJ_U=1t(A9kBE_Jwbjq=ryPq4zq*N@{?tYHzYjJDe@#k0g>BXV3y z64+FS(H$#sS|q7QsQ1~cd>7%1i}_*(M>8Pit6mb#p5E-(Oip{tlRfqj;@ghe3o9y9 zn$XsX*zZ_b@o( zaDmNmb9;bD0Xbcz(|5~z^IOwHh{Ir-Ug*qNm$ZV*e1|D(gJ=TK32vpX?wSUUi-C)y zE+#aWWW3ufra}u!WbIon6sbq3{{UQf>Ae6e<}~YrD}1Kl%aPt~N=gb;jU?|$KA`J+ z{r$UCFWy(UDMYyHj#l~-cpcdpHok0tUL4l$CmVe9py3vWt|fJ-XvH#}fOkM-Dd;Na}4#LO@zn zNeKaKN{6BBeqHq{;Y~zknQjC!g(>cawzQ6?TIyHBvNxcUqw?S1Ri_s`b9hU({y>}} zzUtEY0^EHT_pQvRJ1Q)f7&~oJTSL*%;L&YvB|&K;W6(nS;UHId$0ne(fOiGQS^y0w zawAJzEJJG?=>%_lVeBtxXnc;!=A5iy8k0a?c4gd}wP>zen!Q_YIeRwwYKqjT@aCxv zvfMb%Ndu=uJY4GP>gfq+gRhB570&J<0?y^WRB4eej!m{E;@e0{oG{YiY^jMMDOZ;I zI)GM+fFS7lioDBo-wL@tGd~}Wi zsEr!p$?P5yYg$8I(|JJ#tIG_x{G`4pD?zN>=VQx72p)iy{+nArIL0903t z!!g-`6ST3@67WNd^16w0bG4z`f(aHuvpwL|*_#XLabYc666%(t)~A-)+3Ij4e*k;$Nk4c$N{iaslkf7LQ%!`fKals8 zJQOJl3fWfMNkP)I4v%5?S6zPU>&}#WiESt^s7inkdY0Eynplsr0+e;7dV1GAs@&7- zmRn7=Z7sC8wV~y*f&xyGv9x}v2SZ!#dm8IR#+hR+dx>N7Hu6=9^?(G`btbv#NhTG+ zk0pjX0gkr35vyz)b!tc;>A2<>G?>tmgtU}H^UJ9XC2lTTx>5L)G|&S?59_~Ye)8UB zv*eO?5aL=IKtg|mWTXH#b`Ug?^aEYJDso~hXwSw72oI71L^$eJn?1=?hf&j@LO=m~ zJMH?XQl4+o{0~o<74J^@fotj^MJMQ`0DF3De&1Tu^o^(d7l^&YcbLqqX>L~Lm4{0F z)yrd|oCHOfyg1a>V`lXMqP=5gy6f^(`jn-7aeAb!f<1~jjmqJM_@K=rX2%w{;aH^G z&Cb(vqT68Xf`U3u^|A>SFYxRXTHs&vc`W&Y))=445?E{qDhA;*L;rh+M8~ABZbth zhY&-6twAALQb7sU%~O;Y96B2zCuDvHTKONAsx=LsxhURBYhy~llOl8_Ryj& z!Mq)Kyoc>M@c#fWhWRrvP8;GU3%4~I-FlBPQRHP2V>X!VT!OaSj!P|hYMO$jC@i|5 zwS?^2RNrRjh{@dHj$>T0$P>8(j%^tPcTKV9rd&ht4751pC8gw+P}8mKI7^)%EnSYg z;D3hsGb!LsP{X-n2$=jUrYm}haB@2iEwo%M=yVD2A3{${tx1Nb+AX*UDo`Wh(H@Kb z6#2J@{6+Y2u=B1Us|(6jFy+%LO9nBxS#Gx2igN1~YJb()TTV2$5|sc;D{P}_B8aqv z@eR%PUJS}PYk{#$eKvM+xs$9GDK_^TLJ<~7N4&8qv?V=hN}6n=ML-h1!*z@-VsVw1 zW6nvK>;)UdFy+AqPDcxL1wPpX6sa5P2V-Aasy-*Of6s3aJhhdwLYcW{mzNt-3Y3!A zl;RKpI|s5!`@4N=fvl;rhepDOPKZ8=PPC!iX#EHssEXvd)0D2Vv&(#2PF;P-ZP z$E~vN^49QF*b8Q84U*%7$xM(^k-ya)$7;P^Sk1V5#itxM$k?t%_ZoARs>cr9n_zB) zn-*AY?372lTVHMF!eux-)I8gf0*CNt%vR2^B;$*Fsn=9ir7NhFD5#UYcO>kep8b70 z{Hi~QMv-vIY_`bLQ*3hQJ}ea67P8ZE1eGl%*OoM(+6sO$ck=mB6vKRu#iq^V%#rUm z$xn|w^%a&|Y%Aa@B#=QV9lLh>v?RM@;kVZMyO!E;4gAznK>3f^Q#T}IQgIB<{f)`G zM!dUgn&e}1i07LYUrN?I=9lPFuAPEDN9pS&C0bXn!?F&(V0Qd_>%ZSbSp)C|l3Vc} zcH{h=>F-9oGnGVU8$f>sTU*i9)TesE*;m8U)AIZDHx=>8ad#u|Imwy5@^WsIU}J$c zb~=x{+FNiTB%y*-6!%VnbR{~`0G*oAbAhs!tlWo$GFv@EE3~-V>DhHFGEg*+QU;Pg z6Z=hW1>;`vc!%IGIlWouIP^Id3v>zA(tOYpB^_&8nju}%riRo3DcSBj(H5?p8^F8+ z%2-BYl=3f#2)uchK#y~mV#lRgo|c=G##welOur=(8}idJ#D>tI(}g9(?0VI);hdS5 z@it_}2lq7tBdsR&43h$IF5)gip=NdP4SeJLIKNIFkr zv9a5>_pi8osuZR5E2#GDLHPmeuTPNJ{oaS>MX+}u_=v)D&jrIYgLry=Q+=5d!;si+ zG7{?G&N1I{a+zUESz0=qSW3E4)ARMLPvcL=$NlEw%w<0N&Z+lgEdd=s9fW{)p`ZX8 zsyU3`9JX2vH)Ox=&&y@w4aK?ct0)T|XOvHtRs#Dgpr`_VMAc}Ik>j^GtB&*NVV0Qd zbII5$Qc^U65%G5<1NHjsQ4~Ror0k0-kA9y4E+Q0urWzg6+Cf@dR<+q%PX0&G{yev- zc0%xcTae7dvZZtu_*iKu*d0S@*(xW0 zGg6*Go+GwPlNpB|zRz+fke@PQP>}T1B^^UyB_ND|^2fFR0F-#2D~X45?*W@E z%aU4n%f{~ZYrFvsypG6wzRwUJf|7FHf1GfeW@t~g@6`ZY^WVLl$`=LGMViT@d?HrCvZ6#4T>t5yV|ZE`)x8~r4!-x zq;N%h?kWd=8ktGcGm zlY`$cGgFu2*8zC4PI*E*S6hsUQdb$2hnz~&FGx8{z!l z56vv8z1-tko_@-11a-r?uiipd+l3C5CD4{o0O{9}>E5vd&GK!ULk`=VRCS?faVlt# zt*1pnDjmUAk8S+>`uD2vZ;##a>!X(km{a!y8>7rqVbu9W(xMivE2H>8RGsLZk|>H) z$um5&GQYVxWzJNK#nMwK+Fo%gQ|elfmXNZ4SFOZ=NdwTGRaMC5bAN=a?;CBbx%+lW zBOS5kZMAQ>Dr9)k@;Ff@M`CA6lG&6ZPeGJ`RpqP9Z))i!R5oH`u;-l2v?b0bSNyiL4GOHH(iHz{t+keJM&EHuJhdB;{8 zZ7D)pYp5k5DN0fTQYedK-wiy|7VBZ-9(=dXMW$@E{e#Sw=r1iV-i5k*(xTsGLkMl3 zcL=z;i4qI0ID|9ZvH;wn$+xGxK4g%2UQ~s>md&`thmK=XoJ5O$NrqnxfqlF@`c&dk zD$ywxIMH^?gZ|1prxH4e#_xo_Z7{L995CUuL6v{JIBmC^scI`?HPwjFr&^^=a3r@4 zX^w@2DQRg6ebpyLN+$mRE;uZHMd6FiImiu4bCvkrs8$SW<1+6f-IWW?GgwS_z3<%K zhb=a}$Rw+&#-l5~fdpLm?ACn$c*)wsA1Gg9uvPq6TCTwUI#^5F#`6-a4NI+_3#5nwP z1#6+y2Gi4_Q2E?`{CwJ6>mx7bwtQWw7pZfdQ>BR5?Uq!Qq%0K_mzjB}FKi(zS{2q3 z5_?e;KQQHNitzb>ypr!^foxL{=UZXOa6oU|x|b8(n@ZAvlhp7XAZb0l4O#yHh`vF* zaxPP9yWOrXToV@3h%iflq_G*I+4A&MtraYlk`HpKxf}E;_sEhj3A3e~b*Yk+?Av5z z%V0%w%km^jksS_@wnAM<4=<^GM?i#~TNgNER?K-=i`LttcUJFK>GvS&5VqFTbw3%n zth|*W06Lb^qO_!GD=Gx+Xk(Z7ZuWn;EOP$<4c^c91wpAwR+`gFq`5!+yq*65vfuff zdaG3>-LTxA?QxMl+hv_H8~#_5A%;}8-13=ehdNX}dL3=V9eNZfe5*7PM-$7Wdn0LS zXfJSRF0g14MVbWA)`2?$x43=-9mHmI96tw&$4uDFu7?Q(xQSaBdl+OaiLY>Ae2i&v z8AG1XO}Je4cj?)nwmZ=zn6JJs}Sk2rO23CEGkn1J;Po>Hn)Tsz6nE2XS1_OgCLD#`u8AD{T? zSkDyUpwZ$OAbK2B0P-B);ucT*ndx?y=*J%ysJ;=8A2<}a5~l1fsPM5&J#yBjse zRMdi0Y*{2k^o@|I)*FmOs1Ez-Q^8-ebuL$C6X=gFGmd~;Q^{q(PJwPl*jV&Fsc?_m zSz%~&(KPH{(0r^YFENbmH0Jl3MIx%k z`RoqBR6=AGNU&YxTc_&XB^Ilkme8MyEY?*WC8qv86V}LCC#UtlX`)c(Idi;9VKT^Z zFytk$>WBoWq$xTZ(LFl^eE0Rx@Vpm+whV4Nh`$qw#J};l%vp;R_fFW(J}e^!K`3~M z@OB0^27%eJ&T~U^y7O@i8(^Od2z*nN?$=I;_}zO zriahI`gnF{Vu1T1{@TOLH07-dNDE>+Y5+GSX+1t;sq)5;Qh!*T*Glm)F5vt2$5A=C z1xzsNkToDEbR|hga+AM-DB8B506#I@ifQNKGT`RJVp|(y?IV!1oFtk1gu@^j7}^@= zhdBD-%x`tYYcNGC<-)o=OCo$^I616*P7k&c(gtwm*syk@K&5xq^g*~Z97D-m8|+rq z)4c|yDJP+0M?iw274sn}`c(wXEef~Ka5#d88+AcMZ5<^CK}3ygo%TOEp704XrDNtv+%8$Up6yU6f9(znF8G}6^2OY$Xj_da4k2v=Y>TqRxf z4{dDv*9<+_d}EqT$J)9V=l~BL<`H1nb96VzD@my94RyY_YEN*6yT^XlF zO^eD{L9w{kP*qIfZbWV-6_`YoE$LUs;jsoRiBb>`Bz*>|jKR0A=OD!B>PSk~>esRxLUvT8tL43PgR$I!^8%);tOums zN*PH}TipS)0!q|RV4nW~Kg9M2@7(_088wIrx7%UAB4n0>*EgDJMGPQmenD`7*`cE0 z8#_;?k8V~YibsWLEpQX#Ninv;?8US`M{E3pX6PL12XaS%;#_AL#xak^VxIYBd)mfC zEy5NvD`A2F+Z~%4PEadDt&*a?9C-33Ys-a{;EY>f!GDl^mMagqh z0xK;JwX~)~eSJ#x9~#sTZT76)czV1UQ4fz)LI z&dPT$y3{O{c(U;GjdMiI2S2qn9a&ZjDK0ktkfptAeYLjx4gybDLfma>*PutC?^@J& z)4;MWB$>X%S)@gy&rVsO$) z#ixr4BQZipzE-ioHLNAs%x-Y!8_8vpr~<|4pt%M~9o$|dYdjU%*FHj6nuM)tW>`YP z7NjM@LOOy@!bt89*Ht(1HM1XcA%}02h0eIV{AVIG5*6jGD%!hff(FOqt^I0VnT22S zR;A3ft~8`soKx;w)UPSkqC=%gd7;ulBsvsR@=ze2-RCX`xWA9`ZJ~DyQ_@!4mK(S> zQME&CIsUG-Xy|C4O>85+s*~XuFq1P5`5HlzSp;&_gr878QggU40M-)X0{{VK{{Rfn zjKVSEcp;VWwe!T){0LyC2-z;`8jo0dnJX(Sbi~MbOvI;wnJsu?H?`0yz@yKApFu-zd@~csi%r@ zw>cZcUF!ab>^)`0o@ynvEoo8IhgPL6)DQwffMGk*eHHTUR#)v1&J}p)8-<0bAy(@Y zSg}3TvO|7oZIDz`vI3K{y^oMTD&k?Hb|)Kxz?|i{d^7PAg7W7z&jYv}&G}p#b!~7y zOW$!RhY8?#{A5MXlM^#=7)QCeT;?Pl%+9o5CQEMQj<=z@ z;=Jhl8dF*wl;3{9qes85dXb`J(e3hPu zMhthl7Q#B4d=RCfAnYefQhI>@0N1;H`!(FX+EWmFik0R*r3JX?Y=TJMz&juUNcHvC zYV|B+czlsoa!99DIjjI`T~-Jh6L)LkO^`zJ)-;u<^lT3OyMeuT-B0!#4dUl=RK}?i%~{}8rxrOs!j-mxRw{& zi7#WEYWfp#N))vzOZZUm_<905bQG_I z>ZY3%oCUjND~-x6?hMH;GTTxiI=51{BB&pFd5t-p6t@eFaqRCE1!>D1l^V7FQYUY|*xh z-5kc`Yb~X@bPs5v?M{Zl(v@FT*B8rIBkdMgpBmfH)wRhh4Zc$)w6Li!DMX{;>LmxE zDB3~R_guEl6SqTQ$=IC8ca$=hXIx-On#6>_gg{afl(tadb>)XZ+KN&e8b~!u)yUY= zOAO=~%m>4o$^d{ zwLO;JK1#zKdSopc>K_`23P|5k`}f(xj6+0BcWS{VR)a!V?17ZJp3{_rWy196Em9a? z6PVxLwWNlETE{hj&}*DDChwC#(C=YS*!0%L!BJaUq6d~(emGZCT?_0}(zB%@D(TT6 z_TSz`D&#v9S+^N2Lw)FynRne*;NOz!1Hcr7XaH0W(m@*7AZQBx z714YD01qfHqH^tB){&OnMU#{?9rw_kKnn85JQFs_Y>j(Ja0gRhhcs@`;je-4D%`HI zB6UiPi4PluK8VO=Nd!JqwfTxyM^{$XQb<0XKXIVC(K|I-DCijXm8?l zogLM_=X0ED`Sw4O<5AN#8xNth?bqYB8&Fz6)|oA&I6i#I-*9P4OAST$cA5-J(Lh^IE;7TQE3n^UXwrWM8s}u~d*)oW2B3{PhTR`%>b=Y8 zaTbouNHCzcvT4)Q4Yrr*W{b7DCz7l_5^a)gnRVZlFH^nK6~z~Jwq-6%(}``ApaY0sHGhd9a0<( zuc5+$qt&ry{GZ46v|*PVuF(OTVxTVAxiMm&GB>%RTugn{rS61zjxOl?PAG&V5L8cn ziqtM+@ffRVA+L>*JPfgx#vEGXedjwdaJ!;tZVlfSF>zpUKZhZ)zB%8EjEwRR#ARs& z>c#;J1N*e#+d2ZRUX-O-aT2Nr88K^rFtUBPmz8Br*4l2SIVq5s23kGoOsE6W;AAhQ zS~^y{8mIL-VK((#C&)rwagy69S5s$4;$F6O-?z(OTIg`7%((nKVa28HdqaT$w3wex z1nfcA*`}(@nV5I7X(R1f8`$SZuY3m$4QdBcH3Mx``(H5ZZO%iyW87(yOkW>fmjW%u z=CmdGHmghRt!3#uM?-3D)V>>#P@bYz4*Gr>4*8vO2Me*0S}=RYPi(Q@ZZoWQ%cq#Q z%-`%`$5Q+1KxquRrx4pqTG~odN{P@1RZ_er*zHWOndaW|JAJA?IfNO=gLqg`y@aYy zlWvZpuzd8d&0A-sS}qZxR5zD-{!VdZ!?4&EC@>-77UwO3;*b{o$!>;{?6v7B>2bi? zmA<9^x$bMfg2WiHQb#0{f9Z=@aSe@XfUCY__FO1)?+gzq6*=|5O{f_3)lM+LVPe4;?Ssq*IOF$*S zNdZ7=sZY3}*-LgKOHSjVI$HXH(<9AK%d*_-I{GU^XVyQDTPMG82)DBH&n2?A+i3+w zC_yR?4wY|7Do{}aumeQa^q82;auX|B@E+3UwvJL72y>2vxuwC{>#r+Z7;sX?A}(v( zSQuK`m zEj))^m)UFIQAr@}9-5`TCp=>QQ1k*0Fl2+-r@d`<1pMlZXC(v7)efPB4o$2k=D{P zi`0@D;4ZUl0UGl7zoB?+E+d9^eaotF3x(zgOX0KK$TFL2!+qMJMUsUoWSj-+uo9b#^a} zu$#R90B}RAhM7)?%36VNv(!oo!0o*qogZK2gfGk|N>K?&Bqcz4jd$Nr`}R+5)-Kbu zXe5y59MnJ~T|lyT*WN0JJSGM-24!x6x_}4-HKN6NdG*ysqs>5VO9^R81f=y1jrSW* z(_c^e>$w37mm#);?KE6iJw$Yb=>Gs+v+4Ek=sxY#zZ?>k4ulXD>Q25tL-qZ=$Iw?~ zyy8!Fr7B1vAtU5|{p^p}cl?#J+l;JfanM+C(@m>Sf1Qp?7FINn1@*CJ`Or3NKPNZ8#;5nxQaKHNKd4@iA|O6}C5 z5|pWA_8ZoQxA;^k+l2XTMvnZa-DWT%J8N*O$r~RE0*TN)yAP#K7+zsB5W2f_iy4^@ zy%ETb89F{nl%%W9NYb{TloXIUNCfx##4d^QHIlr=331Nm)7;jOH2__b>J`wFr7T7% zqJ{4Pn2DMRj5?xgh%EqJfI-xO?;T-+*Kv&TM3x_AO?47nZAl3z-u-pH>$&ST+#Rin) zj5d?irIG@aG!>wA+-#HBomFsy$IYtW3gWx@guAa>O}5(`b81qWNKxuL3LtmT*dT-6 zqZUYbK#wVvTA>16LfDec)g5axQs=#KRGVWB+hc^B<5>f+LuskKn<{Nxo9nZv-D^7dBX5n zPZq}T-YYz2(`|9)5$GiJFsEmds!1nbncoV5zY%=0v;ExmusvmEx#jX zb;jn{d4?Ea3nQ!g*Gfu?SF`9qAn(-Fjeqd3k=y3nVM+khKr6p~;@g(TlNx)dwk^gZ ztx6yl9ZEt%c9M2T1P{MvyW^R%Y%$-C<=Pr9R~H_VW;Jpl$SFuj2t&oKg0g)EfawGt z_3@79ml&|JSvO1bbIj$0A_DH+N|v&sQtQqtIrv*o$Rz%^uB&eaHYK12%=b7#3%h`3 zl#tNY0^e56muf1`fG({60JCpGaTtLvCx*oi9%-bM_PlElOgI)sF$MEFEJ>ZS{vY>D ziP2_s;4~LL*sUh>;ks?Px1_&)Q;`@@3X;@R`A1+Mgy@2#Y=tL%k8fIq*?3OVb&EB) zm&wjtBkk165P*iuZH=fnvVxwYuEVGkubo^ow-jFGBxF+~wP|5XTUPiNAQH9gsU2!9 zjqK~I;qTwDu6xlywy_Gybc!09uD2UvQd$vNMv|5kLP>E%0#vip>N{v1_1TKYF`^?e zGCMDMjmp3%8hH&Zc<{CXzH8B9!+yzsW?25y9UcxiusCvLei-qbLB>q)_)x|r#Iez_ z(m9X^y^=_2^1c?armdnM1Wme?n|$l6#H5t1ZCYGdN`}h5B&3b#6YKIP)}jjV?=CzU zHwUkBq6lh0S|r<6%dRPEN_5&>DK2#HxE`JTeQNbAT%8uhkHoV>x5q@Z6eanJx*;XU z2mx=O^rt*C?$jQVb{!vQ&t-B&5l4Xi0fp+5@*zz~9URvFTe3 z#p2|Yu#g%ukmpsopf%ERt2a&Rbn#uU{{W)jvY5C~$9HDMn9Od%vmK1=qQuGzFvZg{ zc7q#o$vpHP)(G1ZoYzd!EjfN4ZVks1QE<9H9c_n_^LCdUN0RsTO}^Mb`Tz``!K}wi;p{llR{$|0WOWCHKYm$v?PmerJ1_7eL*EZGg3BNgv7BprX$_sT*+Yxd)<#ml)j}OEo3;LRsjn->Chl+w|bvp z7i@PDyjvh-7Ph3kmwd^MO;K{?A7{*pqve7Ur2J_eO8rnf?{#mtHz$`Chgt=_ZAGWL z%Zen9qDTYNKpy)e)OG{8!)KkugEnx{Mxq_^MqupK-3v!9Jpd<#yn3uUfY!R4T-ILObmNSl}uJlhE;-cU5FIw&{U3x z3e>{Vqy+p)1t9$OvVMOvP)Fn~*WGCkr7r-rEwYicm5r5mBYIb0b=VW$psu*pye31A zzJ{9r0M=^JE7pik!9C9Ock~~9N%t&;KJd8#P6D7pNZ(*ahR<(Jj^ACH>q$Itv4oJ< zmw@A-abC* zjVPpiN2LJkL~p3;f335#Q5f@{x6D+L84GbO5ZY7DH6%i+bZf#kAK!F*!1j2dgrv=+?=c9s&mW{W2na;By9@O**{eNAP-u~$)R-2 zVWut@*D5Al&cOU9kT`3-ECz^n58rd zl#rFF4(yGd z#+MQg@@t{xU|A-IAiLsBc+IfsdB>c2C*Bm20cW6qIspA*0pDGSVg@0JW+#PzYP zUSzwxS(eE1pOWj%B6AX+NH4HhOJD}jq62Cz?Opas)mxR)+{LozG6Z?(n;tuhLlYYK z8Ls<{t?eHxvy|*TttijjN;l@Tx&J7f(uF0m{apHmV}|l zKGI4OjjX$$jV08p-x@a%MtnL5|= z_JzU^xZa&<#*+4i{zwZh37p6zq@>NQ>1 ztwc*cQ|vu5;OvH0%(s{BjI_gObgi^BgrF!Wsk5@Bc2CZ!j&iQ0#ULZ&c0$&qZ=|dc zk^v;_`t0xJ_tjc~2`tD$gDH;Uww|ZdWl7eyc0g?ngZ}`pE5FbRrF@2??@*D_5R{?q zBU(EP2`eCa1re}5gnai;NoEeS_kFfbfl8D+sMV6zO!!9=lM}!{ByzlH{&(1%;eNy)SV2BQavnUwqcr-I0nlOD z;nZ)@Xy0z;vcv0-BC^Blk0PwoCYGsp2~Vlg;@0b{B$B5b1!_ntQ9D;aN=E+xZt74c zWS-=ouXFhJ)kL3hw?dv;w6a!PLVKv}BrK@*+IBj%q1>A2YowJOYvr#|vs-Ize0J9N z-|4UA-F}S$@+2fHV}6@EAa6vcUY~gN`Te%vS?VL<9;*KUtp4AptxY)eYg@CKT;hG! zIroVG4x*%__GBlpQFEk)Z$J;O`w>;QE^XN`40|1pg*5E1GU;;~?(~l z!1?d_yUAA>Mb#9SI?^|!pZp!~`F|eNMfquK&y*PjAUHrB#?*aPul;lob@=POPX@^3 z35*Alqww%EBc`-xFYN=m>3eM(8%QU~6l@ptsr{`$c* z-R`Wyk#SpTOh~>&CR<4gZJx)aXV9Gi+-v4LdQlXeaGcCm*+B(PDMP70ch7&|`fI-Z zD*5~u`26HA2-yDsGh&$Zm*-;~qk~^8qR%Ens*7vBTrHBIw|0itT0<@+wZ&4>2SSw2 z$R$U17dA2!wWVF%M*4|AsyCy){l1^L)VluwkcD)lsYhBQey^{gJMXZ2{{WGqDErdB z)ME8?l%2meYtocS*)B+N$5wm#?4QT5sS(wvlAhfl{X6#b@BUxlsEL&GEjb(!j79`%WcGL21_36ob;P${qF(WAS#^%pYFb z;IGhhK^yErKd|roU$%&*ysd>I_a#boZl&OdLfuhX$X~_Or6`pWci(5Z>}p;up(#+- z&ikL;t$rK-02Znxc$@F5i23#RMw(IA`vjnO_3!ZO<@Ng19It(j{Zc^thO~WsNTMj_ zEqYcplUVPqpg6lt+6?q1UBb*~LyAVWJCU#7TTb(aV(&Qk0{rmB@9)BHRk|dVTiSI7 zJdTKq_ogH^LF)jiJsTjTgYz}2A5wb9W3oXWpVp60yL%l1SI4x3EB>DErXkJ784RAQ=ug^N0vKI@liwA0R~aDn|mjJiLTpr%b0}AqFR01%27fIJ(IJreKbkg z`}a|-KN@1`G`AvBVxwJU-5#Z>Dg`KP_5gx*PWS2Cy%BBe!TuR=YZrvx9^#XcM#>`c z+VR}J>6JZuZVk^FwyruSq7|(%mdl9@%62nnsITKfb^)os#rK8p4PGDkqT?>;x3jyO zR+Zc>^72cw(&o0eBL4sewj3^@VE}*=t*1kM`WOTW?1eVC>R}#Awv@U*f$0Oj#x4ABc=pcxW9LuK#BCRBUkrVe zK#u`WIOz9AM^Ao)G=cDf3P2-YKT0Bsa+EF{#yl}xuO>yRPFKiR$5V*eBFk;r&`}j9 z-=$9IS#>ed?-3HxRuW0`4_WzQuIVMkN(cz)ZDAcqKswm$6g_vnlfCOi{xwzX+;M!r zx)UzqsBAEzh7&ZnppZ`2(Aif@eSL{iPkjxJ)!({s{FbC8&$UprwvZH{$p}zRwbHJ$ zut55KJACMhm07G!ULJjyrJ^Fz($>&)kKqv73X~gBAa#(Ft%Y~_e5$5!or40q@;)+8VV{QAOW&9+Ob#SpTKv5Jcq=7 z3C0{veQwyriw(J3vrAkB02-TMk1-BRdP|CRF3c{&NOwt3@P&Iw)`ctZ_3gR4S^PWL z{5$fszUrZ8i*Cmgn%U=hHwW?f&M<<7`G^Wjp@w=2{M7=5xHaCam&Lz}Tq5V6!@uz@ zxfbJRSDRzL!JJIlkz}w^>Mn&WpsDvm&U?`7SX+u;e&UFzB5|$;;=dQru^grp%f}U3 z@2IogA8u=m%w@NnZNIddeU6XeA`)E*1EoX_jRGqr_bla}A8Kb7=V^^G83tS~_-`Ue zD0#b<2E-}xCEFfxzJwc+7jv~c6=h{fLsCzct&*GojgL`2j?! zFRb+>q?5gatsVY9b?sVlu5ae}Se=T^ja~1#9k$5A;iNnkhpo;QP?_MZM5Coaw$`+T zXgUe+t&v8r+@op5WpZ8}0r;0YOxwe5+U^KaL&d8U#$2;0#}cisPfR$=l%PR$2ZMu4 zHF!SGTh0%cUNcM^l4l^ovoks-(44x%z6GKLf9bq7#~x}0yMl_&$DySc!8AKN&k z^ySj|9^}ZkT0`Zl0t<2?I+i`>tuHC`hZol2P)3pxI#N$<(G}*eYG?hbV)*FGRjnxpDnj`M*4^9W!W*drq6E0b8ahXn6jis zgn^`7O0=MrK2X0gt#s`PK4IT%WO)94r}EA+b+<#RlY<+B`btsLCgU+OUj+##p!teB zNznv-Ju%DtH+w(a7CC>0?`QjhunJO@rnJ(p7l;y$(t_v`7S^7(x0&U#MZ&L0z;EDT#LU4OvDbKGmwUy=Pa z{{V#-i2nd-98Kl9nmrN67mkb^Z_(mN)KL{`zFQ+TzRiL`2_Fg%sNc;8_Zq3zm?=wX z3SlIX^Fw>@{{X&k?%un8BegZQtw;c;x`|2u03qMkTJ`n$imaH*-hIf3bp!ZS0Fnm# zbd%p;{{ZQGn)sgb?eupfS;)ywcGhScBOpR zh;eB@674k7v>R!t)1~`D-$O~Nk`Pn~0VN7v^`hEVlCVMb+5sA@uzwyZ{{WcXdTKS- zrt2KWqLB1yKIJ2zhwchelw3;O0G|8OeN<1cbKC3tJu4m$2G@{ZAlqlSV#bn{q-{aA z-cxZNEg?%iJv}Z2yR)tNWj(>Fdvszv2jjaHoi{0wC8aCTN$Lqu2e)nfzFYnCL=v$P zDO%f$FZeaUaUdmS;6hTA5J$q}N?Lw<*!{h{QylmtV_xW-<`%YAdeOLw4-EsnK|5(% zt~29%^E5fku|dL#2HHUY^V0U;uRFHAVb=Us!F5HlQ+G>CcDQS~gh)!29(07DC0YwA zQ)vosTHj&4e5%yOJ3a(CSu2qpZWYA29@^_=E-6knqrC*Ett0qE5J(&OdT3P~Mpepo zC4RT5*HKqYG5 zbFYU$o!55NZ@fUvE%1DeyB6&kX@K&28A_tbZAnV^{1|U&3Rd(L>UX+}N6n_Uc2Xj# zDqYS@cGz)22t#XO!01|t2?ZqxKN0M$f=TRZF5~j0`?skC5A4et-p`Dp5*Lbbygu z_Li+qTp@egP24RHyupy#98Qmtpst>vM?#3|N$v>W-CYc;AWf5vElRe!(%ZAaaLiL{ z3J$#}1*>F|7PO=!b~{g@6>6ByU6A3-&AUoc?iOh`Cmc*SR#f9Gl_;ge5JHj;cX*^d8g2PRW zf!SMz#y|rl&d>!JqH%N&{Mjm~gATy4J2tmgGSfPF_PkV-xqZF zrkI@PIq!*}0_mi2?KZGQ43;%(Ls~XcO}B+`4l%c!$340tbC43ZTxr$$aT;`uq#-IP z4ybgkC;IQ7dGdtqcOhAiNvk%@ zwmW?CmT}13)cOsM-s4!CZ#m^M@?H0LQm(;of~575p6DciJL}e^o-4;qj@yw!kn517 z&iA`a=NLdL3S!`uA!!Rr^a2>rKSQmbQ|vt*^k~4(@oqHe@mxv4t!5eHG3jpx$Z*C9Wrwi=ua?VlmgH`GX>fIumX9zzmEL$jg>{D`B!c{wS8 zUy>hhZNpTC!ry72q;%{Q75o6{N+-2sveC)b+#?ytE^I!_QtWVDkYuHPQWUV1g^(A} zR!?9O5}-!MrDHRPvL|tz{XS0Iw8ynE1#h^3g~|>@KvLE~M^={Nk`BH@xIVWL%{)z- z$m2|+>3L%7Ws`GaJ?hBRVB<~8vX48?L2sa?eML--W|Vd17kWRfOM_T~k_#jOu%%yf9YHQ#ErL9sqjcb5EA+b?;X zITs5|81k-`39*8rHET-YxP_pjY;^}4eMhM&R@M@BNd~LmKj-_*FqD*M}8HRQv2`h<-xELJR@crN>4-Je4kZ8HmmJN1r&0}@n;MmBpoFk14 zOW8I%9f)hmh;#e)6H)fIWA ziG?zy+HP((A8u1^ppuxfTXck|0Ft&;2<_h=9Bq=D6|qTNbeA1W#?pl#7R-lIq!f^L zlCOzc1bU9%fYr|U&hd4ec!cF{P0pg;BtpdJOiha0X#hD5HmhNMsA>(YC?&#|96f0v zS|AhfgI1TsK3+up{C-2Mmy5!44yb^%{6wk5e6*6VqBYohitb>c#zDgx%wxtQvH|&s z{o^Cd92q50E`_daWb0rPsb4e2V&cW|(~7}D%o}krf+2_w&+!^IXT%+4td;FDfNW-a zgGp{uWh;(TcE>MItFBZhWiaYglohn3ht#FErKez&r>!L&hyWy~up7vqhnzCmkMMSD z%=vl}?dIykmi&L-P!{x+p^{SSI+A+IR_GqPI{md~tK-{`B68KA4~F7y_X)G2T0GW} zJjE~^LJ9KP2}+A?jTQ9o;U!1cS&nmtE*Z?sABHigFGa)n`aCx>StvrhnKvR*k+d3f7-*J`pkxEjQ%b&`Ngfvu!T2b1Yn;m9ITNVs#cXmaCaK$cWijLw^pzF$DM`^%71Bv5B%iyzO;`KP#(VBc zLu|7V)*DI#VI-uddsnAYTs!ZuH}mh@OBm$mB3v4hLn%Yg==;J_ruz^SI!I2*_4WDw z6RUGUn0f0 z3{)?4E^v7OzJ^hB0n<`NUb+KUx}^4k^rqfgiuRo-x6%|A5=T;r(H@&8=j-$BQ7CXy zyQ6Z_@h@}VGeB!w z>N(GQTGGk}>DjYT9;h0gT{B_{d*gN-7sm1$=>=sDk&S5-b*7R5yC#Vol}zKJ{e~Em zb+%gW_K8Sww^JcaJMQWw2i^kI6cTlybfK=@zI9rq+qlB_BHMM+t(W%12+@@Ubu%G8 z?j!9fWSyw3N!pZ=@bwaVo!f208O%^%WAGup77W1LX5<2+8bp}{7a8*xwDmL_Ngykz z2RrX|J6-XM);D;tBi|!5s4V-X)zqhL!+soflCrfeNa`;#2Eo}Uu=@C}C4`P?!XIwWYCqo&t068l#hyTeOTJogs` zx$U8TdwMBDtgHo+2EHv3t)!8@^;3BFG`QNYu3Rr-WzGcEy8Hc{aWAeSH67H1jio+I zEcB7vu@m#iv$SrYv$2b|Atuzs?yK2%k6+-k< z1{sZA?y}c$OY$styKyhIB2pI_w-S@E*jWHADj^{#I{`kM0;e8LftK9xtJYC}d4R1eu;g?r9EYdak!?}dwIEQLUgfYQ!sV)>KAoVB( zou|0fdQj=Hn&l9>DdG=qQ0JB~wnsust;LdM%HVaQq3OFnYa^g(CN_e?Ge_D-S($Cl z%HHnH*S%4zW)~V(4?8SH&9z)(UE|x>wvO%;IFPp_tt1jqlAx2V=%k%&)$4E%9a!&H z`O_~Kt3|$UBZ{ixBi)_odULXyLYa*fB?|K%XrQ2{umNP%s^XRgUXyEp?7mZOdQAh#9Np#$WQkTu$? zZaB=7Y?BHGM-JM}&e|JWFSvv%B3yb)rM4U+ttutr72P1{4f@r-TH{H$-tH(0OOYjH z%XRg*2SR+ik#09a{{XFRO{JtOzJS=+BedJiIIYtHl$%uf5mz>R2y)u{GD4J=S$nP4 zf{Sl2rQ(%rr?K?d)`faT;lo=5Ir2HIX(EYfs=K=z4UfF6W_J^b#N;oGO7a}iTH%<2 z#*#I%C<>qjUf?PI^yFE#nD*(aTdT7%8JkmyQBy8TVSN)2NF}!%QdHWyfl`tI2VGs_ zbFR~_TuaOrr81Fa$FZzY%YR56OiQ21n0n+XXZ4C!B)*ZeprwK8dq=R_OiNb?+-2VF z>X~$2Lk%`(UE1VZF3qzgGLq`g#Ozs2x>V{Hu$LHMq^D#mo87XbYQgzt!zSJm=2Z_J zw!?j;tTxkje8(NUE#)VurD18csfCmJueqV3HChri&;kK^(d#mA9tu%yFtp~jrIzI0T8 z;8IVy5aVh>!qVbOlBAHJd(?Lf#c>Q){EMU>RkVd6g@@8@&XVg-5|^1NTo4r6f=5X2 z->bI8Rtu*T-E%x16qzr=b1t*PM3urpN55Pwn(AS1pewC%7>+?of-A!RV&Wo9yp5QlAYyiYw-k!8bV%#^V?}_QE3Wn#9zhbl=lO^_qvw3dyK63?e5l# zeCD9FceFKjW*@zw_hKQ`v{KB65}>5I1G*_u3I!yQ+f^mlu&kwaYv_fJPs$x%yFCtM zBJ2L-Q2Qk-@=@2;6p_$+fdL9T9jmd066KBfZYd;eY0D!eh5=Lnyv8s|6}cwsoiB^= z4@h_)5t!jW{*~~~BQrxpUxDKum(1xMpD`jRxT!A8`5x+PKn2#&4*YI#PY+x!(@UJu zXNISoTZn91!Xzf0qFHe<9GWcJL7=6Z1HCff=5|Vz-ld?7kui4}s>$nGg zzMb_X+_;C8q}=4X-UpE*&2YFBEwYo+bT$AC0ir=5>qCD^gg?U^r6p=?U|ASgASr)k z$x%@G?`#pjpWXpI#d=2GOvjB;APkOf906cnxk#(p*DJJ*(z?H3jdM*B1fHCb*6Eh7 z9P)InY`FEiRXwg6;srKTIik=c1&ycd={{eVuF4yL2T|+~%xwK@h29?Gg1vs4ES{rE z7TG%be{0F@^YyA`?+0?rNLt1NlPx7lJx0?hDI>A$L~u2)nAr8GM6NB*;eQD;5xU1h zN5q$+IF8^Q{<9$V*mr6%bKk$aHkeLup8#9Ap{Qo;Us#W{9ZkO4g9|Pj$1r7;n%qdhh*ivdQ8>z zp^r8@?IgZCbd|0WLVCz43q65Xs2-O2YZ0tA%iLSmH1qcn|VrKxS3deP^H70{lP zlIK!CStq2R_U>w-aH|sHWmbu6gsG-vg2PY=Qo5G=YEIM=prrXpCPI`5=}SdjhU%ve zjSQ8{$I5#)<}wz-=zwcUE+8-(ZEHtkOMB7IE9h1?(jFVbIM3{lhKxQcMn^OlPBZC7 z9~&7mHH~Xb`1i&PjxCG>8=aZJfNq^xZ=Mm;g~N*GTgaCCSHCh}VMvJk>^$FE(eS*~ zM-r~Cf=CKIyX(+YjOP#WjBS(I_c5}iZx zVMS7G_bDzVz}@Z6r+&>3D11zpR*2|Ilt?6ipmkW+=>qi9_6UE`b^;oP!Wn+QJ#>JGuE5CIkL)zo04$j&pH;OuM_D|bKqJOhb z+57NZN6_y;cyANn_@mh38^k!@q!Qxr98U{}!@G%a*(Sj87C`whvrN+zp`npYjPLQ~V5xP9YYi}%CWyyWO)Mm_g z+77K+(_Uhspe0LD=}yS_hy;!9tY*eaP~rGa$mt!nQ?cqadwqVI`Bg*5?{34C+DoN{ zA?DV@p+zMQAcdhwBSa`D0tor**189UN!U5f#>!tEympY`@`GRu*<)$T)PfCdS}%q0 zk50V^#c@b*oKN<3!s5cr_A$@GdO_(#a*V~H>}#gtQbx%f+iM)=Iichi0s@(N4;yzE zx=pt=CD+ptWw$!Rfz+?msU(i=sA!~xYe(^^dl$A{Z;m*`Q0j||Lrth9#ge5Rbbyqp zUH7izw)^%QtE9Tvlgc>EETR;0yVboYfsjLKJxRviq!EK(#!SI-4^t0HZEK52S(FC`$FxR8LO8BVHU?V+$LHylb9eICkYd z_J)@d52(P}l(^V2xN~1doAxJ##Brg;rpL*{{{Uv^4<8g9ZfDJiz8i>WmkSs!bW%x; zbfNq;*g*3!$HgcPWHCNg%W*tCM`4B=zh`!v5ZEh%o z+G6AxLvASHj5HHND;(w$@HDfvjz?9ih~mGpY&J6kh4gpOd?r37OEWQ;t|jRgq)){# zMLS`I<~%kw2tN?`n)#k19f)yjSZEg;ly{48pyFG1l`Uh`l&TD*o&5+ff#2-?_0Re* z!h!-?y(e?tVkQ{VJDRn3Z~%UzHWLDZmE0LK2meps)ZP_wpX6 z@2i-?@i-W~CT{s87_P{DC{>!*At~#1^Vz9Sr(t1%-NYp6Dj~2*-^EEEJ{h6z#Y|k_ zGRN7&+Blj4tr;ryvQCr;J|AoJ578e+{XE6~6AS1U1j2BrPI>sYBa3=g$Z*g!wk=a zJBj%2=)X1YVu0aDDJonkidsi*$?Apaua{so_|@ZOTyHl##Twisl^gHiX<7ro!ZDmCWSz6Xq zy@0LtpQgV4`aeDW1bP-Xce_bT7Ryb_WAYwp4Yv)Iyr&5W1EmN;#TuBaU#?0|g0JpjpqOFjSJ;SSKzi-00hXQh)^4_Pd_|?EI52cXAxNAbzrGv6I+D}Lc z8za#ApOKL6JU7Z#xsSzr1G~8P7 zR9TbB_K;dXs+ckY&7|S`#hHYobR*zDZis+ysN} zME!j}`l7AzpCz|7`FDI*BVp%$H2Kh-LKKvsAu3Q*a61Hih~B?{(|FEW-=5N~PeyT( z)>SF62_N_&Nlw7l{(buzVPb2fiOj}G?ABXNh2f`|ajh?LeLU7Ye-Xjq8zII1!SJv~ z#*ETUiuCIa+wST6&MJ1h)0G-FF1lN|BZ{Rw+M#kR7aER}v`C2|u;JIJ3y3u`jyjtbr^QHtfceb;yAO}honMe$wZMxQ!QXEQr&Xn}^6%EqMY$+?d zHr%^~&FL*V1JRFM6ChAZ!5e0MxZD~~5Cc@V6yCoij|mR>^S zT@rP}e|Wg|3u1YS3a}VJ+3#z0?D)i5KiW z1K}UD-%fD+T0SxRJa?lE;aoQx#9ha@R~+<%65JQyn#RK1Mh}c)aU&7_C5xU88w(?i zvy5IQ0>JRs#_49u;c1)8cNwi^wv}5TJLIB5+i?V>G}_d)B~gLt+Q{C3Qi6g>Akkke=Z$3w_UTxd46Fm#o5EnDjCs$I%FaecDfUUuU1lU!0%xEDxXN>e`CgQhgv z5T?q4R0_yP%x~pf>?S5zaS}x%*#pTrJ0>t(V1UvH0ot8=#OYmE)O{lTl6p75y#e9A zoccHWGsb#J!D21I@qQu1xX+}RS4G6w+~0`FhQvbzxGZZNaPUbJ16hy2<1lXTJ_jDO zTmu-4;u9;7F{g^vt)?u7lsEE7_Y{p0-{=PS@~M#+PB{glxS7?2xBM!W*>XaIp9mo6 z$0X~%&DcOzXA00REPy3SZIpnJmVsqI5=b3&ZCt69UFNkL8FpNUEfD65l4Gob z!MPaju;h?BK@P3Aod-u+QPM?V<8xx)!zF8_5<=FnJ2M#4=MvkhceuiQ35PCoU4e2Kn!VV#aiwnhZ*toE`7rq$qBNGNW?{j714LICW8VerG6EKsO zy||XRQEF0^^VRtQD`qccmk`>yd)VwYJ8xeh-|JaqcH`mSn`YcD_YO0aYs)T|856JTw=QBLvnFtD zM6`gVB?t>b)R0LVD*76voR7ht9qe{|W+k%gm&DHR*>e#mK|n*cn_ej7za{&#Yg8o_ zI>T-Z*^9@6!8W_;xX<$;j2yERxy<2F4_|{~>;qrugvr3J?r zZPwT-P&y8{*zA6tq>e`6E=1x>+^lnwaU13zkVkE|{?5L_b}H1{=u3!F6riS8@Hmn} z7D`DVfI-&(0337SyMw$O@Y*o{01w$EiHz}pC1hDj?N-dMEy!h{Bs!MSA~e-UaJscA zq{oiZQWSn4O=I8M2gesr6FfrwvpK-=JX<5R+n>Ba<<3Hr6BE5ka^DHZnvC3V^_4HT zDNP2_%Gh^nSV-5U5oy7S#BuwQQjmfh(NIxHPQgE_51|_R4fgzMwl>0uQCh=@ZL_ca zd0vClxB!1GP`R~g`TKmOsjamc zK2v`Zf!fah0IvD>@BTwyDwyuqz=sseNeYnRDN=@s0H5R_pXaGoTTV3?y74J-N+bn# zQnq`3w|#5oKc0x8k`zLQ$79%i!+p=!@6~k=a(@$C>*`W=c0dP5UAM2O_4!v^f;~YW zjS(qIQ5R*y)Wbyo09tx`eKq|3zO~k_*1Dnf59=Kx@$9eJ{NLamS|a;N9sZ}&W48WZ z<@Z%{;f(prF6ncO%x4y>+(QvgGNmC%NC&9)BVh0E>^`EXT673Kk9}+Rcm7}U`BW?w zgwDyWu}jH{w-ltb?tva>M#4xxA_49D=!zYY!)e>S*~Zpf4$hGGN!d_S?3ja)|U+?&V!0%!XV@E+HjBJ0JtD#-~m% zMh%Ymhr?IOOncT6)>Q@D+B92b_7eJfRPY26$GmhYz%-KBP$UhS$S<_JmG1n>Gl?*4 zHXIndduZr^V~=P=WZT?Krx2TaszWI&Xr(0qN^!%~psoGHSbej#BG88JZu|{6dJq87 zBS4#sG%-7W}j8*a#cQqYEwkfKzzC;$i0@3GkIH{az|Uz6~={{R`}M+?kt z&O(z34L*SCI#By-Nz;uhKrKF-DI|9T=qhXQ?}*0Bv#~72>Vav}lz@(;mFy3m_CQwk zz3hG6$iDIJA-LS2wwv7dEv=!1xa@Y&w;w_jHajIMI#BtAcHiStu61pThAtX$>9XE! zh3(TNl>~)$?h1kRAolydnyi!M4dJ$HjA`sj^IJrJv+h==*aBi5DLo6)(xOm9gG%Tl zwt@B4uX3%r6svG~I4${B>CL+2Yl)8NZ>>7gf|AHefC>a2{{T>UW)5w7-;D(hc8dO&gZ78WD;Uj78G>qI;sc6vw18FKhE&d;ML@w}MlW|K8vZvf3 zDL@

ro>_CtJ`1sQL@Tf0;9DZdT)QtQi|q0R1#iOq#mM4+P?j0b_dtf`S(O!>21hM zYD1_}HbLq-=^wOje!o&XkEKG<9ZOeH(Maq`KM2s@R=r8raob1Ne@X5V8@<{d2TGL@ z0F->l?mPW-NB)r19(yiKh{No*(fwFV~@Apv@3E3%ANnx-O zl&BJ+prxo0r4?&^v^00yLaT=t_?6}kBFG?QIF?y)Vq6<~txSjHI^%8IO4(V2gsDr@ z4i=D9odNtBdv+B?p0=%)8F@tMz^?iLb|<+fL;SV!t~5sn%1(&tB`1EJhvBdCJEAPC z;2t|iAl{%c$#%AW-hM(`V(PUAV>qW5vJjR0EWGm1N>4&H)<(eXSB*bA7kp;nX@bK} zzd4x!u*XY7P8!J zoa120pN$gg<&7(`rdxhmgI3uK4@_W`buGtC)>4%xYSK>hHCT!9eADqcC|ezyB71hW zWx|g1g_jgrZbEuW2~xD$x737<0FpZn;A9_zGYFY-au+0B(H|kYV>jrLq^0x(GblZ5 zeU=hJfF;DX;B6~Xy1G_$b=w-gByKk=mQ!wi@dYt1MVpeJeG4l@#??HA770?47RzYz zT?r#gN!Zf4D4i{{Ri%&-VpxbfqgzX{BF(23VZBVA$A72@d_LK?8c|zTD7G z{z>?Bd+&dDt%z1oyev+#x5xO4Gn9lnl;674bP3)?c~S_GV+yGE`r7q!}-@`qDJ#T0~{r&~Sr58eS9{r&wvDe>P-anQC_%MWyo zog1!;0HSMcHld~Yuik(8lNp3#qkbF!@X+uhAl_Ds=k3!_sU3$1yGU9b@*L884}|)j z!?+%!x8b4bO1|t?!vzU%;cZ(f?mAA1Bf0wb*dY4-!_i+I#`<}PWbSB!ItOAN4&%h{ z^EbNln4eBeGP*MZY;Tx2a|22P%UyL^QBstz>N5UHB`HcGSK&*G(i-UwDM?BSyQCOsw$_|sOs$6Bj@j0M0Zpq~_0c`Ydg~QDg|gxt9c(QQp$Z%9*JIKL&>x?u z_4$gMG7DnSZ;LUP8gaKmmdH}}=u7DZwE?aC3T&wSQJYx67hpuRK=R?;O zBg;#F3uVQn4W;F4WO*PZMEt@v+t#OxyoWOz4aM6RQZE)@gMWko>bSuPA&Z25-CAu; zzKJd9fIN~F@f}Eby{+}$BSU=)j<<^~G^L?wD{4Z+U1{2WEtPE~Z?OYoMyl+~Osr<< zZ+6C(nYu@P$j+%r(kDc5EveRI_90=wbVA8V0BrW{#>P*Dj}r?(V}w8HSH@pzXxxL({q;`X21+XSvXK>lN1v2bYA+M$t~eW_7eLIT}e zD?ut9MG6Q7OHREBI_^(olk2@3jKLz~(QIpTB#kTICP`*wDUeE6(&Pr)CKIixB}!^T zag>#iv8A|AN!NBIg<3ATE!B4kDaWlVwYxIfe^U{!9jDWZR{iABjG2$w^MDB zC7#%*Q&j%r!k*-4(VS^c38*hArnlEW>R}*fI`q~V9O3H&gRB37yLxD-r^a@haPi^TN{4VF0t`crJWK4Pm*|lT#EH>8kCL1KQ%!#j0 zd2N)aBqh`+4fMgj+PSz)RPkY%Bik5#oLd8%>xqj-9w(c(J+X+xjoxc<)Q~s*Gsc`AG>if5-G;$4 z4Y)SQFe{{V#ZpGiuahLnO6{-%`m1s_$B zuGQA#o1`0Eg5iaXca?i)+#T+}PlLG-5aYQyb@eeMW z6DDiO*o2I1Eu!Z;QKL%n+$V&;4Tv+v6f({SXvShlY)+K7Jw3n|24-*pqps&gaMzAZ zefJr$Vz{t@jv@A&GE@Q`F=Mu{3J?%JH%gLJO1pIW)iH&4$gMdvtAmglm1vPVM1{fM z@PQ8(6|#CrB_Tmxhh!f6soDGGmhf!yE$eZ&AB3NIZ4u-w`^w4^14v<&k~^!=9exCE zps~QbgL7?(HcUS&y0aye$TBVVORK0_Tu)dDSGx`DD0k5M`jcH4xP~Q?M~9yd*c|N9 zxIM2VmJyh_;N>J8b7`i*YT~^q^pN7%$)bK5NM#iqcyrm8cScqa%%&pR=dPMAJQp{# zE0eLQac!>L61ld1s}V+(HQ_NSavA^%3JOX>Pr`N;>EFJ{zBlc*-bC=*8t;qRo3dZA zjB43*VJJWeOANw1thD`0-od2u}$Dd!KWr%`2v3-WgKgj7&w9gtg$Y3 zDJ?2R$Ro{5tp}SOL9(WZI{jdDJ8b7`Cc^S8pAfj%=Hk~#5w2MD`%L%}ZPF4JU5-ji zdTB{Y!AHZglfC+UxV&qKx;`g@fsA{b7i9UQV_xXo*A{}-yDeD$Yb4g5EA5_%yBWne zJNS;9R>R;jT=D?w6ZA_30AbZ>Srcz15J!nB_?tX~2gFSHuA% zdR4PuU9#aA#6)R z7G;Pg*Ez%>gZ6|KCBj}`NF7QN`vog~jcZkVaZ~Pd%2U*)(sq&c>H|OzQ?2NFldje8 zI94J2Lk%3}nV25gtv$miraU+sp_pc^04Hsi&bXXEf+iNVq54M@lykMZy4Y`>I9{OW zud~5+eT3p%p>yO6`rwG|+3n9F6!f+oLT(oZRLgQtQj^lA*C8$*mmrwwKw9yaQ(u^PI?b`*x3^AQPeDJgNLipc2$ zp#@1!fG5*_oK7)-hZ{VPj(u?MYaJG9j!2jV0O6?1Av6K+R=2wJ91D!e!0?d8jE$|2 z64N7b1b^xVB3CwsTm9ptXlq(Xrjxkme*D1hLCaVVAV79f~bxXls6&3r`l3d zri^gfdciCGUzY1*h2kQ>F|j%90}|+;6C-JGx3cJv+`@>W=Tm$^E6n<7jbzx&H1Cn| z%OrEc$mX`cJW~SVPM`{K6=Va*g>496I_gqMZLq$ir(ODnfC(Qx{?qe6eE$F@JNBzH zLCKQbac-q&plJc6rCKUITiQEo(0A9}Tm~7qNnHDMBs_-PY&94Zr9>n3m7}DRPRRt5 z+x7~i)y_T23^*_2 z4#MuJQ7gjC>7!!{t?YZiC%Rk!s>(?kw?Yo!>)@%QZ(QZI4j_Nt3R@wE2|j2FPRdkH z{Ra2@KLI;oQf0xPK4itTn`Me4lH|4e-th2kN<%?g0JyayKmbn2s5R2jC3gg(!3J?( zm?4JH%ZXVe`D7(y;3SXMJ$moGjT)@Djx}I}%NSlp!|IhGV)1=)DC&@q5|uG+Lb{R` z=|f3chgV2b{UuumS{SL!Q0F7G(747M+Dck0ywC`CVY5XYcPnZZIk3rzHs~fN)(d~`V5&V>iHIENI?n-?bE-m z_|ur&E*OpOQmzcC$7ed^NV3~5IO^s;pjh*Kz*Xh8;?@CVNm9Diq7tHdNZ8d=`8H^At&FzG2^Ppnc`P`Rl}flI zAr37gT`TEvOD&$Gq>q=N;9CURk1W);B=%(j6Hm)+9O4BfVwK(2LfI z!SOmqz$-IsI>3C64Sssu29k6qURdY`w4BwBSU1!3?by8g_i(RNI_S19cxwJ#V_~KZ*|6JNSNGr5crMV#1hz2 zP_W+0QUM3Hhs&|8j4USyG`NTc9w`gMF)%#H=)lb5m^&*P96=_AY?rSkmK-)82_%v- znCamzdkHR#x`7l)%G)IQ`KV?emftbab!J;(rf&#hGpq(wf#x{tYwLIuq?Xiz-%rg- z9mjBbIUISHJETc>7F=#Db-NM9)It)b8&i@xQi2YW;tx>nqC$tKpo-nOB0_ApShssL zc@tbxedco!+e@v?_lBNkLRHX(G^V_@1$5|;qulybp93M8mPL69S6ud9A=e*SQ>r~7 zw$_kIJqt%l4$4SDI~}X681Fl^!O$ok=k>cZyAvPt2u7%y4 zZfUr3IJAu50Zp(CpXL|URa{c#4<>mQ$Z=4_XwGz7<#}L9apwbVxeac1327c$7K5X; zI=$$3sLwuRjoNXR>~US%V|2i6N$#`pBde=KZ>yjXe&hAkclch!?pu_{l;U|FqLqHk9fy8OgA(wnoiIvrBpaU@KT;RJ@173QZ&{bHRcj`cNOtu5sD zEG>x*mz#?@$0kUS;Zx4RTTvcMOHm!mK~6T`$RrQjJvnY_J-X+SPqW5FqawE4?bfKF z3G?Bx3On}ObIMOg!W8s&WfRrZ6s_xHbxpDdSe1_3oVCbWE_SP!ZTBO&@jQjah2@|G ztF1tgv=r;q9RulI;~|m%0K0u=bDN(qb6eUpHKLkmWdvFGfwJc^ERnZ1S2eAP!Hpm~ zP}hJSEbE?zqi|JeftTY`?G|iC=7Q6AUQ;(`A3}mmdu+(^V?NPOPf0;}*p47|0FctY zW~Gl1)_V~3T@ zvr<;XYhCKoc3kHq%XJJ7c1jd_0*UMMP^6^vfxVH~e5dY7MY}lg`*zQ7nRHW?WAdz) z3w52w-WLnhLwmNyXG;v0*32tVGK$=I5Mu-YRzN(r5Z5+V=Pd2zhGD6xq!kOmqixD% zYn`BOU`^<{t_9dRStA$4mh`-Y1Jeoe+M0Z4*QEV#bG@@Ty?O@pMGP_u^FT%smElv ztv8|SLE6bang@QGtC-{~%NU)C-N!KGp#6{KT(W1GUfPwAxYD}ff;HwM0+%I#Pr?sL zPoOou#hU5JhBuzxX613zM;;#7CVAYo$1_HZ})uO1fKh-s{D9Rnl^&f7#5X4nwsz+49p8(3KX~F-bi|D}6{;l1KisaSHF) z3Nyr*nr|`~Oeqe-wn9SgclA7ld95)IM8^s4Qn%e(Y@l}PO4afQP>xmPXWhQLF5wCF zM3Lmcw?KPM`<=2P5X58#%CrDsq!o1fos+*>$%(2=v~BVj1W+qL}{xO)*m(FB;-4#cYlOHG6)DX*Z;+h5yBa24cnC-U`7(M*(nit;mUIEw9s7-U^{Si1`3E3g z?#(LUnBN(*+F4cJ{{Si1(oob!3QA*#3IcrB+)@ybeNq;6x}g%_i`Dt-q-dovoGm4k zp(CwJ=>VZA18PwuYg^guezkBpGvkkNc-T`*@^81~vw6!MLGKwQwFq{#EO$Onn)!7D zY1mK94Rxk$LKyIo!wAGo@ep}q2WG(5_L7|{n80!p4a=3L?$tkLINwOPmOqI4YsbAH zWJHJmeswlh9c{wIoxIOZ3gE+2=-Oxi63%lwni+<|zy+#i9F zTw&YZeN5Y7u=qe~BP6uO$VYXSQVO&OQnr5TqA%r{y~n9n43^^fR@Pfwl2k$NQ~I91 zqtm{s6Dh?_oz$V$gb*7ewubsbwd>q=`@M8ccNm0GGAMAVK~Erb>$mF-B=_|DzW)H9 zee|%{OfDWK#377!WHqj9Bh2c{RzobEMV&14vtKv#-_t)#y(i+>%yu`@4ly%knrDU{ zA&n3^NnBqeh8?rECIII-t&NSQFg3dc9I#lUU1SGtbM7txdW%4Hm2CY^x8Gp)`Hxz< zOjgTn!ZGQYqii_b*$uAMi^q^cTwI5eTa|DdPwKSKo`ezp%n+g4puBU1KYO@atufh3 zT$WO|omP;D@sZMAQ;Y4=G*XnE_bMbEdQAMsa&j%+J)sRIHL}9kw_Bs7XmKh-DDs2H zp-V_{?vUwFB}9@>b6c2}wnxLm*v7O!vy&3$cR+wWmkwcPw8TB8JewFhN7x0m={*DC z&-6Ee@ju!h8IjUs+SxFkk41_h?~{tl2r_XXA8s-5JS+bI5*=iJiDEc(V)DN&54hwL z;>ZgU)Xl{Y`|2d6XRF@BrO>0T9;B>oBV*Inrw%}d#(j%L&tV}exG%G4t+&#i zrY59oeGjM13Mv`}zf-eRS0Hgfyzp5LDH|OlzQT`j=r^r?;ooMa{xHl@XSjJ+5FfH# zoBKBT5qc?GE)Wv=LE3uRDm^M7kQ8)AyXtX9mPr)Q0wsWD$Q;^p4F^WywUK5$z>qge zD!;RDOxfuR__w1Tl+AoI(gz5k#7^?@!*Oww{u{*FBqX?DC6(B0ZCjvZ)EkUck`e|OJfs@EXlE5>KZ zixolm4(@H5Zi<_kZ*Xm;pby!gFN`>B6pqL6wm%Ulgh*kI z9DW(&FpbTg7Db7RFt}%l!JvLA?W`Qny zi%JxFKUZi*ib&Y(c6ZxfAb%f9l`)|@UBt#zWhJErb-B~n74}DD_d6;cfR9g~t=ZzZ z<-Z8CjGre+WnUL8^k>(;xfI`Hw(dVEoLhp#c>C-!oh}&he<4wH#6J-A(63x z(oZv+1TA!~4N<&H7q#LRc)12hmA@@zDzjPELLHRp*-W`0?4^c>N$CkaMML5m@7;%y zeZPFVU2h#Kfn;FLn);X0%LSx`Mu#0cDcaT=X$juIJMXO;T|X>WTDB40>M!-l)Kp<~JLcVz_b2zoB7+e<;xej@EJdO5={t{x)9&RGkG*6IJ zfTWT@QV8yKzErWy*Db^Jz`Zx&I35>{cL;>R z=y#&bj!g3wK|iD%WS0yCSg_IyjPk_!q=AhwV7Oiya0};_N#=yzIol(JkUw^ZIdR`q zf$l^pT`a|DgfiIx=%67XN*#eD{JYdQEXeGFX7y47X||tjdx)DYiGdV@gzyZa7I^P zn*77@l0!Cv5)D4S@{VTH&iNTt)s~x9mIQ*7^>1nEQd9Sxoeuv1O;u*#maaMF^)WJh z#{SG#VY?+Niz&4@?1o$Okl0h2AHtOM9s2bjnAJ-#xeF4!GGejB*xH?WNezdP*()km zfJrgQCud*b`g3vw>rO89tYg-3j$RpnBwd@bOj9X>`^}`Kgn4RYd5v``CtA@5N4;y} za=$JexJF^!D@{Gkk1KMtQ;%j}-OvlVF4F9;pn4O4#=$l(_JPDdX?U+meIDT$&K&$- z48Upr50~O{BqV;fk(%s%I zvNo;O&v=^KDECOC-O~HIXzj<8g*v0#eSW*DX5R<$k4whml~^t1!R4*__g=Rf>e=*5 zW7B8x*0)s34=UtLJux${H_5S{TJ+rcvYbju?XXJR9glE9AIVS@OcPZp`Ez{60?X&# zUU9_|uS6jvD5xi2EqsB|)nvfIh=4XY--U_6p_$to$rcBAg7DvaeY)xAeK`H0`U&Yr z62@_^DZsyI{Cg3K#7{f$_}@+NTt5P3l0r$2947(9O&l?du|peX86smE(?MKbGsF1q zH!Zoh-bby4&!gc{bY|7C(eT zOf*u%N+)CZSqo2?;RyX_6|MZ}6hgbb!N|{O;eL){On!;Sh5rBoZvdUnmjgcqhA|Wvd|XcM9Aw7$mM0MyY|-&~Cj7 zzpY$@Q7%?|L57sdV_ffub)~%}(p7DZ=V4g(#LOfz zw=0{C;`r%s{5KBpz~;Jnad|{oX`ar$Pj_h&BtwMZHoH`2p<7Ud{{ZVC7v?;-H>EA& zgoSAuJpk|4saGbi3YEuJ2{2lHoZ>D>xVM6`lEk}Ii+s5)NNj)=)YIr;ld?y1>FH7H zvXLFP8%(szbtq-Er>MAqQ|5I9G@bOHO?4$C4*vi%R_~DBl`?#*aXm?z?WG0x2N8m z;{8ALpC80z#TLgNG1Im@JG(ebLmP)~6_B>r+!h`n%r+J_*pCf|!?lK+W?!W#UnO2m zD){^{*x}E?uXh`@t&5Ge9$-)s73It_1*+EKn+_J%kk3Iu14NaT=v9tZO8g(WXn%%t zD^Rz1d$u`s#T3NUA@*ESTs~8% zaZQyFqSx?h3P>Zd+3nh@rai~}jSl&6g=4kdT590yjjb`=ZX=IIQSJ&AA*DE%I<`rpz%gw$9)H)kW9gdTs zWquQUNoAfI`8y8G?M<;R7CnD%e5h(FLZm`*Y$EL)rv~~QlO8(=ZDA_uQVA(NgSDv$8CY+M&J7vjIam$fHlFh=$HEWRp=2oPn5L;1gYSvl|sYWfyTIH=(^pVR+J0=f03+?x59kat?WPz^(9JK59VF-et{E z+fi-cxXF3h2}vkAQ3WJyr{$tWMQ)9{E>{O#2xvJrO2ciET0v9GB@L6@wvu#`bV_u6 zdskgIH=9UG6hYA&2YMeN`#&1{xHR&fR^ASv6Qu%5NFBidfv=xnKEHiJLVJu`RI$Z+ERx46rBJ8KgbXA{Qm%X_1{WQ zm;GRV^e!ibCTIO5e`I=V7z#mVaZT|q5e&)JsQcnFl`T?%rPrLjyMe^Uz_J4-B z>yf{o?Ee4_anToeIF!OuWcMxwe4}=$5S>ZCRBWq%;X&zKK#NOC(tdzQ=&eFB<%0COQ~tn%a0|cEw+g2 zSRV>^@;;mN`~LvP7RTfWm&;V7x8^08k<_Gm5}vRK8~Kn(9rd&8L|2E;9CrM5V%K*4 z-Rp{&gC=Vc)zkyJZSt2Ck^ON1C~JMs;Z)k`FD;*bx8Lsyn1-XS+=N9zak$L02c$NJ zf{K7T1eNG_SEXdTAGC4Uv19obL5{yYnUG<_<9 z^Da$*J8XU&Bq_Nv2bRoMVusSjijt6400X3JdOsubqAOJK6PhcLGV4{vh^@v{J5w-R zXiPI)TGP>|U3bZ6<5x)?ART|QHB;&s6hl(NKS5!PD(jk(fx zf>Z@sx01>;DDaik++TfdN-N)RRCHDA_!4U+?s7gE#`t3}@(TcB!sm5l{WWM)U>1$k<-~f z5$ry-dYG?2@rGx@kfLPPR^TSj5x5bR5-Mdr@eKS+SseujX#|oLzfJb6K1DRirK7K89RaiJ>(~M7Sr+iYn(@vdXqO)CZ?|dqiEa3@n@o=}pbt~a zK|_d2mQ)ApZ(I6N6_@gi>@w><;Ql`I?K9^rvLB9FbFzw(r0HKG0Q>&E>UFl{Z8L*e zUzD{z{AiB0r6}#i1iJ8OB%hy0M$J_2dA@Ni`sL+V7_~%^B0>^ch|&_21-L!|(ArW6 z9l!LVsHY-Fg>Jc7EEbg{LvjVVUjXbJc?eV63H#Kq$A5JZen`t1wb|ai$!I0wg$B3k zt%6X5liaB5ztiVd_~oZ7%cd^+{kr2Jnafhr`%&C|w5dd?422Isl6q2pV|5$ki}EL3 zB3NLhNri9nlIl`^7i23y^#D(F>$bNY=Nq4{`S$`aEJ-JUm!@5b{qN-LE)~F_e9s z_LNIgraKZ{yj#Ok;jGW~ok~kT60}Jh`}_VhMa=u9##Pk%AuT6aa0KhA)U!$mOq<~ZLZ@&9|HM9Q!S?{_cyZ-X6+N2en(P=Q)mQjV0!n~yM1bP@u`*B zGW&-b-0b0ywb^nN&hnf~pTfU*W!Ad7N$J~Xs3*39j)h~F_-^)pxGZx201e*H_XT70 zr7KNorCtvZUmuH!SfU|C0i|4kP$eWK%dGbmqxh`-{rwKA&#L6O?ZqP)w3LqFQ*=Ss zt;%1oeIEU^NAe2u-jc%E_!<-dmx_?k~W}qHeWoPRC0e)$C(g{HsqvOUhqK1eG{WfIap6dKD62=*Z(#Qwy>;pPYN1AWzh64x#?Xe>&_}+ibM@x*l;& zG!mt)X-8VRP?DmMosE@&&^!95R!E2v#!b+BNuWsp5vT&mBYLfNSf~CBzvOoNY`X5< z9g}B+1#z5Ih^km(LQ05oGP(jnhR7*sr(>|!StR!qWom3UEpGHOhgn1HjRLdgAZ(}C zN>{JQdT-XgE`CdJASw&12@Xk!>fBFC_CqQgPjG@hU#<8a)zpdOVaYk;9(j}cmVZ>Z9%ev6jompU_bPHDO|K#B)TG)S&gIxLTIv$#%e z#MYSE4YCY;y3>?70?&Vl$CCF;ZLvNRHwB~y--b~0Y?Y}yLV~v7Bef9BF0$j~lkCcX z9b59GH}5D)yx4)XM?q662TJSn5~jckJ&wM6i1E$)qS0n)X-gKk^VKRqPotAiQs2gV zXeqQd0NMEp`n$-I=2s%*wu0eX?yiufqy)%}hNC*BlypdSp1!0jdRTM<2DW~8h$Y-( z85`kry`CCZLz-6sm|`Izj2+Mo)eFlc9_s)ue6yJg*alp^*2YTXM97E*fV(S0gV;qr>!R=ES8Hwd?RAp25o~_o%3#A%2?>yxD{(C-D`X)=xd_-&sYS}$o0%4t zZkDiy)hQ*VC*gUv%Fq)bX&{r;-AA|^A2CYr)Gk}?q2fpmDFN2bhQ{$9#+fuME=9)V`dn^&`7jY`izj7l!9B}r z0A@1{jKn%4SSjXo^UFcfzY(3JG)ZNmCPZU}t!1>9IMB^Y1`=7O#^YupHem4a#}qBd z*O@1AU~w)dK^Zr8Ni8CPSr6flWoLkI5BT;!V~@j_UG97q8jTwDniO0vQ(r<*9CxN% zlG}?~fLBUd8Yj?I@xtN`A;q8}yEh(Sc58G<>Ry<}tj(~*c3UgzD|RZnTSG}JT8gxk zq>?}ztrq>HUOYLwnEjyHuNPt&##N8lFf0Ae$%l4pScRbfpXdGjXcg*P{?F3JLQW(dQEH=RJ0BnJD zJhjW-BAc-^Z9<0mtOVwpB?bRy3Yr~IkcM-rwG z+bjT}tpz$s&^z9~eelgBP+-m=!JVb8a~K-Z=LZ&sjRUWLz1B$ZlK%jT4j$Z2K<6|W zvp5LGT<&=dI?x&ur#+Qt&b$ZsQskSfnMLD=@tBzluC8FntO9KW^--IY3HIyK`lOjuA-L`2qY+LV||DRdap>Y$}Hf# zXO;XAyUUA=>T(`3xwkcm(413mO0?YLhYV=}1P5CShExLg-D(>2=78#Wy0+DTH6-hLm8r$J zlkp6};~datj-wy|;$&$C-=J;T0n}@Ktz;`I<~u%Lhgq$$ZLqP{Z84)qW+VcVl`|kA zx12+LPH2xc#UE0gdJR;Mk8w21ZcmTo2%2PLtxIn4E%d&J733vIam2cj)TDL zZ)z4hZGyxslP89@oJ`{ra!emj z0654A0*NB7z`Lz4UOaXyiQ~9-BZ#yJpv#@Db3`Sv_n2PL4*3InpjjXlk9>#F4O_RYny6(qy(@y{gCGy-z= zrH*@hLIFB}Z@f9LHXD7H3u5JQwb&4+p|>f?7TJ$Fl(>+sZZpbPNl|TSQ2W98?zyix z1CSuY$RC)rG>M4|gx|yb8bK*>DQs;?K8iiT?e|xuY~N-tfN4OX?+*BrLuY0;TMOzs zC0#)Z?swelx%pKk&%KV`0kdm+(Z5F1>;uYMXkDi2$PM-O*h9AU`e<~Ht@b_ZtYVd} zX=Wlv0qgdquX}|6>valo8@N}Hx?G3w`MYLG6!g}cERbghKq!i%rJJkr=jX$Y@+LWo zw|IK#0wls&>awUX)}l*SPUJkkj(OnK)U7E%6eqCXr+&h=tMM24GF)|+LAYGJJo2>rJ<-m$BH417%)K3H-(R|>M0pX< zQ6IsjMXh~Pzg=kjSa<{RE8vQ4#}?#lW^8*rS*n|Cx<`p6cH5I^N(a2O`O*SOXz3d| z*802FteAWd?4xY1hG}PLbDka9{9H{e4EZC@cqz#LbencEgP%I3#urJQS@s zI}Y34i90@r=TO__`EXfWTV2&iGa<6o#yf6oIzpOoj>_x;&@>1-u7n&m7-W7OkvKWz zXO++GTnTGjNqKQ3YuyL=X}c{s%bys*vFtK5>h^~-wRjf`AmNNdQDt?fx|r7eFGNlvTaB7-N@G0QCKrU0Zg=~(_B=jezzJ+h$@tA1v!XECL z7~U+ncSSnL%tnV>@Tv=Q2M^;O%n|I|>0OkZwb;jmJ`?p=xKVsC4;A=;>0{ zQ`=h_+1B?|w)=|RZC6Q2iw0k~w52)&p+Kc=d`n}f4HS@4LDzocRn3Ih9lTy9-b7Wr zh)lUTzO)sv2U1#3Q6pt(F7+ozxdeV1c%Bo5o;lz6tWFj;Y7P=sM&jpyavN*&1G9ml z%S{U0pM+v0iM%LcaX36H<|ZgG?lFpUgXPq=$Swp8s1v5NZo!OzB;wznE9EJOoQeUi9!SJgB+lq z?4-p|Bn@c#=v6CgEuC(YDd`G+LAJ_h2OAr8OesiPi0n3$p=&K`xJpM!-m13nK4p*M z>B=vTmtbC;$1f2XOTC>H$G_y33DP37Zp>yxon=dlX${7lr7G7THj<9EoJ2`MTk1d|KAnk}Zb)Kxgt>P) zw#HbU7b%Ad1<4^LQyVvSl=1-^Ax$~)P^3B`TIE?TzV5d_($X#SCR^BzHalm( zpXW-Bm%uvO-ByORThlq6kb#By;FX2$$XeGm?V=p|?>g)Nci3)Md6MOxV&)u}jvtE4 zL%7*PoZuUGS!*={JEui&6s0Qy=cPk#EG|d8vW*U>71X1n(2$T+bbjvD60f#dOo=gA zTY-eU`)sN6`m80@1S#bX+e=`TC~S5fzcICfaBeCm9f5H=UYQ+w4Njbf`z`Jpg&5p^ z7YadenpU*55Ypf+*+ZS(X?10@pt~eetekNPj7rIHrWP;4ql!2QhEIA^NOBu3&`CKd+pCfO$0{b`Z z8yzPKj|J!l3@wH(5N3mjPb`eWMU3LZvWtsigRu{ZjfOYF9}mFW$ovKlIh#(NLij?Y zC?4t^#`V8KciZpRe&6-f!;D69M3lU=@1?{gr45xSeOmw|Z>yk@uY~NYzUp#^h2RWH zRBP^OA{>b8Q!-d;5>&R_ZCx%P^*`B3AwciAKPs*m8=f0+OO?9mbjtDzx@~e&`;eP} z!dZ~(T`NFZi;F!eQnsO^)!RgCbxCniwT^Qlo>w$DHH?Bn=jrMW?wPi>od%%jxPFKJ z&t>9#O9JjE=?9{GBLUk*o^B)QHwcN52TK?-z8}RpStE^(e8r4op7ycKV9Y|P*J;SW z@f$ul5_?edERiv;nLDX1rRaq9N69VH2Ak`Q<0q$HAro}y38?25RYs_VdcJAWE9((>1?>7qK3bQ z_)wjYpP(ds>QA@W{5C>;Qd6B}G9A*DrQ4W0D&MM$ z9!EGm;h4;$5sjWqMn4u#hZl;G#+ud!&%9r5v#}0NU3H3u+Oaet_SiR z=sFUhg*=p#q&kAI0S2iSX?vA>x=fob!t{yGE$5wAe##_)t!t$EBmg>f+d!JGnU^hb z6TEib6@)^ZJ#O-?goK>#_)Nd__b0;j!^= zxY6_g5%+KI(BFVMLUEJE1LXs=jQ2|+X>L}KMYYdye6j)j-J3}vmUp=RJB0Ag3&Z^e z^mB;d@z`7?VtA%Mhfd5!7aPU#Fh$!rn26lw$%o;%V9pnWB$m1D#p0tmaYP<_1(;8E zK`8}IB}z)ZTIvIO8sA7Do%(d0=v2`X#?ZL5pz0rKg|th9^-`hIRFm@9J?@ar)Ic->*bCmDE+=Vm} z>4wo)>p0*ZU))p zy|%dvSUWl-HM+M;d@kprM~Kxw+@xQr71+6@3&U_XnIurp5`dL&Xpu6 z0O9iD$4TtOM*bGkl#PUfKf-=>OI=!|A*7`QB}+bsL+99&vPt>+dXKMdm~kTx$tRKE z%`t3}0B*Cfy^}yc%M3(Adyv~A+*h0ZvSVvIxYh*7(N`KyX>n~|Yh77LOE#udpZ!9DfK}4&$u6v~dkNBgYlVyA zqcCF~pA#vjvW8rsft~KX16)Olv&yt9jr%(Nnl}{Z`b)>8cMSAzg)mD#Eu;yjm<;W} z;F(n}Cl=P!OfL?;*s0L1gKh)1-vXVF3Up?>XP|F+{HwX;2!%A92t&mQLKNobL z_Fs0%JE*%6UbsB#O(kF{o{|6|AP-$DTN@p~{hj?P;_ys5%Z1|%<@sX^T2kkoW3rV5 zKXQ;#qKDlC5}2)@i9jUv^(W#R)pRjlehhmMWvdVX%>9?Ps0FiDMc-Ygh~BmF$KV$uol#X9LBpn02+B$1aZH zd1x!K* zj!M1GW+MpclW=(`LeACDlC3Glk5H6?2_%hv&5SU{=*#hVnJ!YtIOf%n0Odd$zsKD) z0oOL!$vx*;XaE4dpT_+ukBVY&AG06X?jBfp3>cUr#mfsD_}({;%-FkpS%{IIX!IjzieFZD-59vQR(an6-0AV z_0r`%$9sHG!kN!GRLRf9dgz>F=p?XlMtYOpCRX{`q4 z&d^KJRq`nX8Xmotd;IDJHfB9Bwm#LgCJ91PpMHT3+Hjf zhZa1!w9?lQ+&C2&%mD+mY3*HLS#W6DhF{t@q*z`Lgu#Bz_(ZsOGloX-#BoCt6cFQh zdA>f#jarS0Toi0Q|R`!`1%u6#oG5K>61WbADLxX7N$~3*kVZPOOKk3V@Z3O*HNoq4r7*UMXgyTc>>QmTqMl*Cz?W@ zZI=R8g*nuPDJ5xAQ~}Y~-Rbt<9r2lQ#12z}qWRw2VJhiXfJr~Wop%6_PrKHqOtNj5Ayq+-#6x{U(8eNrO%+v5kSTwnoAYGR;82F$_(o+PC9( zHuDSblHj$+k8X3Va|qLFTF}x_(vmmvADHc`qd9LB^CYXq#k+YF0TIxnJejw$;aX9D4VhvPV|C$NU`*qkTq-V8n`hr5VlFxWvf zQ*mxD$GC*}r{SZCxF*ibKVujZ{vO*bl0UC#@bkzor7M*REwm@PnY+n~)|3y#79Cq^ z03O61`WiaXYvBWIC03@|`59ge3L}Q=0&K0ip=1sdAAP(-j!IAr7V@l{)Py_m-qC zdx=NzB$M$ahS1)DSE|pa&uTg6&mFob$rkSJdH@Rn#F9xmG>`{}CEDO0voF~H02cIL zjq#|x81%yr784F&;kceD=~fI$i{iM!mxsV`m|g>ij7HeR{6iFf5g?BRhm$MI-wTZb z$FO_M`psuy@acvEn0Btq^oJy%#-Rn?yk1$#N2{|VY66q*$&vO(HM1a zCrqk)&ZJ0^4B2Wq{ zDh;8(Eh^afolk#}ruU9u(9A{y(`bD(emBz0E)x~_e?*>c!eOvs$FjsaH4ezUFY^V;;c<}8DJVAk&MBP|poW%0 zhg$Z1H{6v0*!TKI+wjxOmgtCzaVORh<&QEPldYr>Jj5yGBx|VeunIc$BB|}$oH29#2^zF(oatrUwL4kWd_2Mb0EE6Jjcg6!f4BTZ^UC8# z7w9{Sp7IWNR1PM{4R8Rs*sZKau^txAYzSsnNgTkpM+NDvNJDB{{U5>z$Wlhiio#kX z4y31NW4RL6Lu<;i85c=2W~L>wKT=9q3G(5_WPb#O6zn6Vkd=Hy4x*#qb(35>F2+-Q zmm>8JJ>Jyxdy!m69S)mxvucw2NO7-HknlrmB&3Csdm5wJ)_cd6pAGpSz~8XWh!7MStNEI?(iDhhg?N}W@$^{B2&&{8&~tdMjGI!=er zLam;1Q~AYsako6y-F@|SkJU_NvSANxw`oA9wmz#8MKFF;y&{piM!`>2*qBMbO zw4ptKB}GeZNdRc?>sev-$B|iK^~aG{1s{^K3#7=t^ET|xQ5Dstk8yQvv`W;HLGm<` zM@Sm~3SzU4oUNF0~=F?O~G2$zNoJC?jXkRLzX? zHZ#XLwW9ZSz*%P34ho}5_o(Y~RMzAzvL(h!!ctbAg#@UjU8^a4NdEu;<~gJuLdFk z7aJm4D*Hi51GNcCC{D)^QY4y$;f!y`F;-?K@0LSIPoOInGzx#Bo`fS zHNAZZ0b5@5b|{Ml-)!5F1*9ROC8f@nI`&nd2s5-RUq*T#upwUH| zTvI{kP?OSit!Y0|{M81+WENnc5)hwwb$j1Q+WOG1pP}j_<*}+Fmu|MSTcnulQi>85 zgp#02)D*1~uTAWB1NrJyJ4x9}b~`8V8~wZQ^ZUIjYse<`PmovLO{j$T-*d7E8u=$c zdiqqBR#r*xqE5H;{{S!BL|v4~@3kZo=m&jzcI*es_gBM@Ey1ru4?sR${-gF+2l;=O z?xhia?CYd&MDJfyU5HU0p8GK)Ah`3#rL>O9Qi@9Seq@BC{?YUK^|!A3*WXbaT*n~X zpvpigQxY0l7CU-r2Yc*6LXPmo6@320kAEgm>Q~*dIcRK!R`2e3@!|HuM%jH5&fIlw&n*RVHKsHhd z(oT=zrFR0iUreE_ea=r)42;=Pc5V=|}Fk(x)80%%i%Z*#hWot{o6e$~8M`e(9KDCsOW#1)Y z98HMYuU2CTyX?Eo#^K0!8R~7dM8~b{!!ekMphCVq%sMLRD*1Yvyv#?IaGw<(ATHTe zmdLqQi+(M8wq54B%1bdGmX>|dls-@boOMhr^o@2w^`a{w;$>Mc*ecgOhn0ShZhj^}RRi+wP((8^xtUy5?d;_`der(nw1P2dK7{0uq3YE2#XA_xJi$VdngU z9CsXrXN7E^Yjw98SCJ`F01Iq-wugxdDgf-HdMng#RUuE}i>4`)Lv9u7Jedz9%C@dI zb+{6wbfv`g9+y5aNc?N_n3b5n7SNop$tsA~mmCIsr`Obikfo4MR>Hqn>*d?lh^W1q z6pMu5BKey^>r&g;EubCjk@W6ch5PT?c^Hf~{FpmQQpr-A_jC`0jr=J`M#!Yit@svSjm5+xNzL*r!&9VYa(*|hRFGntAta9ER&#NZzs?DgR-&~ga*6HzauhYckpo_u(Ae~?n|+x4 zt#ss$CU=W*`|aYzD#lv4yCWJ?V({V^Nl}UzQ%^Xi-X98zTG-I;D_IPQpLM<5P&<@K zaD)EKt4iLE{dcYX+U=^We0$6MgNQw1PZZrQn9f-Rne5HD+GMGPPmH9ON1C3Jl@<`z z$@z}1!+Qi-PWGT=_PcbI*(J=*&PbU}JmRubIgpTCK~Ze2sO>1b-CZgQ*j|7YgLf9E zR<*0T)zqK~B%P?8`ud+zet&&bZ0D7;?nk=6aO514=abJ$oNec>F9#2rg(tD(DeClGHJopScwE|dLnjn0W)zksm*7l>X=BasP6r}6F%=hoV+xJly8P@xH zef9aj@c#fWYEp>1EJ%5TsR(t{wyhPR3sCCU?ez8E%l6a{BapY34GVGCJeJ)`Lfl)3 zLR6#>mim;gk)iPct)EfaqR0U6dmAZFZG6viclduTb@J$uj1T( z&7eRuZR6Q|*V6w0Wqz9BeFXH66UK3HVke4j6F(2hhHOrgG`QX$iOo{EjmjH@a3;yJ zC{>YvcIh5!$8Bta56|hK2>JZCzmeTk7z)yopp_L355w>5K3)3{ztiQ5#<^oVXI@?H z;iJFRPu;)d{{Sxa5^tONDWW{Qh2(+tLPD2V#;tYrlOT zPrRQ>RwkM83CheJwUWA%tsS(J@+0f__Wu9~rEuT-KH{!3C8O}O_nJU+e|GNhCqPEv zodMlXw|fRWmKuk0i!GO=sLfoE+h{>;!VrfPQsLUc8z~zke9v)RMR5zSIO}mPEXk0F z;^7KESwikH`a;$2p+E(=lq;+t^r=7^m9i9MHu;Z5x=4R)Bg|!|oKuCk()t9C#9*@87MH{+BS%3X~x`a>=3P?U#r~q(fQHL;&8znn0!!m zwZyf+%);={=+3m@9_IFP9ScO6+HJIaLBernGAS^8L`xTEE4$>lJT;`m_V-@$-1Htn zAXw7%UmS9^`8V4{!nQ;?XjzQ3tITbHv?1jwAJ&$NTw2e_r0adnR*4qW-DBM^Hm82j zk0tjIuPH^eLXwEl(3hN3od6d#q>;Ur)!Yy&iCA(7=QzkNihZ9VXTCP$h(bCf`^ue4 zkhE*0b)`U^0r3-~v$~xm<%>2ut&y)b+SBu5ERFox0r1gN2~Phq{N zl52f`$#%(`f5UexV?Aq3n^z;5ue7kBS`)2Sg*J4rRFIN4quhL}M{Fw9Zt|^BT8APm zNfKC-c3O7hMX7Hk#VNj?q7=b+01k*!KEAtt{!J!o<(oP22gi;))9gIeruS`IN=H+% zgmti&UP2E1z#|l%pbnBd`d6aGPm$vbO=DUFjB9;S8E|MOt)z$Dk#}}2v_{4iy@Rn> zDI6H)v6zfDMq3{noE-i8#o!Z^>}>B=Fw5c&B%BA@Z_RTTEvR={+?ji6snSA}h%rQ^ zm)=TBNl`s*r%4*oCw;r-Q`mRN4)L#!ZePG1xw$IM%P_7t=!;3|g*Iz48l0I707_Kr ztu(cikO&G;+4_@rC$e>1fw1ht%e3F?7C`d?nl%*<4Pg0Lb z(CkfD&t_LD+3r3(ct&l7@{hD7NPuCq!>k4wcF#30HLiq@;Fda63I4a@2e}-KZ1{+= zm={JJD0?Fl5WpMR2M09hPJ>NIt#>4^N!hY~9_Y-Z7^E}C(%$vHJxvR{7pAq#$7C)J z#_o70wYS7_J=iyF%lP)edXA)ylD?QPwzUJ;D2F4gE5FF=(xQCX#+R;n_L=cJ6YW;( zj~{U!EbZ!Rl4Ca^_Ag7B88MIw(0rA2GU|cq8|nIiR~)+>MEgaTd1dxJ>R${(Vq|c% zi7Ezd`3P}Mq=o3Gqy-fB=^%PkH^b<@%kgvK<0QCA^Q~6NR-1K^wV@l1n9SZCbt!Zs zucb>Sgi9{&JsZu1qV`Xi?AB|3IYgaw2kAxn7)TEP8%4Fjsb zoFj@B?jy&xELGT!yc378nUP&>=ZIF^T2z&t6z{nQHQoSt*OcwN*TREMrMh8jwvvnCtzRzi{Fl&NAmPKL_9b;jb-vQxv}`B*bZ(;nwPZ6&u| zZQsgl-kSiuUk$?yE(;uE83rE=Uoj*O>{xM})q+8BCrwS#z0Ym_O&b*UZ{*e)O+PYb zME#=1mKXtY99&2a`Km$`q?ZUXQgyxeuR5H%&gYSD_iJ<3$q`gZ4mIRTDP(me$6HSJ z;VB1UNpAM|MgH{Y04R(v_ro?4I?e ze<1PQ>Rw*VMLgX5gqbd;V@-rB%6Fx~)WAs7k-Mv*N$PJ!K7+Z>{wLUDWQD|3O>~5Z zyD7-gpShrQY72{~iqeAdGGQ@4fr@;MbY?frmaX<(MfJ=O)2&vQdn#qd8KwI#zu2w! zOY19-V~Q0KiE1b$MQvIKt6@$8gTAA`r{B3hmJ1+?5E-!silbb@j8|}C!rMIt8 z#6pC(6p)oTTU>P`U5Cay=+>1GIJ+jbW_MUHU`TcLr2H?n)RduasC6X{0+b{rOVZF! zVblsh-DMxb-#la<5AvH2C*yYqp=EgNi0QoA;;k>YA_CNx97LotT=SCn>q$!bwefZY zkXWg3ICBOg2*}+VV-d7MT5|TkymOlyO%mriiO`dvCq>VMIzYz;=ZV88d|(E`01tO- zMCe0m&DE37T;KBFwNIX9_?A`?GE3Zfj$_<}-HFQ^%{<%JI8Dx6Vq9^gX=TKKq0~wa z)Iy2%*`6L^UxA8BYABH!{fx)FnO$iI&0UyW5QL|>SDN5Tdjqb%{+g|6IUfq*ziEx0 z+2RYs&Zc=EpQ3m|iXNXIM#zBGXu+UX1}CXI!ypmsk+r zZGXa-J<4OONl8z1DJqY&9>>39>s;5E&?n<|sZTVg4WB$x8w?~P%5}HnI0}-Gouq6P zbq@XaTvn_~ZL0arO(>z*xhrf-jDn<+0f%;Nz){t-Y{_pxfJq;A#^W)zS{z6&a~r5T zNWw@4hJ!1Pyu#(b3Py4*v?R(ia$}jO*Gdj(BF6mpT1oMzwhP8@hu(9@Y}pofFDb#{ z<@p7%%N|1e(%W^2cZqEIO*-*-va;Jn4SXRfBzGD$vgZ(Znee~uS>c<<9CAIX-sHKq z=Pj4J1;H`gy<<{jvE`!WYPkZTmfuk*DK)Q)$6(+xNHI-*Sj3KnuVcY6xw*2ixKSWmiK^7yiW&v8 z;AC@gGKO|wlbF_c+5yfw=~^Ee+Jqn4RqVgyj{^LMbc&{=-gw6Oa**yRkitWbMt;_} zO4w#{x9sUL-;+eIOYpv32j_rfSY~9p^T2lJuMc*s7^Rrnoxr0s#LTqN!U^86@_mT9>hGC zi2l;bc-mrl3yzzdyNpD`Wr;U?TxS-g2Chsr-@WOiInt#qzbq{&Q6sUT^p)6b8@P-F zW3a97hhdu8Bn=_G$FY_l*}B?fWm`Zb1HM;94Vj5`xuNZABezHh6l~Gy(zUVEQoDXB zIX!A-6r{S#K4ZNtt7&9)6_S-`p4-;9y$?^7RmZKbUPM?j?eZeK9S7azxYGKIM2)EF zC%7YN*&T_|tg*S_(!*k!n@iSbu9qIST#+M5jHrq87N;S+2b9{-!gdyi5~7sveX2bd zh%v`lpC(IIs0y{RKn%AXYgq5n+kvpXq>-f*4%+UazW!Gm`#^&b4ooch96)TKn8R!| zn*+UBvp2lZ22o23_--ay9N2JkIiyD6GuZaW6P_Eb(Jt1t1oI$nv7c$iE;16?jI=cg z$sI1dmQ=UY6x)gpHj@4dbqe%Wu2EtnA4X((mKe7MPw!Vu!ng!C;5aG(^d^`&d;gdp?~>I(Wn>e@iBKZx|J5bJz+mO#x2AjaZa zOGP;49qmEiVXCet(f%>RT5&SHkp}=;Vt6t*mo=@Y0CJOeM1xnRipQTk>JF{9nF$Iz+K3N@4FG#|l0e?7XR>k4mce3FNKqiJB8sEUNlPhH?G%zyck%Vc zaR^!XNZ9N~s-Rr>XOeN7i>rXP3^Yqp<2(EuzaejQh535(0@y%GPr0u~0?0OfW1%{$YkR|%JqaS=7DANe z)ibU#mPk@OmlpfX`G_m&0H@1Y**Z?fj|S$Jh*#q&nJG-$Q9_ zL;{1+)2_xZ0~pHSTyA(s=DRF;W0QVu`%P@azNHw9{l}_T_DXYpUfNy zut5|Ey(v(hG2!Im+=pqrJql#IOzU*1muO`nO}42lDkRA7IJFd(rF+sJ-^HU9J4rg! zq2edQb`^kdXCvkI!jtw=aaJ*MnE_}1%I28kFS6n~x`2mll`1o6=_x5da1a2i!k=l) z!!W9Pv=lj5+Ld_y3Tq02ok7kIXtF@tn)R@pXu{CL;Bh!%6wVVhgv}6qZ4Qt)oia2Rv;(7sk*dQDDN6df z=cPj~GfW?MaBPLRq$wb5sU8ARukxwQTMq(m%St4+I9alsiVD32)%vk2GM-+ZHC*tYFE-yz$OJQTp z#O|_RZLuye8**U-3yTS7~ZjiY}Qxfg~xYQ=P7^^U?~EYKfQPqA}`A7s9T zA<~s4^puq)Y96CUef>8;q9#jgXhV@Dr;3zPS$XD*Wb}=pNK#aj+p!uyL-MJao4xuv z)UVE4&stAIjf>6CMJhiJKoWJ-Kq~3=03P1E^pW7P4edV*GaD3=84Pkpe|GY2K@e<= z0yh(%DJGs?KmhCX?kW34^h?rC8N=}YmU=nHF*n`^vVkjgWwqDC~SS zf>`mMj%-K&{C1e#X=* zQKzz_9}XO>I)d!+g15i>Z3q#*`q!Qghu^Nl*S5zRd^qx$QdDhG$Htza_Pimq>~`0m z-_z5--}R?2=QzE~GnDrj-m@lg#WKi9xZZCKz3(7@4z!`kC{kJ~JxB#3N$sxHPnyW? z@lsq?M|+eU9k+bCl`^j1{ulM{>FeLrRpNskFl+m4BSW+W@V$Z725UX8VS4hTWM~$| zJZJV=z|a2xPI_<)n#TzL0P`mac3r3w{s2I#8uaJMT&_9ccR!e>#JhAyTWtnJgt2vo zR@x9Zr4ss5I)G3;{ZsdXU~1q#M)%iQV$T_U`$MDHB6dOsfz2js1d1e(bt6UUeIMf=vur*f%I*p2l(7df z%#Hq%aDMIOE?{eY8W~#cK@4!Kv9VbN;_f&&1be0XFLKmkwFOa^ZTG0Mrq2hZDGUIz zmeJR(xKs~F*aVQ8r<|wA6EAl3n_}C8XNdBciEE5P7-T0~aY;sy>)rk(pF%ag0alWX zj_l$|UE=28&`{p|1V#MTPGfXLxxu7#(4;J4!8RyEb%lthVb;e_-Q(w!A!i20jDmR|!6i z;AM@m@fi*=xZWkioBZsX5-|8ou&~jK4-8?Y%?^-0K-Y*<7Z;TwPdMX_Eqe><*a{o; z@A|%;gSXSZw^1?J?c7wtQ}C!d7v-ry6S8{^kTtS@lklo@?yMEbG3B)TO}Ko%q7t>E zzkZX_q4(?$Us{XXBqg0I%~}?b@Tn_W57dpNL-v0E0G{iejFL>QYenYJ5J4Wcpb>l6 z8|}<5j$!z&3H(YdZZ;QU;u8s_h`r2or0Pqa=E+v$XEPnHx&@K@MUL@^VlmZ+#({9Z z->u6Mt{(Xn2HhFdDZe5=TY+hRqkT#VPfCuqK&wy6xQ~;r&${8Yu%P3MSyJwBik9M( zN|s4~x{q(!t&Y`9PEB)ZwIRF3(WH*RHFKWYbUsN+BqS&Q0Dac|d+MdDhEsc1^?SV! zQ~v;NZfV%=dumVv@%%q;pTfb2j#oRhjqY=^7fBPz(25Okc%TO{Lv1u)QS`6&kLYit zJU0cx=HNb&aNZk&fsBWT@XkNOn}*I}V{sEW&A?@nNpUY=eA39``6;pYVvab(!8gOt zCpxd->~TiaHh$BC+rM&uhxPjZ0M}e`{55iFUuo4_VsxcRQvU$#q%aUWY?56)^|CeJ ze^c72yZKf^l@i`Tdr2e$4eI1l2iLr=-%b5Kl`%1Yzbe};!GF9spvUVb^Ktdpa!0*( z*8c!#^!nGa;jrQ-2lw!T+8~IYLC_tulgg}KyqCl{hZg;pLnKd=(a%if0zsL-`J02Z zNC^U*Kk~_|8`a*rg02$DfJEb~bc}~OR5d0pzRs~VEr;K@Nk{}2%WG0m6)Nf9QBhD) z_)fdfK11ZW`6QJ)CGRms#ReK+^s@#y2g_xI4J8Ob(m+p0&>g!9mJ>2%($6L9gt_vT z&eDZHK4gRQ-n{qLPuE9aY#j~MxonDM62EmW2LAwA#S&6K_2N#6-+gTQ9^WC?g9j&Q z*=LYS_qE-b3^f~Sjz+kW37{NF1lEU5E$K(?UkQ)&elJ9Q4&Zo?MmR1R%zibpW2467 z_^vIQGZT)*MTNn!-JEYR;h>HY1{J19v>Bv(-5kanGiA1wJnGW5b#%H&_>Xh76Wn!u zb~I}Cc+U}--ZOZ;T9pyob_oe!YU-_o`wh0;LA zXK1I4?R;I?8rmga=cHg<6%|O=fr%2B}@TrX~xp(oLZZ?L+-U7>xq3N z{^B>j-Rhw=$61qh+k0`b=<0E#lu!4Ilp-W6;sCaw^7?cL*H(vbomyL274#w}RqFh- zxbO$by4?J-Ha$p?0r?)?{_;CjL2B=op$<62`^1PTNL#6Dea-Z?G(IOBZRL4q=1EdU z{_X1#kBAn71d+APY0Z*14(Zev2RXpD>}or^3hsrmKOOy)WAW1BvHSzmXTE6(ag)-1 z4U`gWHKh77U3OeZv1H!r;+WvM*QONX(cs#pDGCHRafqh6_2^1nBoDt{r?pU7JR8a1 zX*h^>=A?NN7w)dLbBa$&RiHo*b$%Ni{(9DozSaJGq&^+FU0XubQKVijDPPllKJ~CR zPLu%o{VV1r#$-nmF|5A#3QTewOCsxVTAe!}f|By|HR=>K`>R4%W7^VWnj+@5*fNGN zp6v?IX8P~4+jcYdMHsgFIqB0`MY);jPX{%cg;)MqunmA3Y*C^)DXvcqa$43_?X`m; zN&#zK!Y0ar9YI6RBp$>Net){SQ^UN7+Ll#lilen`qU8{g@Q+Yk8z<;Jf0`L+$)>%P zSrs~U)_fs!itM%x*5J}-vh;-cjV(!%fR8jNf2a7Im{{W?qT)AkABU+(qPq}ceeIv+l zTcE6LhZuU}5G(=*bRRH*y?uR1`d8yzEt-Y0*VwJLg&w2j-61R>4&?s;>Oeh)`)fzj z=TxfmcFEp-1*@IO4S;%2+V=#hU>@L)dK%C^zPJ96Wow`z#v?5)pvbOzBOT@EM& zw50`(ve8OmvnvO z36)4wcL|RY<7sFwNH01%m7sJ1-=}JvESUyH8UmA##gerJB~D0X*A&wSYUx_k7J4>9 zh$N3fdsZwq9hWC*BLRC!W?<^f+RE0|n+~!(`HdIoSif!_jWR9;!Z5y!@d&*g;E!zh zoCo;E75L5|`DTUq{7l%!aO^Z=5sAUUhvCc(!hzOk5x#iall2~#prNM1NvRqoX z+sE1VSgfIh2I4kC1B*U#dQ|923e}=|wY{2=r*W=b5%oPI9l1(v<`mEbvfu^4tthO% zNg+xEcKIJp^-NpLGL}S%lXAQ3vQpqZF|LwR9h0)6msFQRbUJ+j?tgIaTRF2LZEH-s z?alc6Sm{*hU#bZML_587`W^hnkItzu#KRO!ghwV)@CC(*%$gt*&cg|EU}Aj8Mm;jrwW83Zh2M|1@mzj5YA zCfjngK*40CQ!MeDGh9?;IL!3DG_Ceenxzf0wi16-l7aH6^9thIwmoNse!g4WD6%GF zkl?Mx)bz>jsf5PTI>;z%rNrZni90^Dv2}L*$PsRmR6$ky((X3`kZw|K|PMi`^Cla^Y~s{ zl0cO9RR-AGPI>M}xHQU8`Wud*gtYUmZ7NwvAcqIV*b(bcY_{EEi;&EbCDvzI(JALv znB^-&tT&}>7XBpyH{2hf-^#l93_~-=L9v2BBQ}Mxme3%Qc4K!M^yYs)`f0?md}oYL z#BmbOiI)>3vbISJ=5aDf+8HE}+ut+Z_c_iaf?VQi&07oBh9;qicNY_CZbz9ZxpKCF zF)f6kw#t-0gh@+Y`x!V!@VDTE#k3u*kL5l<0TPc$ivM$CwyXOG#N0k$&l$GXgYJE;q2j( zvNy@wyAd8}NG~0k4Qr$?0jFN|G&$gIYhb9wDN67Qbt?V*C?*6t67cs0ARrvE~AjJiY5@;j0L%+VInV$Zhyl=G$Y&re4F@X^`{r+ABg9 zyH&B4WA&@}G&$7Yn0tZ}w3Qy3%MY$RipvkKJc`NZAmS;>3~Y}($XmHz5oUzj7T64> zs8N!iHDw7x6c7}Uw0uDw`~CG>xR(~RV0JIh>u=c+Zi|YGL#am`=+{=1Qwj#t3B({=PyN$)IGSS%5L$WkOeIu)}=+M@;-kRO=%-%G6 zrMmBWITg{FO(B=tx+!s4EGbR&grY!br9EAe0r-MX^`O?;+gYsf1;%#V_Q8#!?ULhh z-MGYVWQLlQi7mL*!Z%w{P(nU6wp~IL5(r62-*o8Vw~Mm7r-OeOSe|EYnp{Q&l|#FU z5fq?_mS?0gsq7~~B&AKVloXM#QRq!fHXO^8ueVHA-7*cr^6jKY7PuDK21|?T(#lYX z?k#U0C2CTE>*&^pNl4VM;v*5wCCG!F%;U_O$83sB3%$;Lwp(!m)IyTl-Ep9hrS&L) zrEHNzS%CJ|cwk0fZpFM=@#Vd#IF4mzgL;)FDpT>I=Nw56I8X{yq^S2KpHtioZ(De` zN7#k7WpsgQ6+pzWf>vSIkuwU_w?3JH`=1E=3<%0{&ipb$TQXI9}TWT`{ zXX9GgDrgk$MR`cL@euw-%y%^zUQTgpL0IHuv*8`U>c;TkZRQ8vIcuM4w&v*U0|Qoqkagl%)|X z=qX9sf$oHr0r!E}e*J#x`L0IM)Z%wkGM8*OIL@(Z zwk_u_*EsigqdZXA5!UiwY2^i^5|t!sv906Bl&?Iy;y01!iN@~+*K=|lXBkhJ%TeNd z3ULJhj)Lb_+}?=*Y?>mm(n&icpS=420EWJQ4#!}F>)d~r^VjM9+wcBgbtsCUapm&w z2LAwTw!^vHVnMo2ixPZTvR!StQCxRUu;XY@1+}F_DJS7OJ-(YyaW$t7wqyA0el>v; z5-&x9yW(Ml$7)=JGE?WtZKR~Q=~Ii^mXb$WyZ5bG_?efa@)j!+JYgzCi@a9ZYgi-L%AllEVZ2D1IOFG-Rv@K*7n0KE$5DiaHN*dq!khhh$Ie?usd%?h@$@h zqw6zV&zG~1Gj5-0loxfIASo@$ZO5BSuSnC0K~qXAQj|eD1fRyiyhu&Kvp#IWc=L`R zN05f5CL@obW1-1zK9E5m9>Zw|VDCn*Uy?2P23sfekzu=B42y-8c6rvw?wH7Zq_N}> zoq2sLJqg;vf%VY_tRKV1FL{^Z*AGq6l)_@weD}#HT#r12wDV5?01UOb=~9Rv7SF8_ zOT3Z8Ii3f}g3~!@UD?Qv+)|d-wFV=Fv=G}VDILdGeF6HTRc?HxKJ_*8Zn+f9wEYA$si>QUGyfA~b7PrqQ)M-ILQ?(&!?LUK~l53f?s!j$*) z+0fYHsd zA+9b(E2#(gR+@Dw8rbS92fuGkdTaJq3)rqzXUA<>qmZRT$L}(px7#kwyq4RODF|@B z!a5h`q?ESjS5iPyK^s1Xsq;ISWa~~7b;lc=)#^pH%RHcYmt+&~equlZc zIO`E@(`KSv;u486BNPT9vPeI|ODaBIhV+7WP*;*2jMJ|;vf)Tb1fPW?umt_Xx6i-R zwGnfner>*UeTtBv@cp{&^#iwkdj1Ez7kcU+Q}Xm0KOs>_?4d9xIwYkok14j;`MMTW z)~`)~*Z}&E+Ih#Ko@ zYVLM_Tza1W0QFWme}?a8`+~zxG@I05gwOYN;+y z{{U|%f9$vZXCAh2zr=U>5Br*~&Ci$W6b6)~Z>*_GQi!X?DN5t5bfqaoN}MoTB%FsL zk7|fkBs~(~;tOD@W8I-WB}E}U_tMg}@&~yD4OE|31L1~zYiBUn=SrNLb8+pNBdL_O z{N$nZSZA&}3tdU;4!o2#uA%z-`BttrXJPPoso2u@!-tkev;s?<#yP+M0VI+(qOr@6 z#_=mlqb(urX<#m4^?)+Dk%C2wdz!)g&H~DX<~SyFe7l{(1h>->ghday!jgQH6Xh2) zKZOYDU&M9Ye{yS{!-LIb5og?^6o+4Rr9%UxA%M%sao(*RhMn@;UjG19OVK|n%x)u? z;aU6%7{*m=vMtBd)xJJT+bqHi!FVsuefGdvanPhAp!MjIelhavD;42PQd)7OS((jQ ziGO*D{FL69%psEtCIb_Pdn99H$r#Is zW1_>!aY!8C3~qbc{NawzOlxwqxzUL9emQu282%5Bejf`ovEf5;*k`v8F*1>oHViYQ ze1k$9=ENA{DUNujELq<;(jqwhjE@y&Jj;W(Lk?^-lH`;)#DGVX5THpd^euZy?0$ba zTOPJ#GnLW(gQ9$&?+qs$tp?$l9duk8vtu{k$L$i99i<~)+J%GTddY8 zjwNtnbu%J*TavIBHJ3Doks-3B5`?+3(i7Zubc=x=4!Bo3#$)76WcO@M`HVWFY<2=v z%%?(7r7km(^_ppM->r1sKprUwq$BI1}y+l7*0E*lvn9lVW-Lc-Qh9TrnE zmNm^92bVmyh4Q!u2IHJ}6~%aLKZe9Xf}U4)(!Ka*ys+nNpyC@7T?_NRIP`3w`oI7d zEN|AUjkX3GaBAv{P1-CbAGv#&Ei}Uq#|bLy3)vw{2tE6yN9r{t#I#*z-W`yegKxDp z;;%K}9P0tyzUq#_9%DgnH?n{`zn6|}jD?+81p|IlS zHm1?#x+0`uk7bRTN(;2kUNdlg4n0egAN0eA-Yp7-znuh-yOAvZXNrU1Ti}Z1lXJfR@5^k+If$Z)1k>NS}xp2gfVt#mn2t zB+YP(v2$3~7Rb!)%u9o*;h?m4U1v=WB{m*sU~qU?oitcnG)voS$yC0%Gj9#Rw8I?w~twr4*k z*En|9cf=af;Kin^US$;hR@ z2kmY;wobQ}REAqs$ueFi-d$Q&3q1hZKqbElBS0jpt3>@Nx3h#vmE!xu&63js(3T)& zQf0`x+FD9wsfAkZYySXDESG)@yI_pA0$4 z>{#OYFzqgxq(tYrvk~14um+ak4ug{18y$eD{?h!`=hWhxvv9^OZBEzJgsu5khlvOS zr>jx{?f8BlyfJxAk16q)mrq(#4%d*&mZv^KJP2}>y{otOLEL(GsppMhCEpx;)p1iG zDn2oXLQ9|mq6Abgqhos4ntL63f;*8^5Z)ue@aN-uDP^>>=yNhljwJVeX^h%ZvU))z z1#7ofx39Xo#EDF8b)3>-qXnXfp_GwlyhV|(J-Mx(&$EhB8U^7vWwqx^mPWn7#kMs9 zz#er(R-K+9{7G@gCwoOW%O|j0Qn(i@#Cv_kY;?F=nEPfaYC-BfAgyZI_(>pr;O)WS z8qW0G-zAkk&?7sKMs5YZp?|WayR4oyxZm&BTg>}|LfIZeH;pOS zjKzZpw%bmJo(pTsN=tvLttbT~9g5P#aO^W?;PAFXD`ID9P5`(`33Py(G#o6qmF3xY z@i^>H_-`4J>~0)N=;Lb!a!gY8KsNHzgRD|!SROe zcM&cd^PrHa4>={b8ENLwqJ;F4eif&yMP8c6t9MQx<(zE|UZTb09$AMK0yL*2p{rH> z$tVD-AD)opcYm%+DtbZgKvu@6>(kb;iOH6JB+l{NRz?xWnE}YI%!x9m+vY}kC9mC(mlB5D4x#-m zV016ZK_~~|Nhf>^W;K$@Gn~jGY!kjAh|C!B2oHN+B`s^mfZSLXuOz1d!*H?1EI*55 zA?@(k_ea>i2^uZ1y}j)DbjxIi5=(5-qpWdqpo_KE>vb(gv`kz{4*BgN%ap{Z2ICaV)Lup{h~4cX4Z6c~cJFm5V)1ouPkVN|H1TP1))bW`Wa(rLq<}y= zsgn=KnY!5TQSxXiw?(u_CE0Vl)U0JSsL|&L@Xrqjq_Fa|pz+WdL08#~2NLFja@J<{J&vg@(D# zz8~)h&A8hkyG6U&CNyo=CR}he87Wy%YI+b*+pRi;bdW*;*KeWT*V*;U`8SCV3tVTP za!TRF&RI)M-5rGAlEuPQMXi%12vXKiUoC|>r7NjQR=sObJf7O~KKl~OCNzsY_uFIN z9rIm7?kJE@w56pKsP`jdr`O{7R@UAEF}Lwg4xCYLXeGZo+Zp)`v$VJ7KIvE>Xn(T= zk_NTCZ*|i2PmImPgshSXnld&?9?_M~IhfE~&sH+m1DUM?&%*@s#Xb%hBZnj73}BSG zpt_q>VgNa=-%>~*T`QHzWIb-ea{|)TR|7t6@U*8zv^vBEDIjcu2?X!A^HoV^R%JhGTxTgcLoJ+gu2?zm&57yb(_(gh`Lh>2h z?+3(hDWR9P2I#jy($Z+@m}#!MrdJrxdWdY7ThoIHCXLZ8Z?nZ|J>y>sh6-D;!<=7NCehl{iM8iaK?H2J-Mdfq&s|p4(AftGtZ9+PKKiv zMHEKm%Lqq=$KC`@{CZGR^km7uKb=VqEQo#K?y0V5`e1 z2~%rjd)msI(dbA7Xp^uvveU;NapD=9jys>((&5LT#cAuC=p~EnHymOsqFh%}v?f6g z`HN6cNhw(kr?#*gbjB`n-g;!a>lafkXj&zxDYG6seLysz1Ar-IWD(V&uT#3rud-8( z?be^tZcPbCa^=IelqF@z?Wq!4b;82PNgiQgMQG?9B#pv)^>c zVt!V%J*FZuv7ib%WxFSJi_$y?4}*r|3oy-yHb;IL(Zx-QahBWKX_NrkpmKsMQ)=G5 zt-N23;l3f^TzJOaa<#zAY;E4I(XA|X+ThO)zQl)B<+uC_B&Ikk1$`i`WB>>iizVfj zmo4&IM0GcNj?<00!<|ZH1+WNpz&cBwcXFhuL!cmcJFL-rRq~wGJeOp%VG@4Me%p*7 z%Xv#nY}}chB2#|rB&kU&LuF-MI$puoVs%(;wpnCFyUe-d(9&YOq?uEr5bG+~{{T%O zH}I78ZL&!qdUx+w_|_|jLp-3E%#4(Qg@v()nFE8I%!CVTn*AC9CBlxGmfr#5fxIsR znnt!rV_1nW5rgQ;1Pf$=N zw@UnzX>+F@l+;{qY*}lxK!mk5GH;^Vi6ISw+Em%-3JYGg2?sz|D>mDm%M`cU?y^Jf zILhDhUU5s45&0`~{U{f$0~t%!(nvr~!8+07j9wOKjE=(K;f?dPqA=6=h`|&C;tk4P z(#v*3kOvDQ!s|zk{B(vVyAIFZH2xk(MrQ8O9j(OX#($Us8c6WFqf&AXMziIxt^qS| zcIb>cJ=Q9c<-H-`-u+ryi-ieD(wsl4PtK|xyOHExZ!uc6*d{jfF+$|HSyz`CNpY3F z76JfKE{4iU1K>*c+tZphA$Pgjo74Qjz4m!cvtiJL(EmuU^N! za^$nJtUXfy05_R(?`(3?>oX-lVMuwWI@v>RIuw+tPC5$8017DyQ2Fd(;*nu7Op^~D z6AE!p!x-a{jxc5*3ommnDjPMqnO5aJtN8w;~XqEm7sQ95mY9?MJWltUT+=>Q0+^2`TIepjc*AGWl?}Lb*V? zS=xq&T6OnhEr;Ed8*%1d>gqc#$}OQIsFigE5*Q^G;(isz z_>LI4xDatp0tp({xuwhpFz*xl$ClBuk{2Wt6~#`C6ot4qSTf-D-KgJJithC_R`u}aY^hzT7e@~pMy6Q zAEX=!jchYO-Z8^OhqM9lIOA~Sx*UegIV85WO-`ZOf)*(8IP4TG!D4V3V=U}Mk8z54 zp7Izh2OiQk@7@D3c3K;)dg)46>E%?eq_rgo3sFbH7t$7|`xF66^l}LyD{O2ArT5X+Y`;vAyzNu;wj>J?F_t zcdcm$U4l{*v-fYPYkTxN`}@}0ZOPK?wMB8OZ=TqbaCMa}wnLH-l|HlZCrOT7QU}5} zufnQt4P&_N*Cn&f&lHHZHYBk7sF9}QKN%_o)jAiVQUX)lAwgaB=s}%)M`xIL&+;g| zbQ)e4J;?r4Sm_a>1o^-9(b+^6I!glo5`u6&JQU;)L?4+K8Dj$zP_@XStPC|6OU?XXB((dxuo zt1|-p!l<0R+SAWn0Sl;*>Q<#`J9RqN_paT?`1hm!9z8gMpF)wopgjru`=0&3cjHkj zgcohdjH2U9_lA=22yn_s;yKXnyYEB~LP#E(?f4%0*w`A_ z-Ark_M3Ehaq%;~_R4F}ZQPaSZLXOEODcK!~1Z&h(oMb1dB#z}>$8BrVU$^#ax7}T} z$w@#@N&!0Q`R`v}p2_>amvv_`fuQ=^0xr$z%lmJpyKf#NCv@su!ZovO8VeiM^EIgQ zr87s9(vqa8^r)Q>Km|W7A4C5DrTwv1ZUG4d5=iympx4vW)DK?3Yvg`JZ0i zy5Gy^T{&|QR5}zwPy&G0efs@Jr{DPc_pioMnQH}?629LWRC;^&^!4rgKW$)#0@q31 zrxuZHI&WH=09R42yKbg_D{ZG5w%eReg6IHs0{#6umC^|gyP!Iht!+g7BS&Xr_HX6$ zKRwj*0V@`n=+jE+WRKwfN2hvG+1WcAKEE#JrFAe{>e7(Xg1{+CciVmbhkNz-b>5?q zZXWK$=!Go=sz2)ir7cAXJ@h?xe(#lf93B;r5?BLX=9(IFZq;|bruy=ryv{mvFbr?1 z*EQ}WYJ1J9KHYf|7v-%ker(QjqeUxN8t>a)gXQ><>A$G&1GFq5hg2iU3M&cHSNs7a z_ayXe`Vpb(YHGBh3dC-k17Xz=M`8)zriX98zox2Dgi;$R0$RU>1hudj{Cc|k`j6{( z-`9SdS=`3q#Wlo2NuxydsyvN=KT^3t;nHH7G(E5c?XVSk-Fx|U7ci<%mnnc&+I|G6 zHi5S`QN0y!Uv|j;alRv|NtUFy&`yF&$~pvZN2j*^e!tDC;!A}SdWmJU`D6Gd!CKG3 zy%VH>dwqR=o|X2~$#dQu^3s644Y+mby>=VlZ=ct<%AmMVdxNaFfG+5O(kyBKKH?SH z8X76U5F;Z%K{syK;q%vMlxFtaRH<$&O9U3Q8yfGiu%qee-|+|Mb-Yb~#4MOzV7qQ@ zW@9S3F(TOJF_J+`t-8v`LZn5P?@Sh5PP&o&8o?ud_BCnjNJwrlC2BoqVLhAdlf4p0 zVe9(6{`!-yFCh{h4L0HPBstVUJ-~59_BsVaS_&tz@AIn9E+P>Gm}mA+529Kq0ixW@?^;P(zixmrOK;6Ne6oz`QVQOMb3nM_+T z)I1;|tAxqPR-uS89dx$&SO5l(Li$fu!bY}h>mL|7UIJRdL_Nl<)Q8O zZCYEKaRhh#eLD)G7|$W()<{}0)$o-A;m>(tKn(1 ztn`kd&_;k7Ac7C2e7PmL!O2r5szjz+K2nzAmR$|(2EvxIq7sz!j_2vyvep_QeCsXkxug|+B~ERs%$0E4gr*dHzLTHc2g z*8;~yn8x1mCc$g6^Z;_Zv9UGNojekDf#N{cNL=AsH$H1VWDNl1H0Nq|TM23^B&|&- zIn)bot6>BLkVi>L1G(86JMY@KZP)1RN0Dl2QP~rZLcGK!;uMk;g3|jb8zWuG_16A% zEy|A}_{F0kalG!5l)RM>Us8p4-^*lZZ|CVyIM#Xg>G5Vd*^<+13hQk39*}fTPwn5_ zDw4hIb4i8Woranr)HKbdSni$cZ*twjOx7{gE>|{-Mb+e?VBdHe)D658^!zF6DpO8J zl9VJR)|(z%iEs5tSxN7{`UG#dsiwsq`x@IIK|}Evl0G2gp)8*-XJi(_lu23a7B#A* z((efGhZ*$EjZ5&ccFs z>%QChY!}iymLaKaZ9Y+2oEve%uYi>u>0Q2`9K+$y zlX!L&w*LT^SyIM5kVaIiCCW0(k)=m<*BfbJVId(Ue-C1QgS}Ds_xp|B3sGaSDXHs{ z%ca4@)1W1=+Q(N`$sH;x&=aJPSmVsRLlwS}$9 z&e7q$u2RzBkk@AG2eSa4({WoaSbRUo7Rv>e`Hx<#*%T)&@+Kjhe55-rD@+BcUigG9 zOAZ92fIc0Mm)Eyy=ecuCM_IN4Kwfwq(S++g?-FYm|o7 z@pP&i>uambB`a-7LXPy6E8zn~4^NeLf12Dbx8fr9Mv5dyHVJ0GG%zL4^@aYiX$Ma;M*W&C*>U20J7IKewlYULX0*CVT<5eguDWX&!b!GUh|uV|G^Hzo zxrtZN;gVEo+?SftzP`0s?NO2tu%MuY6cnSe2yrES`--u8$$x@= zCVMdB%(FC1mfLR}%pQuS03LgVHY-%Jq0~|e0$tX&Nj+VM&n!N;@+&MpxbiBZTCdjI zUO#!VJsxexAt)_FmRwi|_0s#27o(xCD5Pw462ASr$@rfNWE_-oKeMiIqB#aClJrNE zhM#g-K5CZV#23(yDQQ~9^ra+rBvrsQZO5&XmbRLXP$u3hY1% z*&UC{q>1>=y4M}tmfJKp7VzOi@3Q-d2~){hl!cIy)C&4ZLC_l5B#j-lwyGDi<^}`C z{Lwb^%v^zJgLcQu_RAJMaV@QxPrQ{f%(Ob|z$7*u1QLY>1b6i{laCyEHylIo!IxVj z&$L~RX5@iuY76mYNmS)DKhwocAQ-JSms(3>q<~PP)Jg5G)Le2j`_4%&Mb(JTMYz0_ zJf)W!b+mkFQWOYqE2mxhJ1e&RYbHJyaIwtXag!Yj!QawcxX;`$Wy@jNL zHNE6(Ui;%r7*Bi3fWJmHP zHxZ?6Ui9i6k`U*b5YkqF8~6_4TyD(UlziIvE#*jdMY-nWJornlKDSG*xKy>LvXrG0 zus7fC6+(`I1L{t{4f_7h`>WaE<#7$Si!N?uc`${2oSCFsQp2lvs5F4j#1qk_D679w z){kl;vSlQpN>8SgKp#QA!}eF@b|sd~sU?*uZ5P}vDPOFn`p-c>c}~7(UX|CPDcg=* zalGFgahDur=B`oRY35bX>ncNuK4KN4@ebPU>MJK1Zz$xfMlRm3IThB{;u>l-zC_Zr z8KGzlx~9^~di0Mp1orH&onvo^Zm?{ei;G>ZcH?$|Zn-roMYt&`GLYKaQEzJabfA(n zckAi(%6@CloHKug3FkPXR7nsfwLPD9nH)B!83eZ5>F7?{+4z9!2iHc3scvJ<+5Nr@ zYfY9l!Im6?!Tn{=|nn>490G1R@~DpD2_r8y-4g)d17Y$%T35cr8Bc*efIhhz}r z%UWZhNJu~GOd*k^r&~UOAa>OeN-*pKh%; zdX%n7awOHpb6#{umeZ+8>iE!fKqFy8Vy+jMv)((oV4P=$@#_xz&8`f)rfG40I$3&7 zM)ZjRh8bEMT9+C-4MShVr1cdnbdQ~|kP#pPqEfTmp8luT*Y7{&=tUNmw;l~Fr$buo z4`QG|-`}(HH3yKAhY}CSfPZ*Cn+MbH_4WEx!=R|d4#gTN{{X@R*Y6+T><0T(HbMia zLiME%$FSdH_z$md@)Skamm*B2W2M!pNMR)nf=+hrdtKh z&u@)+ZtZ3JUCFlBqT1Z|pLQ0hs|x6vQ{}j11QL_wbvM4B*6&isefI93LVo>!ko-Ze zRQ{^JZ}WfEL|-9V01fsD2lY>&J0B8M2qc5p1KUGD)IvK?NJi4Hg&P|68U*@!jr_l5 zLuw}h{{SsZPgas!(D{5scl*72eg3|*Mbs)<9Xsq1q=cPmL0yi=r)_ra@~$Tn(zOq~ z>+coF`+>(4uS34yf7iIzq1gxT?MHzsPW@C0hQ4FBf4lFZE+BeI-+tdOzec{wwhn@| zqBrZ`UH-}X{{VuBzfzSQl#fk*^XNZrjX|;&pKaK_dP;iP zQjiEEY3d!u>ST(hn<*|SN8>@zItQm9Xy5xDy^q`V z`F$%Kzr%O4{lR0G_-^)pxG6@IrD?4+s!Nmq0Ncsm`z`+fna8TNRF^0J0JoFB_FMk| zGml$1U*bFbhyBf0X6MWG3Ij?~x7JjpDMVG`l%;Xjx>A&)B~*SV==}M#7kY~!6MXce zugz3*4dD7(==h7iQCUD5_=l}iimQAf8Z@3nwHb(7;zfycoh!?5HsT{hfcw%MLX?#P zrkP6D&{9IeN`7^#je8=&;-rug#z8CI8ZE`GV~GF_Nui({l~OJf3?}ze? z$#b8HX`L80%1m~lsr|Dx3PW$WwYILcHsdROD(g`iR>t)Fj=lSiR5O?GU8i<;k^~*Esg=yvtVC z9AT8HEkcitKXvvtu z$1^c}FJL4%1*L&JGl@wnu>4{+%Y~WEkB1ns40H0w6I+Ogv98K9%p)X%?97k?MFEc% za5?d=5SqE7M2p*rgLFz?X~?TfO}c|NG`DRIkWB0pCE*DoFgx%!~#DQg$nFYl+ zts+oZlB5=b(i~UtXtqH8fR7d%5Wj*2(QBich793qNZH9n!+*ff13hh^C3rzBje4%I?BVE}l0rzDDs) zb)PCsHZ2wiavrz43S;eCEv?B*VFabcsSG7PY4Y@^!o1QHKw0WFZWyjfidf;?GJBhe zRHZcft@9(co~W~~4=tBv01_7BS#|Xl0GAYwlHx$=3aIv3k6CXwi=EO;1)>G&L#vFz zY50#?*j$99qGN?%2HW!zfU<_dfY}w(XBozEwxm4&I&r3EK_>kkY(Gw?(}F`(jF$36zg+|GPurg5Fl&@h)UWsR+NQgb*G<>9ed zsJO$%BOAnLo>yVybQo!I1-+E6jjwasWDIF64jBN-GiXHs)z)tdOr}3PEw?AHms_jF z#-c}(*+a%SOMwe5ta%QfG!+L6XG%#rC%4BY;L~rm7#WJ~4L zZ7XrI`nxNwDGn8DdONAYRwV}*wLp^!>Gpl%14Y>oU6$J=2y_Qy$y^l#x%@g0mW6C3 zVUyUDlT!{kk=t@d8pUoB;$E*48I5tYMQOs8Sek%#r6r|~he8}*Ws;J65Pd5vc!ng1 zahN%~Sm2e$V)howFtHxYvF!3GN(g?N$w0KNgm^0>v7 z(XwG6V`DM0+uDay!--*gt*X%A;q39<$fn^D=GQgYuFAIBkn=NT#ggk%7KMZaC=YjE zLJ*=pFgganHJp!RitQif`rzqS*puMEUAE}OvJB+8@3DNwSuz&-93^d~YFfH__0`f; z2XJc^IbD-1wPe?q?)E3|mlUMNwy@lHnRm`{X%Q439cXn?&|4u0Y-}TB8q6QC_k;Nh zCh@(E=2yRaOdJCkdgXjn_KQl2{m95nI6Xi#*l{h#M@o7El$9i%^=$eKE+hOyiEyk_ zv5gppy5HGM9&sVjfav0g49)DC8JiH{LKi+TOI)!GGe#~3hd53r50TB77MAy#;XoG= z)QaWctCzR`047#9;+qSXIk{JSWvnTPckFgMt}EXflHcp!bmTj->=oa?cdCz)nLAM-z5?m`%Q=!$;J$*Dt9*R6RCLSPWg|fsY z#l854HaN^v#K#B5Bx|I2?E{ONCIdT~?5@}a)Q<%_ak9$EGD9H`k<4*Dv~2^r;B*#E z8naqz742{fuTSy8;>uEGxb$mlxMUE*+*g$m#AEh%mJkW(1NdOIbPrV$c6KU}U@zm> z&j&swrR1^@-amTCGaGfnV*V7NnFvEg(BoswJu3*&JNfeJL%Jjgm zXhR5!;*eB^lEH1p&@>WIldi#4yPvYX_nGj!lH`xIbZwXCUuH1%(o&^Max8f2Qz0d9 zI;5erwh+@x(LxXsQk`nCKgzzv9}`jIMU!QRXUnb@#@GT?zEe%ixGEbxESHqhmbLkW zCtwg%es!E*0=y&2JV)Ut^AA7AU&r%N4JTcN^I?3BzzJ$a+6rNE|$>*@!qdev}n zJWq&Yajuqp1o%M~3lP#Ljlf9eGA6OWJ17o$E-s*3t+mHDgcht7JREXaQ=~57s5D5fy zgRF@1Yj~xvW+R2rl0a=P6stD*P#`y$ zC;33VcbeG982sFohr#1)I_2+^6(ZpE>e$SLP7dis^7MvWLvADxM%0BRY3u zWO>$^8B2U1^*@)9JEEUX|WtRMh1K2=)t?CvI839F@-0lZ^& z+4InePsCe`i{+`rhhHlUg|^DnR8pimmeP_Aj;d9--|{FjTY#HHsrD9Qw5Nz^*)1^A zfj(=`EIZ|>fJ&F!TJ;*QKRzRk@bxS{EWpeAKzQ3P2$X1E`fA`#Y_txCL0WTpGb{#GI2d-(YGjy-9sOQB;U%LvIEK zkUXWW3+PDD=^z2}t!pjuMdOcdIZSKJt5J+evdFef4Y>g^lC`hR^8<-|4I~s4fJXff zdd;u1k13gw_;a=2Zc|r2QM=q?xo}uZYM1VkB(jH&fJkv(QdCMn8~Ffh>b`?vq{HB_ z_^eDFhBu4}aSx54v_}pUv8)bZpx1Y^k}AuNi(emv>~LR-aO~XaV`+z*DZ{$)Kz4z) zNdv;FmSQb=mA-Y9++)R*J;aFeVVKV*N?brr&qA9HDLVMN(X^;~6WrH&Mo|bg9md*u z_^+KRLv3}YG5ID!0!qStchr)U>uMm1p;(T80t|_5US_hyHk%Hu`30uX*Z}r%%^)k` z0DOI2v`IbcdWpzbB(A@+?r zG(Zl^zPp+guBTdBG0w+~@wY*xopsYg)C0F#rBAs(ia4W{u1rkiUO2GjA~5N<-jx}t zPoa^nhTT(LPaz391ug)N-kSBcob%c5;d?z!-ol)KOMMA;)p3^*NC2LMAq`7tAS2|S zodQmi^u6ECtU57RL%|7i)FDHqTkpPwYy_Ql*1sQ9Tn9bE+1N^wNCfRA0Bj$hZ}LAO z>0Ie}e-Fe+CzlO@nnyj(?Inm8wCD+q;D-lLtixfpiz9K9#Q3CUCTOOF5fkHkfsD9o zljoq$!Rn?m%t#*W_;LCGd z>QjDm>=hvi0ip_yv; zJmaCp9Y9Gd?v?bV(thpwmlrN0PFyFoKly{Iyv1%d;Cr zg2QN+lI-hJhviB?3u>Qdyn z0hX7x%2I?TJ8uQH5`+Z~f;!5E>&L;wIJ7^9z|H{0PWv&*A(t~_cx2P!827QxaFwrU zad>O8um)R7!NGXOIf4u}GB?GDm9NU@lKAlrX)Tga#~$pZ;I^Gvn9=IC$biD*s~wgb z_biOGf}WQr#fd4`ToPk~wwB8bIHm|$aa(jGlhQhspq=%t#~YNHFxia!7Z#FbE%K#_ zuLUHf$ZaV~NJ;qA5R|94eS9?xjmMXEMj+}#OmrYjCeQ;9=` zBd2;^l%b^SwQ@n?XXa%#^Jlx?qeZwvW<|~v#jx~{Psa2is4Hn+u1V z6k&%423Cef(&n|r&eg4SvE?rgvNDRgU9n}h-y^>SMqyG*g%M;s1%~9sW*UTpAx^f? zacfdqEum^ENGb^@wv73YWA@u!#@Qkz-EstnU2MH`UsR}1ILi!|S_BXxscq@D(Y}=- z!_W%$W!ugnm17d_HvDx7m?AR;`Jz+7bAN9l0dFe;v7m~*&6a-$Xw`* zQ4uI~VWNhTgJU>t17^+Lu5>(O(lU4iR7Fy*H#vywi{Ed2UFCA4Rw~cwRF}*@`-V7*$m5Tf$uIA zx_X|nJf_}5t1iSH2rahLfT(qJ?L=sLeQYboWwy;~@OR3ni;g{GlQiIuwuH7;k*-U_ zI?Sh5g5ye3I{Fe;kW{3jM}7NVisQRZIl<$X3nWti0Euw2*TmMw$2Iu8J2Ovq_v>wI zK+4moa}C~>Gm1}-!r}y&2r#oU6WU^yi2fy80STFvn9Z0tkV9^%W4G&(PDz5~zfGnC7%W=PlRWR~4Z1!I=*845N zhZ@^ww^~yg(;ZObI3lV_i;@~cY@z8JbotJ$D)R?GD_2LuO_u84SMY_!o`h?yM5vLj z-Os7k&c2^7oINf&>a(GAc^l>f^$zVNQw9Uu@kNa`oK17vsmvm)E!9Uui%jZD(YIutxXv*8aY}yWiHZce6F1*c2ULS=x=Br*oxp)LOV1 zM&y7hsCWg2o2y(cwBagB&{PsPzfwIr{XV{f z=if)y@nof43JUb?t@ivsko|wb_S@?l1zjNPtGB4w{extHci-vWzg|+GNKh$GyAIkX zTJQJ%AG)!%!!Q|3UzcZWK_F%x+Zs{x3az_yuJe4TX|F!3^@ma$S6Y;IK-Y1gI|6+_ zZ`bRgUwunyT0@8_T1tUI3g{Z&Ph+rtU&wn`RV`@yt_2Z6E{jbnjpuz@JSGnx@ZnsH8Tbq>xHL-{1Vh{D|#voHNYU z{x(#HXW0eTM?g#k$39c7x;8Z-T`MYRogJXIph4QZf+r7#icC=LCS(!k0rE&5SOr)B)+Qz6eX(Jt$9>h0X~i63UREgs5xLM0W$f zqKMblzb)`nc8a%KY0~Uug7IZ5Jsa8UB_rV-k8*X}x8Lz1H0DwOTe6u*1Pz2KJ;!aO zAxb~A=>GsF$aDKRrL57g7P7iIz}va$sMAhY$vZfM(wZCQ1*e6024yY5PD zF7^$!LNt0%7UudjqM#0xKN{=8bjWdRFpuHXQ_^2n(nijXl%kRI^#i}*T>~*n3TMb# zR1ydQ8*nHA-9(L@dY$k5b0w1q%H+kDkg;dD{h4lPNtZ3=Ty2yzuA&`M`atL*2Sgog z5%R1OVpv?p7x$fdn(mV8Ao9}aS>_evidQksV;twVL7QWAbgn||kCKVKYPhi*%Ts+z zUv#>TuPwgEvQ*gCwdwc|m2q2fuuGFJ&!MJT1x=Tn4Ysw8$D}Pu3s-ZnHhYuX=T?}; z@BKLCjclSV^B`c#wFw8xZOCXsR;P-c+7z8B02J$F?|UP3J2G~HH zo%V1NKsxmVeMi^%cU@Ojl(H4^2Lb3CI|ILeU%&7CHKSc;7F6EHVh3^t_xB&C)9L%` zoi-!2*Rv7UrzopBZcgN#?0eSCF=aW6-s_-q8=&? z+_l?f%VCKY*PLbX=EQBpmXhO2AoUcGb`Q#^E(ytb*BjiEdBO6V&MR^VQ%aX;mZ;A- z+o|Yr1v*=6Lx?0SEwuuYq1(Bwg#DTx)b1$o^J&QK+}VfQvUaA}?hnYHb+pI3&B$*u z6Di>wA#4<~xe8ltEz~2iS5}D{8u%eQv5#!>wj3ji0g&c)X65}fB`!l1wx7|#3-zz@q>QILYTF~kw z=;+sXwrd4D5XmgKH?+>zzN9hdYi4Y3?8pJ_w&o2Xpja!FgW;ImW(eYr5!hUjG4nwt zCkru%y{vSQ_OMSNd1-NJdjJN3pmbKLkzjY6S~F7S$+q0>vw#rfw(|>9oqre|Z62?M zN1*k35#08!hA)j)JjisuHKr<$2f)-aRd=CLmOlRJaonvfmYUzU^tsSG&`+M z8G@aI5i`r4_eXgRlUCPN-mN8-T!zZwc|md(%MOJVd2HK*i7O!aXj6n}Y@HpeU=NTQ z>?(lX@|(m5mi#-XmdoeK_eKy4`0ZL&0l$X&j2 zFK>rZw%B1iR78a>Evv0XNG!@Ln`f*dJ<^ z+WmF3;xtmC5z?)Z-(r>AEMJ;$I$~WS-l9uu%TiuR^I1z}K&1%|{{RG}tE?WttG8<6 zVtpLNvRo$=6G+J26POxbATErWr!mX{?yx~InKEgdw3=I(FGw&LtVSb;2?uQp+S2E> zg{~$x$PRY7&SqKw30lJ?cLTK|Hj5pR{AJvU87wgRXR{F6k=ADP$U~)1`3gqMO%{R* zv_eaRTL)rLy4f7Jp)!>rhmyC84f!D;sRaeZxOzfu3 zAC1Kjkb~UycR!B*0Bv*4Ze&R&1l2r;m_SnFJ_uVSdLWQMASox|`?vU-VvCuFdsx@U z8`?o>E+CLwX5FnjO)trI%(xSRl0L-AjWadz;)K98kYoYTn46=m)LKrqd>2-frEu3d zD=K~iB;;=Kwr6Iu#+d9|j&0?-wizzAr(Lny8+o0Cs3FrCJ4q!Z^$i}aRx2who&)ch z1+$N`sMfcW7BrmetjbYJe5pvW+BT#qL#ND3AtfOGvDa$L53W3l%MY$Rin=?G7&jXv zW_KXl??Mf(B0yr_Aq^?=elqK0Mw6zyP2t7@OqsvI|-NB`9 zw$|FSJg12`e(#N3GFuhSeY)m@A#-9asSL(?bBR-l494=pLjb8KrKb8qww-pWyTIA5 zaf09%97YYw6o%%HdW$`;E-ct>p(+hB+sQ*tC@6!bl%*c6^(1XIBS~a?mEAI-bfy#1 z1LZCx0RI4Qa)0^(WOGML=(`qAF|88J1a`&VcpW+!l))vm?m6B`(O8@srY{?+>}uvXEE<;a5N; z>#;SMuV9Bd!MwB1J`b|Xlc=|57Mt!#hemb43wH-2x@Sj-7D57w)hb$BPKQw1fIF%b z-WhALT5 zRpv_D_vXSJeG7P$zqD-{aUCj7j>y*aRxi{@@A4;G^&sol=lc((ZHMip;HzgN;CPRS zoIoyIOxquYc`rwf7a6BSR@77_r8@39c7-Wl5GT;+YJln#+ly^MK%q)W>K)2~(LI0~ z`e=UIBIEx61vTmS`d2FqxciAiXe(E9)4c)!KEHK*Ed>ftR>FdaDfI(iPp9~PUX(@i zN>Yf5AE2FFNs%+@?`u+a^hpkBo0Ff?ysj8hO+>CL#t704-ZZUreJM2&SM1d>7Z01r=TMvl%wB$)JZ+h<3v+78iyRoEhuNZi;|Rt zfRawiI}QDRAHuG#^NrnPM(I#ga7Mby(v*#N+4zRNet$02X}F`Vv5{JwXS1o8z(L=A zm4Tu3-`8HctGDD_Taj<QPAURpqH#K7&O+P)!j~7j7!TvRKmh z670CIq4#4w(DNj}Dx_a#?rtYG+fYN^#V_s!b$T3 zVaFTze}Z&vD$yst&-ev8c07qJs#=z}3XX{Rr@p%%pYnF1C_3CFLs}a3BiHHl_1F1& z*BMfY-pL^LKRV_Jc%G2iBz(2A`+NHT06lXE6A9nCypyf(ppoD9e8>5BqAuK5)LQ_P zwEQF=QV-8x%ly5*VBkxo3L6SnZr{K02l4G(VW(S1N}F*i18V9{we#PnroW%-TqA3B zvPlDa-nFyz^!q+){NfUH(lppoM)ljIkO0xq z`uwZ5!-b|_T6>Yw2jF_X?%S@4Q`H^U3VN1yM!xgD-{tB1Xo{ioJ~_uWOJ)rt1j}8? zW!`cB0DH?KKN3tu*l&rF`%b_|pBaQa(QUTfON}Yk&zLl$RGbnvKE00X><~(R9TC2- z$G`c1HuqmE{bTz-m{Awae)lT%*8Oxwzj*%uAFr)`QizqxqI8q3l#O=>eaG{EC#8HO zr?Qliq=WMP-yz@E>+`RruVq0ar6l$T)DNLFMaFxQezfo0Y>waM@*`*O{z_7ayYWPK zf*NoHEFa*>I!9N_sPDIb+3Tu-<&7<=QIg^kmH8?pqg^Wa`zbmkXo~NrL%BKw`Ol%> z-LBOGzPRGtj2u~TrKR+!ot}cAQaAT}zZ%Cc@ZIeH0B~64{u{lY?g|4+Qnc2ZRVB&) z0PW=O{g(d#%;VKss!Nmq0Ncsm`z`+fna8c1FYz7zL;mKgvvcM8g#o20Tk9%Pl%gu} zN>aG%T`5XY607Hij3zH4a^;f_mway>Zu4x&+HUIVN}6IrHYxIBAz&@D()-Mzf_l4o zo6**d1FG%ezU3a*%Jyg9x4Cp&i+%dt4kJXRUjG1dv`c{wM^Z>yQw`|}mbD=Dr7RUB z5&%4|I&lMn@neLL#flt17I5;0vF16u2ljFX$@{lteLE}ruBvu~txS8K+!ig-yCj1T zDP!n6E3z7PQ(?iomYoO`cUd~bG1yjHox=~j-X=%5t~(a_7Y39>cvp*BozmfuuBi~F zvnd5{1h%9pq^HYpD`IxFXO^5^=^Dpz{s!$P;<)ztN_8qrsf=7yZzaTn6xG1Chdi|u zgrxM6uEwaHqb_Pgmp3l6S{Ixt=V9AqF%iiw``m=2rFUl@W2NZj4XFhZRJA0hsVMJL zqVU!)4Pluz%j_l;-O?JBBHJNyAu|!cuPOCFbt#a`;RP#cEkp@Q`cw)~lBE1E860uF zjmNeB0K8;P#C8~FYlvnW@e#`t9M9gFVstkdE^7yI-S)h@=kf6-;PC`8^2ggcAt?~C zvOZ{rX9$ZyxMyLCM>i>8Rbt8>&ho5+G`At%q2p_j5tpVsl)q@VKH3@c9&RIQ zLXoF-=E}y2>jeI&p*aT`US(Wv?VN(#>oJ~j$0o^2ks-!EgHvKF2`(U)lva%?0&{Zzk{nw8ye{q&4&}l@R2hXaE-&@ z+ZGcJ#|AO)EPSk%Ips6AJ6zVikx3AH*uiD-g3?yjGmLR;Lrdn2g2)>bn6g_d<%Q49 z8FQT6%pruvTGtTPQ(6U5k~18CGlw4CXa>(B8xf%oIS)S5>UA+3Wx*_}H$ZJma3yI< z*QN9lqoGvR(}U$!X%jBDTf4VrU`b(yvAa~l+E|ZsZLc-r>d>S)3L%oWULYlHp{10e zMIk2ZRN``r#JCsM(;<-#+CYNiOu>43rXeixU$3hMWl~Wy13Or7d1s)6x(+YZP2u!G?4#iwx5^{7bS# z37RtK3xgcYt`a%2%I65(9!6#$(a5y~SV^Li7BC5z86zBfN4iEmpyg}3#5fa{n?zCM z6&klo!}1nf#&XMi$L?-Vj;3xA3SXT2Y@r3HqCx0iLOO^8%23*g*Ls}s>~75c)?`~Q zDBT6ppslI0o$tQQPkeiUM#c>M;sX|o{qG%-F%cVi zF3EA#$Zek|%!b=8g(;xW0npmIPgle>RZ>gX_Dwo1(j@pe?cBE-P0rM1IUR?t@)>j( z?yV=Jr?1Q)Z1i-Vpq+}<#_>oYY{LmQBeQuh`tq2foHz+1I8b}h7|RGUz3gl1o0YVq z2Nb+ANhOGxjDfgjKeRoRa=11^$474MO9hI1Kx3;wO(`=LwhQIDWsd0VAt6s9OHyx7 z`LUMexkKI%;)8x;4<*K203{CvD0J^fs7P zMJ_u{#!@!5GztS@E+HMk>^%-XY+Pk4E?oF`39zx*mN_Ql?*+I>g>y@?1gC=+7P^-u zlt*=FLK3wpP9Y`4b#*9R{{R+PVaLokmyXzotFgP^EgxrO?XZ={T32^*Qy_&97LO;Z zsFfihVQDEU)oAeUC~zEG2+S=10Q!?8L-5UWgP6ybhd#h#JHsIiYjbx=v&*r=CBNHu;`m+VGBDdE&g$Z%&6jUs*IY^X-BXAP*(bkG zq4ckz$Wh?QEiHmkzK~GjRJ{(vY0w?L2;Z*$RY76}^*3*ar|r_xoCgkcUHIK_MfuwV__ahw(f%E`P%qoXLbtn2eOy zGc#J^4cOQDb-N0f{uzwn(2Pv4h47g~4(y+u?2xp&oC`&O8+Hv*x>y(Xxbf~Kbm#cF zELc#jmzb91d6Ll?xdr(Mebpt^AucqeERWaPiO@}A!rG`;i5CmA7Z)D8$8`)zoXch! z5TK&jZEY*fE~KAMm66+f^sCzTk>q!5YsA+iD(7;QCK~5NlLe<;GQHi0L*Yy^f~N~; zQAWZNkdSsr8zf`V`?N&EREt{|TZ8D1XuB2{(6CNt2;o>~x_IL_j4pRiBQu5x zTF%X&7Y2gz;w)uw%0O_na(3K0PDwt~G-Ok4(qcyuSh=D~S%Bi)Y>^rA7NsFdE9n0K zQeVVIzI7pFpKf7t#k%i0>2{3Vw_Ps7f6h#h=}LXxKv+ke7zFJhUSI2|0Cp;fV_7Pp zz@DkMTbzd2l-#&*$NEP+q6q6Z3F@yuli zfhQ;{b*AJWC8if8zPCt5)KcF`J%Qf9-)hZ&fNWS`9vE=^aZ-KiN4(r5Np%BFmzRx#^BWxC6@J4;s=V6gg`kn>EXrrb+vO47Zj%m-aS zpV(_YehKmzcfSn0d3uDG(;ne$lPM^WrK#Xn?1S;CN>Y@1zGRvTeAX0l6(vEJ8W4U=Ns zy`!AfcEYR}&LwA*Bd;iy=$i6msxp%qg4%c1gsDY96%>L1@9GDpBQW0({Gp9y0$I(k zma8kw5gI}!yrix6(yo*^#=;fVuu8npQW5&PHC=tC8Piv95LlKmEzMam>p#8R8!tAJ z`_iO8rdsN7bgYJeDm@`RK=pcoX!oerE3LlY_GH!=Vk2@C)tr#=acgI@>2V%w&lbXT zzcBQkut7T5`FEz0hkuZo^78-NkW2AO@qb+M8?+1d@WMp&e)nkZtT|b1+79Jsa3zc&lMeCBU7kf3Z zU6&Rs=~tK1%G~#2g?rlc5IO=!NC!Y{Y}D}|#P2*WEX%DqHTwGXsw(ag?ao4dQPA9l zpop!?csSr;q?I_<gmxt1!}7)sAjBhNk(bMF zEpyrfNNEiKvcc}VdQ+@?yC%`v->;v9Ajzm>3|dALBjULwk7aR9dEiMSJM|HbX4Lq0Rq$z1gLW3znn*J%O|3~^Q2|OyyZMhsB7?`W;rQPh zv}E!1uG-U5+Hxe+GT)ynTEl@!n!?7`8%RpaiFHaoaRs#K6;?kK8FniZly2hG+!+^- zcDBEFlOaMsxvi@_AlVWg^3bUv2tj2ntx75=Ncn2m^DaWirdVMv@p(*B8P7F_CNnA4 zn0;CWZ8q~NOr<`QfTeV~)8&8&>}!MR8?kuaDUOE(F|A`bgpz(E_kn^|$oVk+j4W_3 zaFDh@=XT9wn9^IQTJuLc@VqndCPvGLF&EDyeVB?!ntm6u~TH0T8)2;9W1Wiu*;tG%oUBOI9v%*HV_Fw?hSq8cx|s5ofXEMnRQH9 zy%Of!o(f!sz$Cq6b`%BpMowf_B2S`OiXm8fzuYtD~Oe?d*1L~=Cl%A=5~zAT;k~4-L5_WN*T z8<7!}=(v{(!ou8HQo!g9cIXGI;;Ay_f$|qb#<$yWqLlf zR_%EOrbaau>-LF&<(lHvwgt{GdH2@Zn2?krV){w_M$3L;$x2qSrFRv+>|{7cvAEd( z0278TkU;q%%LFnoc5Y^-rgVhlaiBqP%i0ngz2|Ae8;WEcVjsn|sH=(hQ?lImG}vl$T`6J01#2};nVg`o}< zl!SnF)M<6PT-k02oPib;Db{z-SY-z}^-+I)~)4H9~kqLK&10VhK4nqAuB z{0oD3CvJA>i+%&v>Czcf2dORusXY!YC>~%0sm_k2?_`tS4SvNjh%K=tHpYK>;VR167texO<{=U6TzNo*)?mWMtXh6P#EXm^>KU9%9!L z(O_&f*pGzp(S>9YOgMfTJX=IeGRuhVCT11H#~7HG7e*s8c5VO$xuK-A7CP5nFE??z zJ&RwUWGR<)_tu!dIOI9j#alwqm<3_EAh5K`aZWyy{4r^;8UPgwUG|FP_}%Nl_bIJW zV_Wi9haX09s7@)!@+3FO7-WgP3Gz@9(nBw;w-iTEN-H4Wz9{-aMwC3vfHsrKM@#R=XSlxHHCh4A5}~I}-ji!viQXDP4v9b{{ql zW(GC312VTO1D-V`KHTmH5a7^Y_;xC69FjzGNNw^KI}?YHwnsKdpJ4WRWD$@Wa-6Mk zc2G5;r72&iSAj~EGO$Y!_40jW{vKa?n(9=`J4T3gy*$6e{ZHrhA5rtIoFD%HQx}>4 z0Be%3PP!>Xw&3bANBUHgl!50s3<4n`@avb%=h`%3f8rRq`0(#4`8AP z-Pj#>-=X^bYw#4OvDW<$Vtai(KANvYO}IET)`j2Z=6s2|SY8EyK(1g4Jh#4nel`?o z*{XgzQ0n{k=n3@K$Sd}G>%acJUvZMk)z<35Qa98RJCu*r1pKx_zJz*ipy*HpAD7U2 zdTZ19>zm8~?IfinX<%#xX!hQc;!@UDq-WRHzW-^`y)?0Tqo z@7sUPU9>x}0zgS0GCuFGao_3wI)_OfdJT4BYc=VxzXRp%=&Tslv<~Umk$0GiH}BtF z^*>d_EfI59_m-np8o(dzTg}*zPs4-)FwN{OZ_OfjE4W;ti1E3k?DVjjHw* zDC$itGr;*{ENjaw9`j8&6PIsy^&2kq$jB}M7?g!9qy(#Aj{V3VEqysTD{;4=)YC%so>fNF>)$1=7Zq-Cu& zn_KbT>rbD>RZKecn`P<}Z*Sa@jhCHNh>;#hPfFW#BTX$yLH-d)Qu~dK>aOnj!`w$N ziVq{$^9SyCDR&gIn>y|7MNC$d<7j;jz9%}MwiH&LuOx+(gYboDW8KG`e@Jz21h#sB zC1+>8v^I6?+kT^3`P5y!viB0y^^Kd1v|FB6nCwucC9<$Wfe9ohe+XA`-2At?iDVDY z+Gc!UIlzJeF3QF*&;SNEDWRd)Q`c5ZJKo23c^f;-K))z)Y;1F!;sHLh%YbnpcUaIl zRrztp5I}DonSHUzZt8~<({PSLN|M-4`c8`a`gTX4J-t4%jnjo@-S5`3WQ3vbN?387 zX{0QqohcgHN3W>*R9A-b8QgP=vFv3GeWc zy1L9aB6B9lZC8wXG$p*38fJ6_c}?{=q?D=B+O#xPvZNqvZ?aCScLx)(Lk)(*xwj+Y zjAnHke2u(<#`bdqc6K7{_}(Rn#qi8VDreh^8!&6g4P%IlD+|RM?@$Mj)aq$iGyF4p zx7>1O=S4yU68`{NN`N6rNh8SzxkmNrqDK22RZPY58x^W97VdR*x#bq9&bx2EVK(;D z5dDVRs8@Llam31VOMgjQO#pipX!fr>j|%N>vhe0oB(g*_DTo}MWbBH;CYe}%WJPXn%xA6F(JWrNLu=tij z0G1izWLX^^wDQL|xJV;w9CB86f(fB~k(u%1IWjLeUAp$giKmN}TUGO+$D3ibB>65a z4ipAEr7H+3)`xD|stw3FtGT7kS3zvH*^;1yn6|VOv^o-yR7nWh(J5PSQ_*-&DOzlhjb)(vNVBZ)Ey@&z6bw)p8+Y zVjYubl1C8SrS)%XO^dC^vd6biV>UVX_L<~@D2v|rwkQFPHU~SrLtN*PR5x?2b(~s= z(NOq_BYwZP_U}Z0)m&w29}1jM0DOnueS7-v_k6yhCmPuFm4M=UN$xhSYw#Ny?fd(m z#xCWRh>_cRK-`zfWCnw)=fPefL>mWU@W(Eba#YPJEyZuT69uk2^kk*_bGQ zDf#u&gftvj?j8ekUdV6pP2-D@6+)+ zS646L}mq^F@MDm@4wf=N#FPs8)>K3%F=kkeoX5}kGZ`UCOS$Kl?-8Fkjwu+o&G zK+r%WY>~6CmcD2CcTyhdL5fOn?zl36r)gPTRyEh<%|l{g%vj>dU!gZZx4U4Tb}shY z#Z-t{DYhLg!M7tl9IZ#G1StznlAr5O#79?sdj337bFm3-6fxAW2x)s7->bdCsJMyK4YS~gs5&>z)Tts9B z(3B*U6^{B=l19iKws-f`yY3Mdm8pkxT|He2>Qa<_K^VQxI*{oR2;9g6>b%SB2KsT;p=xM5(Ejiv*VUXmA7ddc)sBsT89a&;SMFc6g z845^RRCm3Ruy?OYij1)rhZ5!4l(eP1+Dow2(g09TQkKC;1f7ycS^Dm&yY4$7!0X9M zKSHBX+M$C=M-ON(0PxPh9s zZ%PMFv{l9z4olu*h)8R`#s)jy!EPYc6MtK(#xcBRN~2D<c@+9x;QF{!jGGxAFJf}csY3eE}?zHHrpP3(C^O?EIqqpa;wTDX&>P&1*1j7A=y5Kc^SWa0>jRk9_H8xY4s90$%sTmWwKYT|I5IPRhClEL#<>y9Bs2}+Qo)TD)gtqz{&Vz$=Wn~#ScMrzG3HMBIh?Gx@MQ_M?o#HqzL z?^2dpN*qeT2rJN7`1Utdl4pcT?(uNs7Z$^}7rI}Kk20Oinw=2fWP(F~5=uf;H|f1p zfr|LO$ymW;*l)A#_RFQQwMVqu6&=qgpu6xcLX#axd2jU-)D+@@Snf62iQ`@?d9oCj z;ft@IZB@;=%))&HG{kn@QF*9}bqV++00U_R1Qe6osFFo;w*Dp0-r=i__Q`3w;_+e2 zW+1Z4X;CKJ;e@(m%2;J@2O~6FLXTfZT9l2Xde$WTQ}H$L1io4_iEOr;R%aNknx6z&v`-u!%sb~6UZjb+w-860YYY&V+O2#(cYZm29^(#jr_sWxd$#tOsDsTAWJU zaHSzcbPqzUZ2D|$iwZEDh|o)}y5e02U5ChmG<)oh{{XjrzLnCFv?Bo4WHTD`FBK9MZ+tVa*U|PEOt<#*r$D7y}R`K{Wnsm z-heyU?ceVI059?Puhna3M`P<>lts$J?#Fqj*;WFSC+~2247O&f& z57W0}L{p=A?rQh?rcZZQ#0ds-pdSAKaPCO?Yxeyn8+GdSCDbZ?#$)WQX)cFkIIy&A zb!?Ijwdw#MZ@o%ZymIj=XheHl*Gi6nLk^Dn`_chEpI@GjYOGd{GtT9qu*NHPa1a7d zxW1(K^aO^;)`-`#Zo5!MmE7nfCI4kfJq*^ zsyc~?x3q+^pd0`wuhm;R+Ioij4d{3O00xMv16C-O=Oez_lInl~^(+I{;q~boI??U3 z^z`}EaaTuMC_+%!N=Cb+A5Q(wzVJTkrHIJndMK0gB}AQ{+CQ7W_#zGDL21(0-&*! z+qkHSx_O|0phl1D9{s-)>-7HsuB7{dgO7p_a!&sM9goLj{1ipsYfFN(5=zwIQT#jq z0FUcjPPmrPw5RE#zmYoi1NQy`y3I)b2`#JBWE1rJ&b$2o0PC-ntr}Pap8o*1(4F`E zc0GPnMbbW`{YfMD*X4emb;R01e*H_XUn$;k()X;HA1!m8P`PsV-0d0B}B9;($+T%Z2lPX7SeZ~V?Z zY~g>1@A4n_HC>yZFVrXvDN5g2Qk10;SBg@V$6D!1QizpWygphNd*v$>CY3o42=`e` zIc{|f&28wG=?}Ey3vmvksS7|-kfzogNKy(&RZ&{F9}G7uou8Po>z$pXJ8^>&^CCZ( zo}{!9ZIE7nr~2Z{9&3pNfK-vQuE&|iT`M?86LbZlX5eu~BJ-7kK?}vUF#PIx*W#;FVTr)+%#pOd&($M>9TEyuNDM^loLYpn6VWJlQ z0Ggr*Pw+qkyOMAtHp4AeyN$q@tr#rAn$)dNg5&^|c}jewCB`08T_b5oB&9m?p*mAeJ9N^IQH%b?kwn5}*HbID4CT3|)Iwp%NSw))NjlX&5Zs|40oP!bhZ~3h^ z9#k0*t=9e+z&6gxm$a2s!5@vYY+~(jcOSmOyV`M;O-$Q#M*Qp4R;49}rN>{Au>6?r zB`6Ifwxl)Cu7Co~O_{_;wCNeFyId@5rBIbioE*IGHZY^BnHr2`XnR0|;hrGnL=e{}QJeI;X zkg$ZKV^mJtm)W@!Gfev=3yv*igw)0)+gu0=RJRgXWGAIb2nq^uMIk3aJtLAHPbP3C9SJ`$jWuZOPuUr!^?4K>U5G;3YZLOp&!1Ib+b&Uqe zPkoILDp!YTvfy*A_Y3Xa%i1QXQl&F>Y_lOMeYN|%rc}yfr9nMwJq0Q#-^5Au;J#-! zMBOA_;$l(pj8f?-xK2Ne139TmoV6B|*mby}l%%ai(t3hbzLC1BnHzSDqQ{NfWZPT6 zA(->h-RvbS`OtxJ#R&)#ji%XDs8sGHR@CkNMV-&ccq3pP!Qij$xC1bdMdJ`>|>8_%6Zc^yW0z?x8hcK zme!aD%xMlSa>@!xN=bO7Ne&a!u92^$ZD4p-9~bEd1?(E!L~vsvGZPKR98HbVK`;+4 zlkpFf5+!&6tu)qTHhMVr@aS-{z8Nr>_r4~=T*fj^#J)b#Ib+V|%i5PQ)<7{dlF?uQ zvs3K@$X>^MC~`{>6r@hP+hf_{mirey;V&uBj-U2kQaXU`pglVp)5hhIUQ zN8!R{^3%$<$1>G5gCopDiG?V1woss#6c0c`lco9{?YZp&yTVr-uF*Ma5u-<&b(X-= zlu*!Rq#lw&cejWeKBlzj=l)x{;rBN38(o>`7H227XPV5lhXumk>Q&Jtwvf>tY7&Gg zf5HG16{Fg{@1{+k9go0p>@y&h!KAcGKy#fC%Gl#Ktd9lM+BQHW04#H%fV+obSR|4c z;xSW9aSV1sBaM-Cu6xB`-IgyZDA{w5v0lT-b`lG<_2Al(4rei8c9md)89nY&yL;hmTQWm!wy{S z#xJ|2*Mfc8BD%ZvK3BDrfIb4!KIM0>pU1-Zh94I0!xw{wJSdN|!^wv4gORU$aT4Zf zCBTCl0~^?u0vzJ!G7;R1^uQldT@ugu2N}B0WUX&Hm5_C34asWP}w-5GXUL(!$@e%`12a#sH zPL73j7Y4)SWG7_y>RIUooec`p=a5v&xf>6WCB#Q+T2tq{zJk(H!CKx&We`%3uAb+z zy?YJp$LwId+Rq;HRf5-lP3Am2k=bpwTw9Js53v=;60)SEB$cUZNJu`ur?{`C@W)8r zGlj(oBr%KzCMU9h&m)>-W1QndICB7PilS<_SbZSC%`Xka&6woF;-hJ1dm~oIn2G_# zrQjFkrwclRt&w|F>`z<#M&EwL#~@yA*Ql^0tX$3Ue|4yh$bAasI&+V(ZvYOuQxGr`-57Ue#34#=KgZd3A= z)XbN+v`1;ZrAcul5)`Eor1u^5Ts&)Ln;DM7{u%SaXPRcY%yhAr6WVrU7wG)Pfej4} zx^|1Ffx@#J!QsJ{89Q7fCv)bJ!wUCd+6!V0ki4=&^45^XwS)?=LX^thn+wm69x7xY zzJy&N#9VgWl!dHFEjId|Wx`(T(QO?)K7+MX-Za8Ye7hI9UT?52cL|b{Bi)^9E7DtG zl^~@kTof|geu7o8A+@ZXo%&S065?-{Q06k*4clR~h22vn#Cc^u>{(J%&cFb(JlS`QirkIwLt)$Mkx|s?Nx{w2IDbFX%_`CF{tJhm5x%@s?LyCZrG@0aM zacFK@3PI=GmdTTH+=aU};PhgtnJHWZ{I;7L&-I@Y(c4Q(gj z_6V@}U*l+vp|;tNjkfDSqqKB9wK=2RL=YB2ij+>q$7<8jQu=adWJ`qo&UM0S9V>bo zmDZP^NLgFK@Oo3fgry(2dhQp(?x!E{wSas+>WH`;DT-*5(#vVLCRUHnUm!c^YveY5 zqr+rt@mQQtbBiRy=M8lL2@L>fF4zWLbO2QnRXrqyqXB|rS}tsJ3xJ8yynz{QNb2tp zDZScgs>{u5MdO2&h0nxSER|axxgny}nVBs-hgWV9m@XHXY@uozRDw@H2WipyRfc|h z-)%SRdRKh9TiTHvw*fLkMUQrSNmGT?vf7Zf4v-SJ2-d(Jnyj88Gx2-mjQ(ZtLV6w3 zBf1n8BRE|5b`}_H^>rtu&9-`$N#AdW)K!TbmT0}iMYhz-j>}VwnN#}RZMdmgJijkp zn^^gium+C1S23J1n4>(dh9^2koEX6mwYf;*Osoq_>9Cfx^kk^|WG_bU7#gw@6mGJe2~4x35uMc*8Ww-;Dd#_{+8?*7Rwq zi*#?KM{mn#&LOg))XF@iIo4DHx{ty>eq)c}v6!YhX(R!$!xRoN$kQ7Ga`s~520%mU z_mQFcG1day7>QxSV52yG-LhpEi6VP}!#(q{&`U6ML^-1Az#1ZfI+cAKQ(&;$tali* zBF26^>zBRTqezNnGjA^J`DwYgq=e{brEaJspb!BkcWZ=Laj6+zC3Kk@3Mboc4Yy{P z!^=ZTN*Z!1*imiJiD%1Fl!8Lk-$y|wzVWEZSLAHu!XIe4;g-A%3qN4sgjmsjHcsc%@a`nn;a|eu z*F41rE?F@z61DKX>~w99w3@`MkwJ$t;-#+1Va^TWE4B#*A3Yw#6$-2}-)!P%2WA z0p8S}pmawDBbZ|muTs#(!G%egwOXzWG?LkMVJ@N=Lfr)kA${G})S^K`NZF1t9}pOc ze-0&QX^3>J#4|7qggoXkG!~A~fFo;xpd1|F-~&&nWIetI1BBt@3fbLq(qK(k`x)^WDs9+kN)FT(mJ4caNk}^=>et0wJXSh6A(A=qufXwKc?o#TS`*HhEQ<2+ElWY6_e7Wk4~QZ zqfB0G@g?3ZSGrm5pE1l5^Onn* zk>~dK9`frA+F--9KA5nkA2s#++_WVt@|s%QA<#V4I1)6Zbe+_Pcz2GmHb!Ir00+Ubk9Q7y5-|QUSH~j;F@ruOteCZ|%GYIw z4)7Yr%+|yjWsC;X+)oFAGTZL<4n&t0JXN~u(e683m0eJkDYjDPn?CH!(1eeN_(V1V zQi_i1Bpi0vH7Qlr=X{kdhgPeEr(J27?;#B|g`uVyj?zndL0a6FNxVpip02dA$CR+*%9Nm$2NT@%=!Eqiyf}B0BOM!BW18EFk&u~4eN=#lcZh3? zbS$wzfn+al!#Gr!c%_RN*rgzo1}_jd3&*$_c*DAJ#;`aMJosqK z=KM}S4HQknmiEROfG&~mCApg(j$@mc`7cR9mxn%HF@=gr@VI&K@J`SgC7JBYg_jP- zIJKq{?$TgGdy(F^OI3@~`PuCBwUq$qsG`~Cj_$^QUda3KXr&>c$Ou8(g0fj@_@-(0;5 z1t|oM-6wxfU&HbKZ(6u@4_1eOq08^w6-yZ4;$6zeu*W%I^pHrc- zHQe_*`F(xQ{JgRSVN*SGlopN$fL5~5aiJvKXj9>4lPJN@L6QdFV8ydOdJKBw;dNFKF9 z@*ddTVXh>Xfts?B%C){@p51I?Y47EDaL^38QLT69%g@1bp*=-JkOy#(djY=M-^_f4 zb{k4c(uHrKBl_OMYVI^Z-(&op{_5(bX;Od#KqM%nd`CmMC0)K?gP?owzoD*T>QjUzWslj*XKs%Xf$0~7ZTSCq(98X@r0ekE|n*<&F zzF_LOR<5zsN=lR8L$_g}uT$G!FUGbS@sZukpn_?}fuJmabRBO_UIAlhc%oPc1Ojg^ z^f?WE+8-Se4E!BENJ@&k@7G)Hy`KL7hpkMO)}wh#3R05y>Xiib0tod;(b4XGLGCy6 z*_78SYEH+{4R#}1ABX*W*Veo7u8WGIP)I#NLt5|mE3w%1KEF=;8>BNdNL(zm)mDP& zS>KkRnk1cfS!Im5t^v&s01-z)>h;$3*M5Cy%<=rD3%hM^wiH2Iji#D8tQeg zC8!Iq;z?$at)aV~7{ePwNI9HCTJJIigUw2K^o!V^8{B3%g|-22%y~;nJN8IJiaJki zl%uETzT^{hJZs6fTP^*zAC}x(ymu1fU2R@d5(-X_!c+$S@P2-p?NMG6@bdO&$ad^5 zY{b|uDq4iMWH`!Ph@=n*(~cnk`s=Aceig^eTty3w@;j@8C1>7kvmJ{Ga%3`zOL?`Z zEV~(!@`$S|146u6_%S;NzS}6P(GQdmhk87BmAVb;mGvqN|Y_mCVF3oK21| zgg*}>3rw^e!u^^vhtt735o4c-zV1Ke942m011roWDm>UtmadIFhTn}UKbBiAqs(nd z^H7h83PIQ@2e}5UZ=8HWWJu*})!&YCIhL$`8_QzM;u?%pwwwgKmwedjO4};z0RR9r zHQb6TJ3MtZ(AuW5WRwRPd^{uuNoas556wcX(zabKX8tAJFl`CKF2 zSTK7GY#AlAJjv<({>A5(Fs4+0IVprFz8X{Rh`XH@>8O_&I!_>dr4&mV#lJQ605gC1 zx5qm;Rp6y_lcxlco}of=65umP%SJp}p)Alg%#E!aC5;u9HmuYLEC0X2MLJ8z&NOkR zX7pyvW+6>iAU(oKygF90qLURBrZiImupy$P8>D}EjSdZ1+r(n-?20Ss`D=R>C7h3C zS`$IWsz_a5ZzU_M*<0)3xyAm&(cNPI?sM>3?`pv-AqZPpV`Zdv%f!@7P+Xqknm*3o z8`8D$#gbI1_3!&$3I{gaEr17qjoW}*S@dOm2|Ko6JcZ!N+?!t>BU^ zTD~(H%KsZgX!c`4e-ejcP2oPSHthSu!@-s;c+wt(T#sbhI^ua%vtY_l(N@mueX%wE-VqprvHr{EDlr-O^ zZkSgdO;6jJEprtE_&i-6AJ4nd|=I6Se)yW^+yX0mU#2XF~s{IErRVCmq zd)TDt>QX*M6T73%#muYpw0Or}mAcs(d97jBj(vDhB6A=)e>o|6PSD z)~ll>Yl`zHpSM+fMTWL|--5g*rw#xmY=!+*kF!~YLl@hf#j3Mt-@5Jz!yCWZiaJ@> zXr5&F6`n=sop|MZq0&r`CsfpS5H7x{W~EGmP=x{t{0<9J z)~>n36RNZm?A`r%6{FUCBT-RuP`NgFx!b#gdI`g~sTU1HkgfzrmM=PY9-Zx1EL|s+ z9yu5*V?B%pC#wrO;?WBqDi>}A#$;1!?zJY+%Lq=#L|KZ{`v^{uLSx>qvx`>eli1@d zP|=ei2MRn2D2;=(;>zGTKS%}TI=Fw!8zA_PSDPONN8-e8zoy{KN^?8MsK#F{OT9fw}byOcjmE#9ekMe2=2j|o!@tLsa zxa~Xu7qbHB08@zn;?J5>v9E30X_NIOK@m>PbCyb#&KGdw^bfB78f8_o&td$ z))6oT?ykeiq;Hmci55(ef$^K5YI{40&VmwwZ}fhI02o;grP{c)_<{N0sX*>m@zzKe z-tTb$0d$9(_F2jB4+p702J+mIR|4WFs;Bcvta+<0gVC@lI11928Ny80@Cuzzf9HxX)Q>_StW6?B$(^>|rZGG@z)5)>-l)`T@={<55=7!w&m)j~sWwj2!H zDviFM>E*kRonpS1N`Ks`QaA)M0sl7z^FKU{v3Q@9{XfC_K+b>D9-c~O=3WuS=E|`D z05f@OIXY8WhTtyb{EGG~DjCBAi_L|Sz~)Tf&LLA*4Yww4>*%Vo-Ch2Q8d%CkJ|*v_iau4d*YI_83NH;3@DGfd)mC{+0>9pPDRyt z6M?l9q|;7V#b_I|$%Wl_amE<5ILtBnXsh&PZg@P!W^F9^u+}Pr3A>^G^m}OCnuER7 z{Ld1$B<@l!zPSjMM4Vj}FRzoJIQ3K$b@GdK08>F>9yDov2ZSsYhZpJyX1bT`%#c)2Bnf945sdqD$9ZpP8kwnNlVMf{uq$w_o-mk zY*;|f6+(M!o6~WhQEp@vY9@N^A~Ofx-yA$mJ8N5Jjmq@Lxc8dPL)}|h@XjuqS)*A# zg#Bo!#AqxL+0XDT@V4?YT@+_SHI=&#NPiu4xFI(Zr{A7>{2EVb{W~y1#}&r=w7qQZ z@4>&nHYFf?O(I^rkw?d^DW_$}zqW?=pV1nTLyt?VS5S*bLs2d`Y`)Y4qkyjzxq6!h zFLQ$O0&Y*BbivPF8TF6K(Y|`uLuz%5F@4_<)7|4y_{+cpy5?Zu%E@{uzy_N`N2d`^ zBRyJassmN%jDj17U-#+t`~h|_Y2ZVsLMo^t6!b4-W%y1Sg8eVD`B~*An7Kame{1py zo270^w;RhPHCOY8s}_-9==|yo(+pwd*;5)g$xKG_0~l}VfX=1WczKHeyyolOk7w`- zkD`k4KczgGC=Az;aEm@U;@cho$X%sS*i*;!uM1s`qeD8#WDSCuC9o)DGxI89D`X(Z z!$RXc`nL?oH>5wHmldZX@56uIH%Wts8wk?p-45V!R0gL{TT1GeETY<3 z9t0`E>~oSroXSc>bKs|zsY);nY+H27gWtiG%Ul_Kyly(@<$r*3LqlZxsq23LUfIjq z0$(zZWqCNZivA~@0Z;ezP2D6;6{W5iy9@+Cy%t3H&muf_*DK}C`jV-Q&U)C{I-#z^ zT(RGo4B=Dmo$EKHp^}#mU0WAbv15yjf%x2Y6M$Wk!_c)+6;rzA_m$nQqYc&a&lWZ{ zG<21=o;-nvqj(S0njT+fx2tkb7;-;o!53dHgQrj#TgA%IRm(V!e;SiVS0U}8&b{;T znz+SiA-i88ZOv&M&3kE>vpj3L{{Ogczfy3mJOm56Z`@UtXmyv-Nh&1Nqe9 zmDwNdV*DR1Tu)zRUOuZ|YWC+csongkE5 z9n3L=EIxsk-l=|ANz z_uHeZ`)ZVQY+GkU#g+fs@ZKJ3ts#Q?+1=GY`K2x}yGI>Mp!{gH~3ZujcxL z@M-(1^=vvhLr8gJLTkam`GE7PYDQ6pU@alCU9%FDj1tiF* z?4Y6^G%x>Bah~G@uca!3bL1@Pr$5H~Mt;Q*Z#rbab1`$o3%gpsL8-|vO(eQLW8oK-dhe8 z7c=R{CvpLWU-(6wdT4)b#oF0T5TpyYQ`z#fRh-AxW$hyS?W2a65?_{kr?%qvKbG!d z}X0ExqtRL=XUB_1I!HW&qtEzgiz0<#YYwPI#)lu;Ah>EBcRF!q8DMPfF$LI|5*-m@K3(yV&AA9li`N) z7rIsdwD0+?Bk|tT2I=)vlabEc33X&nr}H{;v^HvBW)E_7_^tv6$PS!*@O z`SC7N8tocZy1(RYOu&&SS9&(5)31*@y#G6jy$Uq>t=;;TO}H8NW$2z!mbq8+B-{0* zauu)gu01K|6cwz)H*)by2g3i`mvPs2so7OS7oVwzNe{7&`nin+`(|4mKXebOWzh@_ zTg#D#APNhZFxxUe{|4h!uM9JPQ+{jM{(3Nbx76@Cl6jBU?Jhz+j}g$>Y`Q!BN%LnN z>JD$Chp30ab)8}phRYCBuKp7kc{Xxmxq}ALo<5`1Opf)><(*Ty5Wc00aU#u}pfeF> zDmw~y#t8SHV*M^pC>New!Odz17aL+#=ZAb`N2l5jdW44 z6NE*GG+Iq?&&I}zX5Y(8$TmcinK3C=>(iIBPm!&%uJPu9)k+-DZy%k3ujvALK+prI zo8J?2EH+iXC(pv2wYTno)w!@KhJKqS<~0{(|KnW$R-wM=s}5DSEcN`BQEC)8n;O9j zF^mYxukDm=*p?W`ZnS^CZ12=%v3Mu3BD;(F`E}`&2AO6C@HNd0A`?yP)enoCi}R;p zz@mJq-P5arM+SVkZkzwx-rtE?!HXmw&F)rmoHLm5m6UVD5s1}{_jLWWB>9bs@LNw3 z_*;S;2}+hMIwr2{XHd5EV+wMPmWGY7ww#-flsltR_js040^h8G zlgU5x#R6x|xr2+W61G`R$#M?RBPLHRqH7Lv6&&|bT2*%OME=BuP&W9wSTy{pv0V@J zXG)=nz~E(Tm;gywL8cZI?@KKdl9|*n2nUWR&4d)5B;j>oDSZwvDQ7rM>yOeTZDF+J z^S!EZ`&l?_p4^ zLD=SmvMvja#VCMVD*O(_P=({->k-}8C7fIDc? z@{d#p=$Q=k_ks$m z0vq9_t66Pe2m#hCm$Wp^Vo;-l4`s z3C%5x)~T~gyuWP#Nd2rKl>d$XM+RR93m{B8LPtW+L1K z#2Q1)1PkxJ+=-u7-s}GAA#OKEbRH-T`wo?B=lq5|tRTLf75HtD7p6@+O(kG77}kb8 zCXb&*^_;4`ElvMgpSCR$vA&~9TUo^B_ACx-jvFU;`?h*j^YUYFy40M@<$x^cIo@#yLFiP1!HH&*x|Bi$-updeR0#@%Fp9l9ZmV)_h>l`rq z1$)<)hZVOui~sL;HVFB5jSWM#a^zEbKvliLD$ZmQDIYY-G1v|lhnzXwx17#NtkDBOY zV8&WUifuOMj{w2;;j3Zbzec`+=1sB1&By-lK6zOjI!rK7yE6UuJ3#WCzNPyCyxdO;l~kz6|coC+bT`S8wyJZlc|!T*%LB->Aig%!j{xgBaVWBTu*aat>SunVt>-!y~Mmi+Lp4mpOUzu!XTPZT9P4iKQYZW+QaLE-@MgX zlvET<&3)OlFM(y@h{i>lf=huX&l=&Glybm}6DUhC`-W)3R$!t0PCZjV`}@34w)U|D zez6~PUYohup<%YLf@oM80k%UOQ|d3{J-R~d^vLr-@9nqyjP&sAP_gbCxX_2tjA?bR2{N*ZxQ>&c$na*Q-O zJ$X_Nc3wJtuffBjb}lk9tDZ}21dljTm?*hX(q-(#B?4Jr!<4I?Dx^Qy zPS2#2sx`M^@{W=hD38Xl=kEiFV%M3_?I(pklGvDugS0$-U=_#|@?p@PjZjuSh`O=4 zX+>h#fj4SX)Mhkvo3ViID~~>GC+hVci0FGM!dcMd~AHL zrx3@tK-cAu?yMZ}(!a=f96h4oU^567-*gH97SvvFa;n2YMzT)Z!o1T<{2gsx!Rj;L zpLT^^%z5nCX}6jQEljojL;CBroaD$n*Q?J3XA#@ji;l{L1SCG1%F6L{@n4NpOH6+$ zG}n!05y$^^yuy7WvGsINbzo5N$?s{Bz)Jf~PC&F6jIh7 zq0p2)6?Gxi!i??WM`(&uh3aWr8P7pzlW|G9=YN3bgH3VX>iQ08i?CQ0r!vJLl}z#l zTqSxXWdc7{Av~&}?|Ljqqmd-86}tiZbzM+e^(WZ?#?VVHD%`0)s{{h^Z_u%}T3W8j4F3r2urwh`*JTVCtb`oEYH< z#(5zealL%zBd7LqE6f_2mNCiUPzPKstoXtGp{zveX>W(jPd+~fqcJcK< z=V>F)V@gG8k%p4YTgHv^6IR@{rL&f5e7J+xCnA!AYxjB^b)}$Cszw+dcz{W{tnjXe z$@;~LgUM%wfqy;CGW6z7UOTTS$Z5HRA)nrh-s5Y<_Fp1oJgJ#8kRRK|Mf2k_{}8F> zG3y7k&s$R|wQJN3ILjC?#-NFL@pmD!bk25<=i>yNz~mFZrihC!rs#qeQz_372%gfR zeop-2C_dY!eqbrH9fz>&6vQ1nWqthjwRu>GXx%iUig&|v5HtTbqILFtQQB9|%Nm|+ zKdp~lU%O<~W~*43c78E{aj{+0qs>BR!D*dkO-#q|%9t{qixkuk%Mbd)&vAdjrY@tj zh@yEDbB5>IH}sfIJru_;K9`kI0}EjtQBlv9Z?R#Y+!FiWCjCzC`;!-#8I@aP-?BVN z2$BK$fe85Yu)B1jiQMb5)$Jj@Tf$rBM_Dan>|0a=O^RMm;}|R>{bJm-tH^+G-%KYn zoRlSTSeU3_{JMxjUkhzyj^sC|C4de@s3*aVo`2@+ice0d&G{;HXpkRHAJulJ z+xbtz#x%WlBG7AZp`)*(sP;^}+_Q#lHYT<+CW7L?8eh(D;{bVtf12@5d057CIKN_gp6cC$^cC#PABF9l1Z}FKFv@x!8cRQA6 z>^xianm9Aj0*c)UiNPt3QOpexi73QjPpv?c#Kp|)l82V8kJs*7+aG5f(Xk(stH3%r zjxkZSG9aS}x!5+y+m;$)>?Sh1QNc*0zbEF~PVi@+LG+tW%Tu<9tl1ZSu|1;1ef`+K zh;!5ZeQ^WC>T?ShbiGD>hRYsSJ(qXX8XeQQ0OCmF zjvwn0^LqQ)r6&nOxx2=i)zd)0!0^G|{_5Nyf)#fX+R^wT1s9b3L!!q73uj=Df!9+& zkR6NlJx-jMjGjiR&|1sroPM94ogGm@?8@gjI!d9(SyFHO2u@AsP2zI15kh(+A~5-q zyTTE#+5xd}dbh1jy5j$J8;z)H6x0#-H(wCm>i2VdVIxT!+1K6nBzHPgoBUZ>41L@g zl}rR$yW7o&j=0O+M%eo0p*l)~jCZqn3k+kCB_)SsXX!%HiotDR^zZnN{5G@_m+Xiw zI7K#uGXxj98Rn{k1;xslY}fK^C>CG`CnCpXuYJRM3^Ol?{!SuUyB?bBBh4eZy*!`HUo2vV>0Il~WM#CpVC1$x~Sb zf!A96-Paf7M}2nedIv~R_P6VFDT2-~EzR9q_S{Mrmr39pNKIZoS+m~>xf=${A8{QN zDT#Yg%0#fmkbYpB-d_Ajwz^(Y%wztel_ZeI>nSH8ugS8=$f^zQ?qjU5#@ir(W>eOo z`Db2C+a_dv!R-?)ofGW${V^mH#A~Wq!nZ$BLsHPv8tx}SuGZgeZho7-@Ub^3xePbE z(ck0AaeTm;%gTQMhMsk+*f16fB861aV1tDy86$ad$+n(+5$VAo?GmcPy5Ph6APhmw z_cnrT0tdB!Tu;l_C(Y8_8hQvoknRF7jM#A3OJWC^f$Z&Lj^lk`ilU3p7@RZv^SLfSNiWINvFt-5J}2C7Cm*w z`t?7QvSn>|??!Qcsnevmzx#wUaV`g^=>5}@!N4NwnPaZ1pmeD^_d|eo4;I4*Yc#sLm~B2zHk7uVc-T@NPS=T5Pr$_?n?5X)uxLr zz6%9~tnQsTqEQ3So->ZRPOU@EGMc3pbBywB)BU1xbaC@BWpDXM+&+r>WAlMW2mmhVa|H`?WKDc-7`zq#aNokmJ_e&>ZUTujOfUtQIv?JLC_ zHFq$jZvO`mGzQ7YKw>X^(?|#RboE(DcRKqgZu9(&#vTHYPyFRhv>)W{H7$Lqj#b_e z(R(1@xF7?+#L{HsI5q1BQ8ag1IiFNH=>tM#LLVz^hquPN;Vl{(6HMc^=O6xAHM9}v zFVJ%z%f{c`{RhaN-Z4MAt3TTH>+lHcdjI$-R(h?Zb)&_zsy1rG(Xo-BBaJH2 zvgO^`(s6d65CQ~wgUq;k{BGntOE%`_;LCATgtCo=WyS64W(YczEcBuO4~m-g3Zpl% zFnM_xDv1o2ynaQiImm4>rNQw|#g!>2sHm_*CSbn$c?)_`$NKlcf_whe7se4B&<6xit^KxyiNQdNsU8N)85e+q)xnX1R8&cI++WHs6L=ptj0FP7{?H z31vO-zSDrVyOkO~@&CGXd>DLiaDH33MBNK%s3=@ExlNh1VH?csd4*&&SA$m5lQiynjK48+Vtt~pO%_T0X1ZUM zuInu(-(T>H{g{llaNx(sbM{=}H9lKp-xQ)G&H`#z_4Q9n^b3m%ESzD^AU1`Ig58tm zvs8W5nng%++ldDJ{HQT*Q|^yFucjiVD3GL#GDt5ws}Y?BqE`S1Jh%^Xm(^8ZvGD>} zEHfQ%4~`dPAKe_;2=ls#z6iiX1U|jfxTb{Kj`-PXeby79o_r0me|iei2Be`8CyZC0 z93q@TZ^}Fn7V!^yVq|L_hLG{JH35;lBK!97CT;7Z+?Vzp)PMiQkcl)30s}7Ola-)0 zvk^-EWEC1mB^MXB0SlKap|QqI+@CMi23gb8Iuvh0SN2b0)y1TjeVFL2Z2A@L^q7~V z-2?e#>(k84Fi%9X$Qc)>4y7nmyqA7x5e_wQ@5uXShqcLi!N5g@?}kJQ^>5lvOrg;s za0Jh7DypSyBy4{-T}Wvg>|<$HG2FU0tGHSX66fNxi zBivHTf&?jm_2<*%Ug$5U7T^4R$V?s)@bDFHn+1qFN~q;AXA#$$*H z%oH>aqUQwus{>x)1Tp{XY{D#RK!OMdZ%*p&njROGb^GdHzP$1fYERqW>Wn*R<}}KB zVm>gYSc3b#t<;Yr_cNvx$Xs@$@Bw*|(c#fm9#?_7+FLC>vj;XzJMHT#USOd$VfBf{~WYvAS zwZ1nt4*qA&z{PUriMm_i9Zun*Y||o&h;sc!;BlF!btM-i znrmbq%R6r~NK6Tlv|p|n6>=HGU3z;cmOkP!ZJB6D+_nHQcj}vwJY|Z|wOLZ1-s^a5 z7Ii976(HXsWF{dAtSKws0o8TKzUjySalQN$mc%`L(i(|U(nwxbvaIHgzue)&%L5ei z>V-RtWYSBa;3+7^_<-fZIv9p^A;WNTuxDD>L+*mzzDfFLp7i*whqfXTYcr&ZDkUe; z?N#~fV<5S6{18L!i+4Jj?Q(QzF1|dkD(6h<<{|y%1|!2K)bjvTfBmtHhgntN4!r3e zZ1!e`0JNyU`WHaD7O-;1XO|tIiLOMKDcK-YLU#;gd96AJNn^h&4?_~A8Fm*T@MLQK z1ISEP(IOK^mCRyFhvent|DaQAC5CAv7If_G-$_30&(FA^Dj=%YkP+;dZMSXauU9zf zW&ksO@KV3ua$tei-hjl<29;*7(UAmxX4xBks` z!~6|>^@}aeG?bKz%?acstEn5xZNo}QATx0YLP@SE*~G@QIWm?YdZ5_&|EA6LryYFA_h1qUxnWDC%v$CDFKo_X76qf54Ntt zhA3uGrP6A#-zVjwnMnTw44b?22VIlv&IGaOY8O>_6!@De+xMn*KCr@J_-ncqGof-; zV^15t4Oma-1Z8C*}_UCs%~q zZ}WPnQirh5COLeno$p_ay3w&@){J=L1yx9FqwzvJ=?^O&QSm;#%a(opSYWIcb{-e-c$bV)$CsbJ|RPewObo%>kXc(ei_LS{M~h^8q}_Fa6Ewo*0hc zaRlnJd@bH|3LHRycD*&xyF5e?vR+*!%Ct~|3BdmW4w>?|yhqzIHZ^x?OS8dTb$2#DsWUpeA}W5%4nC^TIyr7Kvfj2kj(YETx+TaXbOiPoGJju0w%l`~!?KpVPkAZEb? zKR-i|HU+w|wohfuEJBcEFR4D1G)73CQ%l%QFeV8e2F~?{_I}NO13SS+kNF+lbwTfiDTrWp~@{2kc>YcO3pbjC#AeWtNPCH#o5hf zPBJ|5?c0y`1Xkeh7;q+ndGp_XK2kC`W@WEN&>ms|d0a9=h|wS@)8G>GY@9!tn;acs zrJ&0yHQ%SNgtJZT+7`RpsM3t273r~6PHaQ(Qu@BoNF)fa*k2?2uh4BlkaI_3HfN8r zvY1_@4e*x#x5;(Ww2EQ>tEOc$!2vj`O5qWaT z5V`m0jp6C9+0U2<%ZYW_6g^92>QSVPXvPp%VC-CX6u3ir;O6SH@sbkCG5jcirg<|Z zqGj|06A`Bc7_tNcD}(m-7b7PaWWTH!+1X}quf_la=W<6i6F6foS)@fPy_#ZJVPfPZ z#R1D#rGg(j8q7&=zkbXLbosz!L|pi)!;BNX9|UaQycrjaPK~}vI5*Om$4uZD=h`mVysDDp-j{p~ z8IEj*bY#YSn8ROKz-@BhObRqP*7r(W($rFCflk89D)*X6yTLOAvfNJG?>cuA6%oA1 zzjIehR#t8C?SBW7hVy!KT4JXRs zHPBSRKYwm4$f&ilI(zh@Joo|sO3QzdaXdM_4e4wOKnr2Lg(zq9=SvOhw53%Elqk30 zS=ZX`Ed9B8qhcHLehSAI6{ZL{_-#hbCCs5-b_Imt`5f%+shB(v06nhAqKg)vv_JAZ zm-9)t6YJ87AWq)P>&}8UfB8oP4__CZ`Mcj+eU>WUa>Q;G_YCPY_yJ@((J?XPp#-42 zzAu^iFQVATm6O&Qs`LFVh|I;5d5G{?FLHUKV3eT*q}<^gDthMfL_;EKdCruEzaWst z8dwlSZ9vxQ7vAyF7ZeQ{X&`d%DL=mr2oK?+6!ixA4;9Jcb+LN&hKT8uzb{8c3cdU; zg-V%6#cn9C?si!Zi`h2O6i69d;DWJ8_N%pgDOyf*;O|0`wLF}6Hk5l{;3QW-Kng#M zl2tsp(x>W4dCxt7dLho|H%Z{cu>pGFWN}p+*+9V2VHj{vC&7}q3k8T=_)6^;o#~{V zi5FjGf##VS1Fj3+)LDkS@p+wvpPc>?iZ@HX-Urg@LsOv*OO|$#sp-^O^V?xM9hPo` zE!zvkaC!eI!)R>-!Gkq=AXOtdp-%hB)@7;S-w^R-Xs;AmikC4LPzIYSjy$Cfq+}6N z%n6;8mcct&L%0MVVf>#pyUS@g;fI+a#skOp_>^q~NDGlLf8Edr++@3x)IbN%4FTL| zK1OyuKapGpCEuH$&wMwAWM%AQ;lTWL22VPESuG+5YV8+RUwJ?}wrB>X&{vQw!}Bk8 zA#eqZ;=%+=oHKim@^>bR`n@jQ^2_=3rHOxw_pT4C(JcL~wV8!m@~X_g4nrQ~jvv}F zLm<35TX>u6oarM_ONZ?X;qK<>27*;-TsMzU%#kd<=!W=RJUB;4!)N zd{^$X9rjw?>gD5wAQe26b<`Q=&*s!0YoBue0UiX^ov{M{0~kK;)tMXr2cY_YaUSvi zFE~%@INpi!yF#xwx{XAe;z|R053Ra61ActeQ`^7X70rQy%VMbf8r|{IbSq ztda;S!ts^HZ)|%#$71CIeFYm9q0^^W^V8w2%f$X;`mTFaj!f)2fvcB}cE>tb3^u_0 z%lr_^5lg!gmz0zug-+YC5AjKxEUHZ|XjxVxGzVVyp9(;k3~+}A3g=q<(?(;RT(x^7 zUQaaeVt@Gf;ZE-Zbo3fP_uhBke~`4Wcfpv64L5m+1pU22K1)#BXl`mDK8V^(ncvHHrZxJ(DwKm2zP4t%{-tjPIs# zKnMx)1F$ysl2an*OkH^jI7ZX*Rq!aQD(N1x)46b|yi z1uidw$gx;j%LA@K+aw3N%_ELs2PsDSt5@~gKG2dM#od3Lyv|RBSaF00$XvBZj(!9$ zp6ACe_RE=K7n)&~dgAwyUr6bKKR)?Q@2?{0WwV$qB%|R31O+zM2=w_%#k3eqeAr5# zZ3^r1F+69iH+O&DZjnN`n47txaw9Cn?S_!r@3C>oYvOeRe>$+&H164QftreYntlir z+^Y6z>D6)>4ihlJ1iO+WR;FUUod(V^n;U)3mVUm4d@*Fls~unz7x+PHBe0Ikf^|LX zXVd#GQ>0#*ZXEk-2lk~ULg<9)`)^Cu--;)5nVMLH-6V<`@%eOB^Nr)BH5U8S$*z;$ zs>Q{BxBV=Te5Rdw{7uTWpgLh_&$~Y962w%r~xwn=&+vEsa5O>W9tMcE40*FSM1u?UmVOzXU^^40Vrq6m@JEv=b{V&w9kkQ;F zAcd#WHMMt+K5^<)rykFjc6wtx2P;5Xx%`?P0$+*yl#9m-8`xtPuW@72|5|p;)91U3ve}dZ)$-WJV7K`{{tw-@L>Bx$ zbVNAT{|@og`l61aS6@tw$(4@ED#p=qa2 z4Htg3zRO3$&WH5eWyZvcqooeqa|!r@p`?O*IBCc^Zm&ZLZ1R`bczNauS%@V2nBzpX z<}2Ma*wynPk)~$f(0sa=(;F?~N-w399h*FQDo4+r&)hCy zTthib83L5j5JWhi%dmP>H8eab_rzDLHl+|L2U063+=TS(KfUJGC{`RSX8%TEC@PU( znZ|_$YS{Qye)bWVm};=b&=$(XGmbN~bfo-*M>h^B=WJ~+m#-`7^J!*$;w9rG#V#%q zds(P^`Br)hBXb#PZb^E84wW6$_ zZ|}<7R4fHUW%p&JwyT}pb8QK~)TK1Hp?`C+;!V`FFEO)7d7K0P51^<4qk4^o#kds5 zJS&xM|Jz^fy6CkuZNltYU9k8*dZgO>u%3wKKosmo=%%Y4=#`(gA2dZ%_hWx5vq8dn zLu&T=I#xGn?dbV4@0PF9&yof{Z7$|~Jf0#)KdcS-UC~_c;_I2w6Pn1qg~7A~4^^%i zx!--H4G_K#$x!FlWJ?~k0ctH2w4&YWUNc(t`%3g7n~2Run#KeR zzmNK}g(cbFBgv@D=$w)1=U`ob89|}>4~wSnv`-T2U81wLh}=+Tho7ol*%}W1P}9@H zmplZ8fYekicz?hr^C`3Ouemul?tF7Z5C4{zD!9oST{l=Mp#-JzzFR7Laj?da4!M?j zOcY*PezSN#K7K|@(GRZ)5;%CvElGOHOjN%#;F?W)*br+k^Ry{Ae*nUu50R7^G0U@p zf+ki4nRe(zt>qbx_*a!-f-?io)9egczc%<~4E^YcRE&&P`#KhO)Y6IxjyiSA7;WyA znGe;;y2InGPX&9_<1>$KgDKKGP!7`m>fH9OxcU9C9hnzCe5d)(rk{1MyHn>&Ga50Q zEFao>(Ry0W+h^8{# zO=vY%lf5(19@F~Cwnn;q6H)flOOp?m(YL0m-w^+hG9?Wjg?eb2*Wu^Fqg`eGo%Ft^ zWju~Fq=%F>y)|bjr=v`j&ez?=3W6spH8O3{`FyYDz0uDPy*34+f3CA==_q=YvAW+D zvDjE8;wZ5uoPFQ^_6o`VwWZITlh};<1C0-Jvn7=`L(Dh6*8_#?b<1com&*Zz{d3%D zgcM;=b20q5;#;`XcXvt6qs}$AZ?@!lS}~>Tm3@MROh5anqI;Y&W@wtNOq7^}ZfjnY z{iS}xwp0=?(23K#c7*Cvb0`m4xVTDe~8@KBGm}-k(&PY9D#w+A{6o#7a ztk^(MK&Qby4Tg%BvRwH{*2?UcgkK;K@Jm|{Ku zv2goaY_DyABU1*?`u1}^^&48A+B7iQRVM5r8K>5j zEb=9Heg2;_1jF7ZSy!hAi1j`D4E_->Nl7NlR9lwu^z8YpS$RNz zpe6a{hg1j2yQbF0RuR$oq|JDwAkfJve4LO^&scGa68_<~0*zAp4=^wbSF5Gl? zAKuKY>H2v?2bTG2jxe5u#Gxovvj-rmTto&$k)|k(jj1=}ZA%!7$v2Wz(SBEy{OsK* zVE%5}l^Ksrh!;iXKw}Wy8{0+;be+se}po&rTEUQweGQ+JrwtaU>@!qk!N1!@g z#C74>yUxo|*Po5-iX>G8yh&<_pA7yLeX0AfH{(89^NGTLoPH+{6z4R65^~1y2}sg5 zmaD`(=cv0I6(Ju^1xAJxF5_}_K0i~0$f{9ZTKw3CE9jDD5W6~!;3V9OO* zZ^&BH1JA^!)zOr6cJR1Z#h$J{Pb!!ijkaTz1HC+6PjAzbu}$78srlKEy8t6vATCIE z&5aNtdHyW=)@nDK!Oq0hnBwQ}Q`&%B&y)3ne2Lz?xRVq~CD54Jvd>;}5y0zvNy(21CAnc1Qc4mQ2h6n6GKXuOggX6YC< zoVP*2tNNB4tNxaLD^(&Y;C*!Rr`A1@ zSbc^$#m!ez5A~mmJ@+z~#0n;+zO22jr<{PlT-RM-aWu?L(6%JewKPX_?#R_^~*1oWYRlc4u&M463RBDId zGeuH&?J0h|d`m(Xu`rNaliI14I!D92{mP5%ij!8p#XyN{Hf3*7M?F4so-t997p#&q zP=S)}*eS%5(u~*fT%$Xl9DL^@814Treq9bWZ$x`TYmM80CXX=QLY+ecES2z<}5%vFdO?1`#-2IHf zHc`^%T$0zv&JH4uO21NVNs=%Vhe(lY_YbY_5hMpR{1|xJIqMUf*-o$WRZ1;wDY>u= zr1Umvp`>EOpgEG8Zi1_1NjVJS_rlOQ0mbSv zYbvpM28O&f`?R*cT+H^3O}ZNxEz}|6$~Cgi3cyRk48AIvgy1|HNxe!xoXb*PPg?&2LAEr zRQ-<+S5xZvWUhg3pJlMOaq9o}q%kEgKo4n%4$^@d`8~R0HcO#ReQvet1|u(<{uclT zLHNFvMs7p1o%7^%IzU)MVLJpTy<5&_!Z_|-hX!u5a(ivU4cVKk$!h9}PQJpD_)Bj~ zOjZDkr5`P_&sqpiQ;R97jal|lDl`ji_9&Ge0v5RQXC=0sLeLV>Q!O^9lG8mcCtxfT zs2wNfX(ma?(`<=tt97$1OzVS+hj3fb+J4T4`}uCP5iP|`aU>U1>0kvR3Mx<}Cc4be z13)gGS;J^c3@icVv`D598q#%Z3!jwGc4{~1yEjJ*rN?3C#5t_*v6yj)WVrxw4Pa;u z&6$m0l!oSE&!yDGi(=RgH7+CxcR8}7`3_Bt{FZ-%4YZ{)w%nej^uA!Wl?QaoYDi&3 zdID$sM)`?Cx=DqOFihSELk^nu5I24LS?S%#5Fm!$7DU$+8;?Nf`?Mr2^u{B*EzA^aje7xhnbI#Lg(Sz z6i#b|Zq^|7h~$X2Ki;*#Zc7~c8gOt}h%t2{HV!+HTmb{ASE@+z)>mA5&qq^#6hnBs#m!!i9WlT$Kr76&HeLi9C zb)$>V9rF1xJ`Q<>oT}LZC8)WBz(V4cHz8?{%J^f*kor1$9YfM1peUd<<+|VmucYrN zsJf!m-M-?*f=hQ?f%Q#^IvSz~5g84V+-c1j4Z6hS^_4b|%J!7JdWN)d&+QJ(a`|2n zx$_&pMohTMdjg#r;?h#mqp<$~FJRKti2;2D@VO-|1+rap%vx095T3Os(C{c=e-+`c zbZ|B2iE!Ck*BrA07A6+4%pit4K-Rb$d$Ivk8+#mfJRA5{K4--y7E#0TW+2i)o#zbd z1;NFSXaHCtuN9XlpiZyUr7Q32;fYdyU_a8Z$@EL^6Zm<3_4}%DsYfm&-C_@`&UQVw zrvCtM`2On8h5kHyZg<<&JvCA{6}^g58k#!T8UtHB_3?Us?RWfYV3A~IC5GR6RoVlN zTpV?iYi)f}Z5)}18D^K;!$P8P)g2P!DoRwSsdb|z2_F)f^8tA+9sCO|^#rVi zj-#@YI~cxD?mG41_c3_$qgnb}5t*Lq9g>B4X%6+chFMC1bF8OLCrKc8BXumHN3cEj z>-FBN>*DG^Fn`PI^8Q-N)(1$}IpomTJC}&9xqnnLI6`8`+^*f{*17_Qx@~t{hgI|e zQ2=}=Lu8P4Ksz3ugXP=yS7orI_TIL85#N7Z?^@Qr{@+UYKpG0s`S&1?-M{khT;czUd5T8-&zeBFaU<0rE z_IFuBo0q;Fv9dPhu<;%`-uf$(taYd!wQE&I=xxr;E(8#rkgXq1`q$z-YZvintslKNrGNvG9OYKEFD{VCn^4 zpk12r&{5rF4{6ekosUktd4=2c0FpF7?V+*idyh}@`PbjV03$^QVM*)`{{SD``}eO( z=%rda+3t2bdV2mp`cKBAjN6+^Vo1BZVq2ym#Wed!Zc2)gu8_8Z{MNQr{c=aA&s9?+ zO;~Wrt3C;FF~9_5_XhKU4UP zcCNFIm2ao3WR3RFBVEs8PQF{$Z{)7K4F$fIkoRXDDMP9yC`j2UQ-jeXUx-hq(z~uk zL}^NC*$lX(_fv`LTO@!8PSTQePJz-$-u|YxKNMt*tZA^(S+)Ye00UYP=1prubq6b5 z+_LJtpdDxp3y2!&T{qEo(`;zkRdhEviIUuc!fm};l2M?qG$X#yN!M4@9`y!Zio5m+ z?W4Zx*K$D7{5EyJO8P5MaFm3D+@6$y_;l<72E7Mv{{SxaGh-OUoSj!MCgE&^EpXK_ z!9D)~C2xe3B(lfwYc4#Zt)&C9w0qXO-q69{ActiRyxY~A=i$G}S)s&{VZavhOitvPi?EUcWe^Gka!gBDIgL1kRBiOGsR_JLYT3N>rtE`9nb@6@2PT zDJd#I+Co3B{{Ut6J^TINw!5oK@m)yiM$$n&XJcE}zz+H}HNQf8{QY>}LW7=gK~Zo5 zM_`hB>F7?Q_*Cl>q&9sLCLI;)y zNkV>Jfc1KOwV-xuoiU``9YZk~TdwE=(@86Pu-E_rvQNkZ^BUNpc&p6QwH(eDxAKtQ zM!ow`ShWP@w`H@b%toMD1C>=BhP_v1jE?zkw9T@^xXEVMbl2JtP_gc^N=A}hNJv>J z8rR3KsHjwTl!f(;q=TSN&dK#B&}f0*<@@Wl<-T+&DPn6c#92@Um)dcsfG4q197=k3 zut7efL=rtFIQWFSqT@4a^XE>Gw1#6LwK)5VQ3?J98an`<DvN~e+hRY*?6-^+}X|85~2bW!Su(={$lGJ6AmAvnkP^7kYJB@W7$3S|M z+=_uhfc@%9*l3}b)>NdWZjP0!Y9Tx68ed`CUbS&Nm*KmRcpD$XvQ9&Z20MI(Y!mFU zTbhY;8;In8d1={Aqh>R@=>>(TEj>hQVAZ!R=UZ__zP8uYN)(TT0j|mf5JCBZ28rLN zVOtWqc9ybOog9r|?#LY1WN;Z=;6Nas%5|zc8&$%)2KIK)N+n~Yqbx3Kn{4iuvEY`v zh%F!zK?D#8A%4zf%X`X>>woE{&YePz%07f>cL%un%PfJTDBIgc2MoHy$_dR+gsnu(~>$9 z=QQ$L>rz|_TW<}vpbp9iJNQ&P9>e)6q?*dpkhp?D)1cd8XmaSNS>A9YWdLe5%b?}A zPlru-86&KzUl2*wiRn;3LNq|$yNZlGIU=6c_FtK)Y^ZoPW!9oq3AoF`#Sa6 z(WnhEmoPc9X4KPM2pVn;5N^lmu~jj-%^6&Bv}!*--1&}thdb8SE6ydv2UPBgbO&vy z>%RSdoB96Qo3YGWAjxCCY_VFN+H@p>dR%8oQ3^ZsPJ!RkUcXrgr4A4@M^9i5_D1$Q z4Ue9?=+|N`!hNv1VabsZZnRojqqw9MwxSA~aG;H}tpcR?1cZ_fg;CGU(9l32>Q35d zMzk;5vqKv|?}s}=S~YDFT0ygSKvjEmTymLqB$+YX0WPrn%s2rG9%BwErGQ8tib+r3 zt?bv-XvmhC1<4G@@)&YJ^VAR)8%a`-jRVx-1K~&Y#Oq?ZZKcQF>sI=ez4Z~k!$gz4 zpG~KKs*(n~YQEf3Kq)8+NIG7|^a0r&{{Vi+uhy^~2djM@M)m1thtVk6E!K#(xLb|Y z>EU;_+VWjOe_0^N0@A)eRD!T`kk~9`8#0~s!Z#}kG zAO_u0q56IGRSuM;aaJ^}f%rGFLda|x92v~Yl-oHAauF6N+Lon2goQe^be}WGZ7Ol3 zY;C|h=+-|gE?&&3Zrhs5ZmF@yzq4w}E%ymF`3vYn$cqva%^}Sp!lu;YdMW5oUv!WL z&1Hwz9z|t`*B(V(q#3dCC|P}j2}SSoWtQ!bq~vA@FEsr0$P$o*4%52yjP>SY5uzs15Uio+tztZd#c@b2lq5`2zgue1Aysq4 ze~6sdFqvff3$ zI+$BXZI3xgLz)U&9B*MsOPU%_riV!1ucb=TBHz#(p9&m@pg!u)y(P7ITTf94SxNX# zhfe)B_|$7Zx6QQPe3@-XZPZ9u_hh!FyuPK4xDw!P1K|XY;QjS=IA&wT_zp1^VuP7f zyS=TJ<=bTjUvfN!Aq+@uEiL7?$k<;?h*>()v2--}k^)dAwdJX6XW#^( zeMh7#zhZZ)$d4HKmf^fYG8nRP`xxZs1FgMuUA6nlq0WTbdnENl6Brv z@k9B)%N%WUzStavDs8=O+|uhHHtBDn(BevZM@jgSN(#5J+;*ZW%j{(4Yiq0J9}QV6 z@z-tS3oQ5T3Y0?Ii6LzH%S#O};VuVyiU1&zK3(d`e0A|_IA&RLhRenIb}Ovd4;91r zMJ^$f^-gL;?^b z6?|QRPpxKi+JDDRW$og;+h$3B9nbN*bct(%XAc{%V&Ufb>@u+k;1X3nyTwu-?Y~N9`38VmcDgQ*I4@vX1>d zJANL&f$#F7DBD+C^rrxJ?J?CW)ENp%KaRhfL-E~yNE6&%mm))jBe_$K9dGBpy&mVW z*W+JsBBDd7OjkVDlcK*ZDqg#HBqclkN{Sm~^sR2R)3O%c*0NHDkW!JdJjYbpi34DO zo%(<_D2txeXpn}M`>!aZr>F%el6`lwbas6`clUOvt-doay6RhOX!Z+U$wz-(B&XNc zx%vU?UCV1X2N^<^{jxWjG?YrXC=>zy$qhE{j{aYE#`j&*Z+0T~E|%1-M{D!IvxE_^XjVa3a^P+&fu?_nZ3!u#7Um?#eq#CbF#dLebo(@n{^!} z{{SzoMH@)ox2;tp9-x!I)g=Dy{e3IwtK!%vTG7!XW9Ub1ej|V2BvJd+YZ^oIY_k-m zp1Ds%TYJSSks92W0fp}i9U)FFwUD3?k+h%LFQ@_7Z|X@s#>b|;Kg;(vlfIxzhjgg- z8~eRasPC{nMR9?nv)rEK{r$Utm(%c~FR#FF@UPM|NI&{t!|?6D#8ErgBVdEDc2WWQ zl0U8b{nari9`mg4USr(gy4*xJ?-5q(iIkN{TcIdzw;E5Fq?VMWDGE|Fv<{#~ffTu4 zt*ExHpi~ci^q!yaEBwC!UzDOR(soHH8$0jZp8o)T$M`Gr{JxdUrb18@k_h?!KZ)0- z_4~g1;i4re{$EY~!+xjk75V`?*U0Gi8wc+mzqiv}=!^287oYV;KlR1`0P#Ym4R<59 zzy$XmhtThO@A%Zect653{{XHpLK7C&H`Kyr?K60#7!e-;dP3*sU` z*GKp)S7;vG?JN+yyg8&0=9YpBXxOnv>1BbPr13|QkL@I5GDvDS=WuREk_ZfS(}~am zNzrFo@A@RmCrXoI$s<`GZu0%3CbK&0@TLZgXpS}08Isy!$5Y9CwJmM7(zNP)(n^I! zWni|8-JMKW=BitNWw^|Vc6+2NOA2wmi0fQSfp$49f}*1tr^=o{DoPjAu_9qyAnet>O4AHYWyTZ|<>Al2<5UQ6O*m0}OxK@jvSCC1Oi`%9^w_Ikl^H}=sep~5c3a(GE zhnG4jTSyL>W;?DS0F|WZ!>>3U;@Oa{&ywY@SyywrBIOQ6qUZ03n%6^ZvkptnJ0{-; zeF{5Cj?fMgvIzuAisf!Q#;!N!a6DpLse9iuwfyj~lHxZwL~ zIIQwc_MCZSPG%NUBV#eb_C*#Y<}4I!f!TwaaR8p)W;xL-)xa$_X0N$KEM+k)mzXdm zCCjY2mn)=67ZWZ!&pvI>$(L=D8cR&bZBB@*LOj%-q$x?<#`ZyF#_x9Plx8J6k~qR2 zn$@wF?e^=dC_qfaTbXq%+gwV*U14Z-3IQQqKr#lT7~HpM%2*edZ20nZ214acgkH2u zdNPnU;euqorM$KDB)8^}7N7vDe91YM$!2}Ivn5<+E_YS0A8Dkalv$gS$%e5JYi;7} z-!X0}{{T14LLE~70D~F=ysr}BQ}Gdq!(thUgv!Q6WztQFkokIIby*?4rY9Bf{aP#KiE$D> zK_&)%(p*MqrhDRSk+GqoLz$#d9pzE6tDKv~+8wIlZi8sLHwa0&-lW-FU54JmmeNq#qurxD zY(viNX+uo57PTO$2|Wae4>0m(S;QHpH*Cl3&E@z+#^S#@cNlKA+!egaU0e3%+e~o{VT3q3RnXg7=}mXBJl7^`-<^!?+l{{8YU{YilBho5u97t&8-wS`NbUgh(i7LP zM{1I!@}4VwQ&+!@GCG3XTW=w1N>JEX>GPac zY+I&_63inciwhgx8qf)Rj%cXJ07-?En{*+>Woc>eW+43A(=Ii~F_?@FGZ6UjpM(DZ z)WakVoH)qGwn+qrY>&fC4{{ir7>GwBNPEM`R|y*>g_TCKWvH}9x!bRknSxN3EtZH@ zHyI42_nzrQFox2S7V1gsBrna`*r=BvV$$1RUmfQd^{xBKj-b>jP#x-yHeZg0lw4^; zU~Kdk4!{c5s@3L;^yK8Yt(xl1y4jP9eaZvKj^Vd}`)x5HECk6xVaJ+hscK7r6|GAM z8|fYFRhFmQr{PJ-?e^$Nxk6>Rb5gY2;9X)R@>+#3%1DsR(B{;T(sWr*bsg6Qyl7%w z1{sn^E-1-7xTA<;rNhC3Eo1Ogw*wB2#Y;1r9AS*fBQW@hrEo2bJEG4^F&OL|?ZwR8 zPCi4Af_~YeOgY1~gyAQ`;q@%U<#TFCpA;@^E_Aj=Wyi?*G^|HIZ!UgSn*rUat4HJI7HD^tQW z<7#iqd9=0^l_fy{cCTB(4-Uq~hr#2pyh8~L*hayK$GNO)TqhvpY!M7g17EA+Y^`J3 z(j)Ae0CC&lF=4T|Irv2Qd_;JeFmWNd!gnQT1iMiECnDH1P~2XyUoSxE)R%I zzF6QQWSfk&O}tN!99(Sd2Ax|^7Z^$#agBhI#KgSn3X%|%kZOtX)+5J09kKTAvo9*i zoO0xljA^tZ-4yF-LR`2qm3gaMjsldV^=x!#C~rdLvMcH2OMQ-49!igBdSXJM;n_S6 z>u04ae{tM#54@$Mbftc10IRU>HJ$hzBv{NmP+;*6!Apppzm3F$hmEd{j${p!vKfn9 z;$56H!Ul9ob3QUOUxMz(iYUxku?!@d|N!j@tjbIF`l2aRH5HXE}YF@^N#>T!zX3R)-gx35!Dadh4 z$;B@BSrBGVT!vX`a%MRV-h`>v+=FnC;?nN?vQ*lnIM^Bm9+d~3oy3oNe&vtel@}~Z zoQHdsQE!(5V{u(5brK?>mr!194fLsRARv^IosuevG0yn++AtMx&4t2kzH(DN=$5uv zVpN?iDTy7K4orNuPgoAMAqQQQ6|t(!-Z6w^zWIB)SYzAQl_7GX%5m8dpIma2B20kV z9c?Y4qK6%Dl&h>IK#&#NCJJX^%#rb)9g4y^*{%CUIf=wE%nxH6=F15Z2At9~hX>@g zwE9j1#F-`?(eVrNerd)#9ZN!cF_j8t_%gOG+#c$FMQoT z?G?GmRjqEsc6*&B`vY z8Fh({Um?neC_oIAB@x5O3ql(kP%Q!qI({*|6LvOtv4|daU5jMc)$H0OD$yDxvfD4; z`AJi0j-`-ECu38!rp+5D_ZwoZE@D=zs@5HDBt@mxO!{9oXp;ZA%UR!lgKNMw-2!z*0ryFf5gOlIvG0~%Zltd~14eNj8r!x+jq z{p>RT05ZYa0vDYXDz)P(=>#(D(gDBc^Q3lr^3`Cro z**5Azz2;2iys3sb369FAtxh)kX<7nWN*yV2ItmKe-O^+p60K9=FK~UWiMdHqq9gejf5A$AEHAu;aZ39BW+59RC0>AITtNKcTtO)bSGUQ(YZij}yIgpY!(GmvpR!t?d3AXJ-7=5uiI!4w!W+%rqVWW_}j9X?Pp*>r7; znFdE*bIQeVz9)}^4;CwkVxyKpIRZ}1NWq4Uvb;+xVPuYOQe&{j1{la`6bieH3nSz^ zRqAVZzCp5BERI`UkjmmX#Pu^J9&m0&!RLguHtVTFZXzpcZB3y_2x%@VslOv%^GwmV zt*qWHFGgjwE-rYvFWbJnNv#G5(b<>oxd^N0JD!baP>v{I&qe5qyiLGtjKV&HJ!d22_6K;7>-GBlL)lKx*;j9#8;q+K%C*@DEWPeXQcP}A!z5@E8+(GhGd1cOR0F%?z zFC3VQbSM{_%H*?cW?YHRqBX3(@_gvfVwng++(KIEOr$inkjh6ui*3px+HIET)2wZ_ z?ZV@8pC&!hRKO!Tq`49-7`EV_HA*filC`Y}N|qecWG@_etR5wkO*3B^WREc6^9Vy) z(cXV)C?(~<9U?7ZGzN!UG5S%0m9jp5B@A*!H0>A|=Imz>VVrW$D08BGrQyAyF_*9j z7|k9Ys+=@Ma^%BT>wB{e&NH!7?sDC@wup=+QM!yZJt8p?gMCb(v(S))sVVFYOE57` zK9hM>=GkXc_gh3oS^m;nE;{11DpTrI$rA&X#@8J(QgtW~EwY3Fme7=@c(rDHZoGXj ziOj%#7dNA%w%}afF>)5%hT`=T(8^G!8|YARYODQd>4s{iF1DA;6Ep{GBaL`QWI5sZNA!u`4@WS^N zXy#z@;SM;Iy0oLrR+7p1lnyw(qPJIG@S(}@ z=ZT7SD8Q2iH&z_A%VZ8k!(E*0bQDEZ*@vVh-h{lFVmoW|kPul)+sYN5WK>!&tj5c1 z7U@Zfxb_%Hj3h>!7+MQbB)GXsb`MxREVQM(m6a@|L#MtWJ*5s_*znJMy*@0=Tkx&uN%gAz7LJyaj`(-6gU>nTKOhxCYo5{ zwgzEK42*(T0vh+o$s7$irL~(~dQz48ReZ%NP0Y9Mu=VTbJL&%b(CTujOD}Ay3}*oB z+?^=wXa%;dc0JC@K<(+@b)O7sc>Kusf(OM*cOGNrso*4mzTZ84{-2F0Ux_=>9jmgk zIOXvNmrAX^{{UO$NIgTb`TV})T&V=B>IoVj&;CNb61|-tUzL3U@pe%J=yoHw$b5}m zo1ZV#D0#Mn)PwLVut@|bKquE-f!>dQuYbaWLu71yN!ai5_4Kci)CfsXJ-6TWkMj+8 z`g&8OCqxBrU4bKK?;0MJcNzqb513fjCt9=!30A&jjsDG^{+}-O`Mv3C>h;%7kI+ ztCx!AK>ioGKNqIS^w;n7-uK&My>qFwp2;I>8YAXCN3V1EZ|m}|upk1EN{@Y!@;`RI zWAOXykK#(Z1$;;AugB%<>G+xlxx+1x0Oh&c@5666Zo23>5C-}W*XX+ir4goerdTh~Dsr2qX z{{Y?Jzt8n#sR1Ood+Xv-h(I6g5J~C%fDYe5y5@DRgoPBj>+tpm@X`I-`qxd;QQWQ0 zuo+;b&T%T$g{Da4GPmr+uFH`^q@~s zJL%c&)4yHzAC8aymZtGkc<*LbryiMMgqW*#LLSK)DzwO3O)sU1NFXJ^;Z~44{8~Pq zvI3$<4aad3OR7>j)`aMm)xPOi-qd>!nDzCk#?zQfvSYVAdOfnoB{xS~a9k(DdCRmv zEUQ8+gvvuKpe%YqmD))19Q~5SS-Pji8k@LUxqvN(Wk! zlhlNg4IJ9Z7 z+Wu>-Tt1-aZ}q-g{{RQzDrCj-d3c^fWys*Ahh*93#<(H5Z6WB=OlJe3i7Yq^Z#JTy zg(2iADjkR;dVsjso6wfPLR6#G`#+QRe(hIT))co$PsS0j3Vsx)r(lEJ{Qg_@(fZX{ z3!9bBaSRWpxw<*8Xa%GlK-XHTI%&w%MVl~tnUsoiO_sT&Wz=s}NZfCF(zs4{<~F&z zC}T2JhSiVDHn>owM6$q(Y=GpLjfRx}0J}nImipXCK_v(sYS}*#Cc6ARbD0Avv(NiY zt3x)Y((|_m+e(!kou|x7QuPb`5Ef9Pl%*a209#(W*wH%tD>QDoe-S29?(4l$%*C_?(F7M8B#I`<(3Plg z?TP_~j++Q)XBm4tY>YNF4!pnvP0eAta|Iys9>pViAn#h&%X|EHTrYKx;LEP5vHGMq zR-TjTt&YQ5{{RJXmXMHCkfol$08jPD*Z2M!@4Cr}#XAu@r48)CEG}+$$w7cODRi!M zhUgt!;yF+N44gj?_+WBYV6}|ag3nVh5~Zg7i+faLc&tm_<-L3 z0NH*YBl|XgZ9~+D0qRj8lh_^TXrEE8$L{%h{c1cITXEWOHhi$43eT@mPQrf=UzYy> zAD>lW51~V)TLhhtUc~Fa`FHQXtTblK>pJ+~Ayu%i4t~Y4m_%F&ut^%Xo`Xx*PG0Q^ zwbqPMrI7gUKUc9GUqV3o`W^hgZFU`)ge)PnfI!!8U#_>muKfqfpze^BD5tBbJ%-Pw zzhLzI2jg5yStF<~;`RFPf6M&e;>6h;8+uz^VAi_Cby7sx`&6t&O+kr=6V#OYmGF zO6mnAK!d)@00fiL0X={Oop;vuGzm2rjb)OiQFE5usMgPXZaedQrxw#|SwRhz1gSkm zDnaS>=~7ex1R7gm^dVO{EV%3_a^iY96>%9$p>rKv(xH2(qG+uSFs0OtI$4?`^xrR}0aNlw_ z8xEJswNm_!hF*Ovugqk+pwiNgfV80NYC#D-L#%vrY_(|F9c9kDd^Bwd(euJ>xP8eELsWY8BDMJl5Pi+G0icsz9NewGRb{aeGMPkGX zf$U+xTBqP^n5+AyF%1Ph&p@VRs)7J|KKY@-HjL!7(g1k@4O( zcE|a%b;>4Ou6Zr4$eVT3+$ zlBVX`o_)KF1u3&SJ4lfjs~35mADi*p!qJf1X4x)AJaEi)bfPq-N_t$9+?LOj>J&;z zS{+hVvER6@#l52$-aM`c@Yj)X15#b~fvlz}4u|lZ@m`y~k?8xa(nyy}*a$`QfJ; z5*8EW$05Ig=9>v$S8@u*$IghWo^OKO^E=<$S*AlA%a~L>-q4m*mco*y6f4~jrW8_i z4^F`TdO309l~^8iP$r^mc$w-(4=I?dj`K}Aw-lbWCB3P{06JEZ2k>k4N{_>^kyRTGgOOd^n{Kr@ zmBN=9#LABI!I*$6Ng--=NJ%4m2dIx;{i@wD+>uW?aB}H1r3yk96|E(*rJkYF=Buc8 zAOe7ZKs(=Ys`-xJVL^=vQPlfVoQ_ueF33t*Qin&D8bTXF{ynd&scQ#G01X|HS=MV{ zk&9&aYf8#I*ppoW*y}>ll_AomR?C4!3PM&8prBNeJr8=%{{R4TB+n6k19BWDLZjSr zZaHb>wwZx0yY4aLw&+?KMuU9`>LF`L*&7Fajte<``BQSIZVyFf5}ryrTWv)_ULvb>y$i4lN{-prnG85)aJi1Kjrm<=9pF z_`Jbx99hY^1+5Uu0xbzKEd8xjv>;hXPzA5DJibzj+R?6aJeq%esn1nbL=6Vz6q2Xf+pJME$c5m5XW!j$`|M2nR05(-L~X)Ufo z$EoNpB}p3oUj6k~Ib!f&-N(uII8&Z%l%$u?v~-YvuS*F+NKgkrgHX@&R?fDqADXzV z5JHNZRm5#68yz4e32cOdH{C~nTANRc{$iBbYo^@+4y7d`-xM~6yAjk}Dp^;rV0HM4 zBDnlZ!j?5>WxIXb8HhTWN>w???S!E8f=^R`j=gV1TTk8Issx`6oG)Uxs&el3YgEL7 zw;Yo)+!Lhwf|nFqLUs@8gq?bx-K-Yp$=sWhAwO#6msXOssp6=w`Cx37p&cN49fY3y z*Ke7ral?1Zf9!rul-xbhmk{Ywua;DXP)AU30y@Hic6~Lo>$)PnyrJINv|JH1H(#4RbY&UH7bM}|xSrrxe(Oyc+scn~| zbtSPSD_cq_(b`g@uT6a|CMOzQUE@semaaD#5#m|yQLM~b%VTt(ABWz z`R(jr$m5%gNX>Z{w(iodDN>O9pxaKi(=Ri=jSjV_9Rhm4c@+@p=p`H9LX@7zW8oxr zB$7vN#FMe}rGAh{uF3kJpWZ&>^82guh?*zR6JNR`)DinA6jQxxzt+D>i5fbhFTgk3 zdf%bmhWGt<)j#Zq%v0n^J5bmi)H@w@4#Xs&4HN0NqKh^BmR3d?g3eoiTi-?Eo&?IuLX*!7})=4?hv(As4=8r;N(kiu59B~B^7 zEGa+}vIR~F;C^i}<4#`>a)MyZcgc@%e*nv3!IdMZBu8$0mX_hUFp?15M7m#nWHy%8 zq7z+CB*?QYb2qX~!j$v31kH-msE}k4Q+@{}ouyOtN$mU_se5skbhg!)X zyU5D8L$=wm+fzm;xLxd6L>O{il^v8qj-(}Zf&Ty+oUw}YUpZ#nkA^{N$BpHAj%9ks z@mQ+4h{pC*_GQi!X?DN5t5bfqaoN{(Y0CFc*wta)BLHQROf9J@fh zS|>6{axKE!CAo238Y8JGb)@-?t7vtyrq&dNs2Ck8uVZ9unD({odtAq{z&WmKfeve$ zY!IPTMXx2EYi>4oU|Eac3Ha!h|V_ejq+ z$hA2w4LOQITk;x;efhWMHy}$8no80YB=LoaGShZt>6SaY0V@fQ zbb`#7jy#5x;+$dDB*vE=!c`taU2X;4hHK)J5au{`ck}e;R-3*(0JR3$8tQJ?!pnUz z+P6b>r(sWlZYU_2h>Ewk88O2S&Y0^>zbnCyMkT;aHXjDx{{XjQrWlUeGV_YUOBgSV zGBEL*w0G{LBljjQ&ooVRLpP-1xa>3~*uLIKu`w8?{2Y%zhr~-0TGvR&nBA4r5uQdl z>=}T_*KRL|^nHx-N@rYGZIOM)rM&gq8on_iLhkWb1QfUByW|Bp<38#ib(Jj+wH$>u zhg1@kQn|LAQysgpie0ReS2^{E+?t4L5oaW*rZ8?^z2ZBpkl}H~#{{W+?t9lahwmKt zJ`|aD9FqHJuvq@WQtgr$#BWnk67tkS!zEFz_6d$RbAQTaJgLkx?(Z-|&q-93KDwG3 z@q^*|{c)KqFwNLeej~8cDh3Ub2U613rL^k{{MPf^n9`KBbt|Qk6rGYrhO{wpd?eH1 zb6V`G`PnJ{=faKwkUe5^4} z$A52(K5o=4=S!ou=RM>4y&YvuW zI_pg|!u-Cd!NTH@Qj(sfB2drx)^(oiBxSixzRJR`tlaE(2rfA;8>>ieWH69~AV-5W zklJN{N|cA4bT*|cpvNrUBm5s&)qG5zLz9#nvu!@(w;X4Ze4_MboNhculIAv-nMzXf z(4~Z-3mw$84OKeIJV68el*-u47}44GMWw{Zz)Q(9HJijj@VnyTV=^sQ zxYL|vtF5y3;h6?wszM{UyBh_!Lj~kCkdoqASz1q0Kme+tXI#sa+Gfam9aJdJToBVz zCdonzXoB-EC2(J85>Y87NYFiiO3wRLmt#41g}hoYyss!{AI%WhkqTxvdg@nX0{y^X?cl$$1jY7V+LeOsAi7tgN{F zTyQj2z)??9ozdzZT9h_L@g9$G#>m`LusGaPFs#f&xOOG;PVfUmVum8ZLjgH$u5;RQ zIo;DHv@QxQEyatwyS6}!ON$eWSX8`GE5n2Cs^u6GbjEa_J4vO2w$Pj+XNRm0Ly?Y40IU?9JplG_pif`@NzlD#(+dHBO#NPp#ISvtCZzy=Sgz~(2 z^xr=Sl1BEX+)*Ip$ZvJq?I@|N9>ZV+A6kM7E;gL}hv~Ra@Dh6=F;ik$Mjyy*(Q3YV zck$jBPvoY*fnYhy^IOuqy)8k2tN`Xbe(k>sJ&F zQqbK>uF;8C!^S)L9mb9FhoB!EYv(8yvWiuXs74sd5chFa>m;pn)yc*{D<}36?gfg2 z11!v!fZDFaepTA9%2)%5?*eE1tQ3r*O>HdQ7||ktU#5wHULrX&bftg;Mo0jYZb1;@ z5W_TV=<8Y5I1iJY-iW1RJz%$CP~rPvA0^oqp~Bb(jvX#ap`Pg2mX8T8yGv6fDHUD6 z0NZZiGj8bkKK52*!HC{qzqyy~VBnFFX!(bX6}oN1$KEYh*Dz~yjjqE-H;r8mY-{5K zPmr^2+fQ~CE7uodyo0=Z`AbN%?IBy7k#TrO<4J)nl0jC|h(6VHUVo8S0EB1G;V+tcscZbCDet6In1&Fej6x!n6DG zk9E+o?(y!}K8$g% zRtj>r8k|5Jljt*2B6;E=g!JKa2O6D_f}^&fWb!u%&=z%ov`vHRPx!5L>V+L?#``!3 zUxZ((gA{ONxcXE%U||E`_XSreg;ADQU1K=~?pS%rD9f&KDJaEF!~4Jw)2LH55H_W0 zCr?f$cc+CvX=>#*8M@!H5{qI0ii_%N4?dZ3a$HQ(u(4+*Peg|L*v`iW$& zY!Zx|TL3Zg1pFpSKqN(U?nnw4Oeq_UQPIr`oT0^9tU%)Gk2PvM_Kgx05B_rRH%yt; zkWb&1>GLBhyZ&DO0>k9@p$=H6akKS=_za9qBi^V;nT1fL#fna@u`pUBQ5b9(k4t*3 zS>#nSPtY`RAo#l9BKNlv6hBQ-3?$P~x5X>M=frM}hcBSp03K?WL#L{-Q1q-oJ0TrE z=Q{Z~yO?F+O81dRM4LCt31U~r7nK5twQp`9ZroRZ^5a6fuiV<0moMD9<Bn+{+$0q zVIke|pbjRFj}Th1)#EkugjD_I8fE{;GZ7X5_QL=kE zrtblNhA^9pk;>cGNP{Z~9@vIt8VI7-m_q^G3F02<$m ziMnh4eC|6T$

A3NPcT-&ub(qSFytQhOFjE**lQv1GOjn&;jw$uzYss9pJJOCo6t ziBP8L&#d7q-f+rbDkU3esMh=lR+JB{x1Q1w#}!K}vVjwnwwln(el|1Y9WP&I+zO-b zGijHaI`@}Q^mgP4WSSyJzFVH&#u3tT3;#O$4c0ywY<>w3F!+K%B^7%Y(}z~${Tg*`0(^x2HdBeJkLv?7XrSxx7gfdWk5OOj_ke!=Q5r8*on*HPe-3J zr)Mm@c!8O!QQa-j$qeNIBMLTPEZec$WGN+M%IlKbH-sKpBxlyMhvdIkzeVGI$k14h z`UK=jf=z3D|KI`=jomVjbd*PhrKk?J+jfZI5oBq##hT@?4({>kAww057sWEa5jCF$ zriiC(+4N^-3^0~hP%^GB@>#cDyUQR=DT|9#MOsc4?RDKH{m-k-RS3s4p^|m)U`?=^KdqK4FUo`4QroAMjXV~%{)`Q2tHCs!xmW^vqhFyWF)e@rbU=WPp~NA3#x zHjdVJg$AU866W3{v)HTr-l(wd2_#3y6Q3wC*3M_@d!3IhBHO5)hNn@*q*O!N&RMIiBE9< z3nZ%sulda-48v)Zo)GW(Yi z`RBYdq@{gcw4&er<_iO-8HBNTGxJ=VeRm$7_@hYS3qbuvu%aCD3?WJAajOG=q=5Mh ztIn$R`Q)kLa(|6)1$UDzPYy(J=B-%ULX#&^TH1YR-bMMtdMym~e78L|VlC@H4a37r zONRW+8Ei!sGC^GeXG%E?$9(CodOaTvcum9_`2}%Cxf-k(w0A+m$ds#*Y!ju}NVsZx zRLSMAYkk^?j8jdN0H%n&S6`>5+)js!akX;8_QI}g{gd|q_OM5Dq6aC;oGL)MggH2^ z2ze;hx(}FZO#0T$nV2wz?pxZzK6V$vTUTMul-T_BypEo1vVw_zMJd|hHP1mpWAVQ8 zu)1aBnyH3Vek7c2wh?Z>?fqxcyQ^W@wS7-Yfoghbl8a#u_{NJF;nJbtV{ZD#|!TW)!O`j^dC$dxji z#1!6YK#UahrLp2-nAck(xYIaG*)--A74?ZfoS!h}Z259`dvQJHvb0IHa90YDmhw>9 zJ)Ki4d*pVOsrL;uZn|$BDpe|eWE6vK+w*I~wv?TEW2Ciiyfhm8$CD<3L(inYEY zqQ9RfPT9S(9GzltC<69=;I&{BtbQr_V0JS)J1JpOh>0xoi9-rId33XngueEqd(+n9 z=L2ur_FD#%nmak-Xf#m~d-eW`&utlF;diw>N()ex0YA}+i0(s+R$51NOhV|(YSpY= zwzTX?)qIkzcpmH)g~sl)2>VuLt9%Igf!&2BfklX-rBFsb@H5*5wIk-h)zq?lFO*Ak zQ21T46%^>)Ub&!uFvxE+P^$s+A7vCM;~hG28Zh7X!_s&Q{-ubW=)M1Ou#k40G!N#G z@h%k^=>6Oa9#K=#Du?eYQ?*JPo@IuFV10C#J6`(Yoq=yb0i z?-1Te>DE+8#Ia@AnNN%{r9YpA2La%Kp4(!H-*6pfgzDi$(jnxii(|nQvnn4Mt)D+I z2a9B0E3#I|Mbl7zZYXNm)i=^Ja#LX7CjTs0G_>^i6tH$-?WM|VC3C*VUStIV4z@XV#IY~vn_HzE zr;arBMQ{&06QO{43;FiGCTx59cQjJ93oY-+zAw>=A2$(le`v?qz=UJyPKv?xhw&w* zNToU+dnp5K0i_d>hO%86S+0&_=9IGquH`0i$ItyNMnIj9rc#&QGmN1_L9woH>Nlbj z#Il6+DHTH|QS17JZgQ!hJOYgj(lRN+lRVtm=VH#!c)!m0TlHCA_kZ%o$0R!JTBszg zULq1*ERK&Sv0fS=!b3~hO|D!B+TFK`L4vS4>50#8zEJMFfK=LQ`!BheWnKuNQl1)* zYK4AAqo51Lllu5dK+8l~w^$6}O?hA_Q~JzcDBew65>gz27lTP#{f4ojwoCgyqLF!| zJ8Gshq-3$t@s-c z>M`4R$|(MalIvac4`tZ&se$krm2TQ1v!P3x@htVNGi3E23gT2><{YOq@zL>b)r$|s zD9_`_o=0cu(<$DKFXYSN7k5v@z2($^XsO63vuC8IMP$YPP#u$_cJSp9BU5&gO)_*YRiPy9o^OZ=F?!#EGchH7bhgJc>SaNd_T^$4OUU zh`_LiZtb@ZD_yux+k#g&(YcRLfzMQnURQOG(xT{J@5j}aC9fD;J{3@uE0R-n9REGO z*@+WWV)E5%?;-38xQu-D7H@I!&#jH?u z;~=ZM=h5`e;q)R9DHq3o>))^0^VG9({m0+dSCQ=Ke(>o0Z<)@QkBElaby|-iI}JYp z?FQt_dFO34#A4Y!D7e;o^`tPu6o27wM`C#qt;9xC&q7BZbw~Um8ReD4IVI#DipgVs zhxhlPx|aq~HQp4-L=OK@>ZZpq>Mi3!*Dji*98@N5E6|#k#3~Zxr$(C<@k-f{(FVUh zsbjhjm0XKVi+3D>Avd6(nB`Oki8`ruN$4`vM+YmW6Zae+T#rVWvcgk7C?=Cy2H>4N z4oC{tZYNauzdT}KpZ*(EwIEc8t^ZSDKg{@=T9IO!3EAyHc_Q!qtNzv7Hz>hko=0qv3C00j&k@aMa0$`{3ZG@XuzG?DlA zGGA_kjspL@hc9HEGq-kr`^~V2^JQI%JYGEdgaMGNZneWrNO36blvbn2!Tbq-_A?#y zLT*8LB0aOBtgCdj<-4MUh}(>k1q6$su3+fbdg#@BzM%Sj8Lwyk^0Bw?M19r`I0NzO znTC5E2P~ylFlGfv@G6>3_hz)zYKEHp9a-)nP+S-sx^v#!$Ea8xlRdtnQc^d@4^2#( z-RV~ccI7jjd|^70Zj&fzS+wp0eWq8BQoRoge2V`Ju%okQl z4ojRLwSVIn@N{`_>D8=L6XEGLl~05>UDhQ|+*gRc{mnQX1;=}ucrrCuIySRKZH;a` zDoPHI|NL{JTq0Y z*^~U;AzLgClr9V}Kw5?rmQ-+m8x(nbFZu}UCjHv!EJe?FbczqTR}2cGzrUYC+?;iCR@fN{XUa^u|x-#mdlkGpkC74@d6=>cOk_ zhB&PeICcsKOw5XOH_ymo8w;&Q4I3N2PQwZCF@yP*_RHbP))xG~qXygT_PXm{fo1hx z2&dU0dDnbMPhs<&|Ihs;xn|FUojYi!Zw}0c`3Vc9$Mil`Y{1(xsn5g9%_pvb0I@V~h2?v&a~9 z?BuYmzrmnkaA8g*E5o|)y%soRrNZ{Z6d+MI;D`LqWo;Q_64dbqo!Jy|*xkn^HVw`M z1;gf-Q&zfdr0(f0h5vT-enNPvHzk@7DQcPQ&ABn+U;IPa+k0ratYwBnioNKa4Ho~J z(@8!A?L6WBNm7h()JOY8a75q8Fa5o1^8RwpDoK&vCmpIlJM8gN-Gef$he3jaly}!O zoq1`3g&$Y!qUL=BDVmpws}&e?KFtxFZoBV?CvD6J_%zLVe{*yjFtqc~LM^m*BrCDA z^f%EbrgpDwv9RWuTa@BKRnQ+p{f3*c=SeZ9N3E_iDC1|Sf-EW*sD)23fGb~1DAXMV z)x9EQ45$`)_$K_yTi5caU2W{(#u;*3)jhYbWXyi+pS*D*Y5ENPXs5S!nIiLyg+UQ> z`44638@Byj_t|7r(4%6s|MfqVvqa<{CD*qt>m6)3_j0FYgI7p#!7oXoZm)dqd?IFD zVHg;%EI~KVtYM5-PX^K1BzxSvE6S{tZDslw+sE{{@*27&!MINZ5*FQ$@-=~h!1a)& zh}t1b#h9CC^J(}fXauL}EcjmW^QC3jDR1J|GlTKH!-IK=8lmFjwhiEkG2k9iD{Vl5 zF_jh-=69gACj4dI$CHHaq*8&#LS7~~rQmV)^n7(b~`qEr4fDlA@&Mjl<5> zU(io$RY^bePVsmLYZ=X+q^snE+U3WB{E9fhA#6xP&(e{g+yoPa_RH+BOA7k;MMYL) zzY{jAaEE32r?S*D!HfOU5+~TS7x73lj{oIfw~&EY4!xv`eOzC5%!8u?jgHIk@3vQ5 z=_qL-Ei~&bTGcwIO<(C-;w8@UdN!;$4K!XF%j8FTwtchaelC>EhHbUIn))!58IMOjMGv4lng(6~G}0?7 z(tQfB#dIXMOeK3(PqIi@M>~UtNjL-f^IWf8sB{EfNb+(fGR*G?W1OgemBGJR3g|WI zN$k*#w2X8v2Rj@~`dUvOXkgf(IT1aFGEO88zDGlN?|nkHg~* zmmHjLMVok6_91djkGDMh0M`>y`ehb_0==GzRt-?dC3b8bk;DUv2~^ho@$sd(_{8T( zjdIKa)Yc5q+R(e)!;nx9Ogx&7j zKn9USAYx8bl0Rr(qB6fQyXTNrTMV}JgP&kEGske$0Ve&j?pCv2d%+MeU*d*vb6asPO1fm9 zEb$T*apH_V6kYr>Lu(MbXOuj2$DP7By;Br>C?U)yLphXnG&*CDNx%O&B|l_3t^E`9 zSoy!CROS_E{ST!gKzD6)h2SuXr8PJ*$48WTZmxxYmAE+_f4(jn1r;rELsNj#Z|JOJ zqKd9zF#mMnAlXtr?}dd>6v3gI-hnGX=bKmZ5@Ai+%}*45p#rHTtv#ZR%w~F(Q4Q%Z z0|iCLV2SRSb)`}U%5-dzx!90R)mTkC?G&%q5CoJ)XD4oLdQ?k>}E`A%*^d$WXI#`+$8uQ-25dZC#C@e#}P{rmC%5 zQesR>+FMGR{OD%j> zdWV2-pCy0F-D`D##(=I)stiy}t;rI4HrLiofx>Vc&KgB==^0Zzb8*ijHFQ>}J}x_9 zPJlfsn`SbPxb4fmCL$(kuUpTf%UT^@MkgrdUQ2x;8lf<@rwK8L&>1Pe<)x6 zSB26JY_=u|rh~YFRTGGom70jDnVY@Q%WVxZ4>mrtUTZzFz6*w{uu?zExDxHHl5N~C z3PuOdt5N{^&Kb3KW6PM&FWt1Nxz9<-r5sRC-Jrr=?rntEX`O2C4?MSBs^<5zZdU?O z_IGaf@$BTCx#l8`ZwUwmt4j!epk zIR3y-QP?w?DhLEp>Qsh!k;j5gAvzHHP6SL$+) zsI12SrPKc{rOHSTwuSo=0U&Q-I?5|_3vx^?%eEC~=0;IsoYSw=1>1l0w+vPJQ;}fa zgl)p(49XWyN!zAOQ`slU+21nNd%SqUx$7%A_U+g;QqnR}zMxPv92gb41l-{@i471C zDFQRrKRY@e&@0+{&4{N2e7Yp&93LTkH*s--+n$+5bO9g$7}FbnR(-`-Y7Yoj=YV3O z8ZHq{PK|F|;r;wC#R66xWAB-e_lgNyh{=b*mjvWNZA)1YSj{i%G4TpKv6L_`qkB-y za|?9uys4{`98l)_TSXCy64Q-r0np+JF7pVRuL}3zGc)81#e*gEQl`uJVEdviqnUUZ z*?g|0k@fnC_WPQ|$@|z$#lr|^U(^>UkY)wT(V9EaJ$sdn&w@@{k4u^ZfzN3FA zlSh01ajBlaD?RuB+b<$xUYYJ3J&D}aCN8UA)rlP)>~vjiT#I0(amRPc zIH;CSJ*_Kp!sE5nXI)nBX$rc61mPiqRR8pMRBPdF{$21>nAM#A@OfELR}tkJU6E0K z`sP2?$>%?A>?T6OM)*Xta9dD$NT6%7Yr`oan{aP3z?rS??*ikiiu+Eld*-b8bnuK$ zQi~_Gh9Vw0p3nh;s4hIHK>?9`xgNaUrQLK{=p$LYN%Kvty+mdt1{i-4<83}eP|oL;Cxzq zrR$%yuc&LXEugPB1u45BOqe+bIu@~1RD$SEKeDbXUq_1*qnB!})N+#8kIfKeIakW6 zCZuy`0-~8NJ@MG3+g2<}4 zT_A`xPT?Cx!MIU$P0$$ia_e)6Sk()2Z|$=-r*Zml;8;pP1^$OZEb$x9_B8xK;9?j` zN?~Et5Mtq4SeRnjNuJt~$I<8bZ4Sw7Tuat6I1_JUYS9}@4)vz2Hqz1V{`1y<1jo!8& z`99XRMCj1w`%^h$?dzvp4i19g(k9|7Hr6q967`zybYBy+2Ryf-X4?FmvztD0JQ@q( zA4`$4IV^i4H;yS|aabmNUs*t&Dg(S+vxhu*l(97X(~Vgt9-@1I#My*4_%bm9dj1kI zzb!_aaJRcRb0>R2kV!+lYn5?i<>qA=_4Ve1C5g9Q1m6x0iMKhw zbGN>!z5gV@H*3ZmxzygirzlsKGIY$YKRGVRx;VK0Y76rS&d)XeAPPOhVtvyzMG7L3 zGad;lRq}d&!Zm16g;vGj6AqiO<~>H@lW5oP23a?_DJARr?gQ1PlQ<^yi(p#8s=W)? zcDl!i+iR~2)8^cCcU-#7@gK3S%9)t(1EXZm{4vtb#sQ0Di=P`OmML#y03dp`fzeJA$7D}`?A8{G%dd(VFWmBReLaI){31A9aOB0ak{`y5#YS?c2!jHJ?dY&Xu@t5D-j*HnN1)SyK0P zrXbebW;fw;!qy#dt0F$4R>O6S z`d}Z?d|6JRESHy*vupb?%Tx)|Eq}k7Mx&Otw=N1hcA!qubuUU;UJza&Hts8dxEB9r z>TAB20)4-TRC7z`SWFyiGhZsR@r?HvE|pWs=-pUylF#ox@+sFPJLw+r z;3q)2k(-A4q=sdJaXX8aQyptO>cO$Den+WkKvhQ#QBb}iG=>O*HRmQ zxLbm`iO(K|!URJ2wZ}h{x1Yv0mkK*K9Fa(jPS2M-Ja7$j)uDG)v7w_fpaitxFUrH! z_s9&c*m*1oMR_$s=_y3j>r%&+rwGpFXGwXccG z+c3Vsut5t37iNL-?yI#?J2m%JnK{+B@RjRZ4G3uH%-J))Al6m?&&SoV5p%w02pV_T zRO*Gq2VH!AO8$p3!B@a6`9p0+ayMvK?La=tS*)pU>fNl)m%EW`QYh`$pu(SKgk#|seB+ta1Hj1v>#dXxpwI()SW#G2T7- z?#EyDhx><%d(oHe)@!uRPbn@N6C0w4jV6)qf$qD&pzh1Mu^V}=#a}}q5}wx)7c#Zj zo3GDVkh#|6K2xWQpxt{$+5rSvX~vFa<7k^v*{_xjo`g(iedcd}NSJ?4xMd-7Q=Mue zw|M!CY{x$?{@LhLx??ZqRJqo%1N>aw>m%QYax&Xaz~0_}e++$g#XArXKSzvH!NBZl zZc7f}N9S=noK{68gdMd_y#x&mu@IZhw~7KlW!Wn?tS|sN@<=r+=Wh?XzpPT1-+6#H zH+F-%I_!Mk!>zsrCDni_pNtl;%tudr_Mo<4L5mJ6g6}I9Erj&BftG5qd;uv`1eY0K zlLYDXi$GIqAF8hH8-D*k+V?8>8R{lh*5i-PxxmfZwSpwl};n~?_PP6J0>MS-D0Gl%G_-U-JH zI5USIk!Q+MyQxiKlS5utsqd5jkQj0Lxdf{}yi2MVJAMP}cFN#|KcQhL7 z8Y zsOF$AE!Vw#C$ag`1lV&@3lD!)JKs1NwcN;yhJ7#_*p$?G|D(zdBEBpRp%`>ZSxebe zQjQEUn5%cd`o!D*OQgrs>5bt>Tz*NaRl3*Ehr54Y!n>`BWz7ccdeWpKgdO zn6q7efDf9gqupBmKKzvURB!ntaXW<7tmI@o755-$o-tmDbD3`F>?;7w-}%DroK*b% zY_L`U3j#<3aIxq;~97%g>HbRU>K;I}kJ@?DjvrBD2CD{0r zzkD>BuR#Oe$gkMg-`5b+&CMHjNFE0*(Z#rt)Tyh~N~^J}&rK|r(Br7T)29~hwx$}k z7!D0mM+i4wJ52ip`D;<%i0ciwm#Hmj(^RgnKWWP#RTBbs?moUmqU#j4D@-59Ni~5> zoeG{M+0F#KqjGnJIq7r`y9`*R!xya?in!#Zm3qs!pV7m$PVnx3BNzXnPz@Y$o4oNI zA_w2wKUUG8-hBp6!Rjc41-ay{hnVMCJ|HINtUHyu^_MljsddEF#qo)4Wgj`zjJP!% zQMrsoa91`p`+9+Y&Fu!V*o#HOx`GW!In(N3v?cwB=cz%v+qaLO#gCc^!;kY^r;5!(m+64nOnjrrzq! z)-HAL`vX$ZTJ+x!{@xQrhc6T8dGy^Mnm7kEcOsL(uGdD$(2P<}Y?|>zoK*3oRncR! z^Oo_2ZK}H9lWa(2H(9#770$;DPSUpD>5gp!R{iyGFpu8kX{YFGw81i6CHJ0+gOz4xWm)ML(=wL8?nG4!~YMVe0mrcJ-%6_t@caAxk@! z3E>$V&fAlp!d{@q*5Rvv{~eXR?P}H=MVNtHIyaYz8yLYJj3tj;u@>`EkIc4v18io- z0LU3jGQWHTnb1jYmij;4uPW*)!TOLn56ZUz=dv*VP~KDA2VzkD*^m&joEYk?Tlo;2 z+qJ$JY<<(I^J;QGOBmZnp-|#uWu?XpexyawjZTK4k{LsIs{*2CV$E5kE<}r1YQo;v zR4rw0k#6z=)F_JYmN&*RVm{zA98XC%>~xcLzD-0jhTwP2W(J#ZSE^(h0(8`iLu*rH z0WHaDc7o0B$z7!nkZ`vf=1np+8k6@<&!Zk^Kp)PV#p6_9a-(A+t8A}9-NgxCs<1|* z;+HgXU;M{)QM3kkGXICm&w;It7}kqZiL=bt zD@Xa1Bc6 zK{tv>Q)rXyKVQPQdjKz)_sX~y>qvue<^^gpuS}%~c52r=b-ag$AZbgi zp<jw1U7J+>n5_7V-k67sg)~a4XPFrH)H%vE z_c`;2ZUsU{8YYzcTIzWpk^yVMBS>5!T1-SMg|~wmsYB{2lrHxlk~;#_uHU*zKE`gX1N zxW4o@_bip>STNlY96ga6s%r)@WVqOsm{&*+F<)_Q8X9UFqKr&|(djWT%=e~?IR0|9 zKvwbRl6;1DM7xY>4O*?<{zK_A5t~$AG2j3j^MP)}yQRU3;?b$sm^GwM@g8N|>kPf$ z@(jLk2Py}}a5cR-uLGUcu!xeQ6Z#bWYMi%RI-V+)1;17sT+084!qsAl5k?PzE6P0h zu_(rva32D3j2YIyckw#2j1fo3b>&h&jHCA6C0HzrAC#G|%SGlTUW#Q%byDWqG)OP~ z$i3|xrI&%+{ydLfEAu8daB>vjaU31t78H7*wDF_LhzVSYfi?2cIQH}ffcuWunBcPu z*|woMi}y<-o^2Ze)$4EHWmY0jAhA=tYqb!ElAG0ZksbF|Ses+p#qNl&l9xGMU%2uWIDpc!Y?0Ll1&-u} z&l)CD?;>tM4)`JXI2qqR4Qe4YX4;iL*Ke46Uukuc26g8Z-sPrg=ZMz2u5Gs7>3aCT z2zpNy@^Yp0K}Vs7ZPzA-4vZI%B}E)UWiOvS+WQ)kDxLzAaseI$Jzx~*b^hOQHO%|<#UmFE}>b$Bd9 zE$rAer(POH%-rmg@d65K@0M?lH_yE{(FX&iw-%#>_WB}~!k1mdu-M`~w#TJY>x2O( z9!6xw8Z1HDDUWy0gUbLnF?I2Y$P-?^8Y%9_-_ z`J3H{i=7E!4^ie_id}+<>kSPgJB;slxK6MZnqJ>?&yE{<2nSxT+MrF3Qk(6Y1$?j~ zw{-wjNo)6*e#Ct-*$$oU^m{zsZZbn~cKG)+I4u7f(tELJd5-6`@DD|I9mF;yuTM)C zI7_AL&;lrUZ-x% zGgv{V4c90g3j{u{cvglqqcaZ!OH%yWJqxP@7lLwwmfz-6Zh@X2pHrK)KmT8!UDMxN z+*xbJbTO!MrfieV#{ijQG*KiB~W*fC7|QN#~TOYxmP z-m!It73>(b!AVxqtnUI9og5N;%G}l8LAiI&G1{uPp&njkf+Sl4(r?n{2uk@@!@<VQ7Mz6;uwF&^n50^J5PPOdU(bR6G7VlsVnF?*_)s}Z5Zyk=snlVeb5)nMPd&9 zaeg|@8Q*An?gg2OzYRH?2|C|{YxZl>kUTK0l&t=79M*J7bRT!Oy&ey{Fy8}NTg`Vl zH!N@On9K2h)c}%Pe@@$W-IjvDhk;&qx||j5{@5U*+s4gx)g3j<#dyc^{@14c$uDo# z-)*ljc{xF#qY%~B&-H@=WNKuVWL;A%PEa=o+-$Et9xtq`QOfI{78uKi`!sge-nu56 zh=xKN@xmdsXBY4fMZ#Qy+%3wyJQgH!c6@wez5Qj!lF$s+eMN9NaXo!J#@23FtGgl~ zYS#qK0z=^+(~KK$*}Oa)ob1Gd+teD7SHmVuU;QOo7Y7oUlL!0i(c2jAV0>Q8EK?$b zYp0A3v{7*%qxYbqY^@4;$GuvAjBDO*;F~w+dSqxg@oq#31@EB~$^%|<>!A_Iro!;j z?TLbMa6VFdE+cgEF3}APq;A#EAR|w_VzRD1k%1gB_~d(xrk2Y&=^(A>j5wzi+Yo_^ z)NNm^xCRC-e-nPQmemu;>!rAdn>oDIuLG&B$rSt@#8w4Erxv8I+=xuE))~ah^TI68 z_23zl#=X>qax%90G($uQ{0czz4njGY4MYs2QmMxL6! zv{>!6D$tObYQk8(<(_G}o9S29mY~#=)zUoVl&}x!x%5bE6Mu4Bx@;o)tFGPX7~F9u z!GlI_;VBw{F_nRMT2jVk z*=^O|BJ~WeOYh&`2|{DXuc-*xN6g(=soXx+9tFVyRYUCk`uRiyQY`hnkGKzq5gp$g z?@T`Ij81p-lVysQ5chWJpWxfzi@P!aYv7u_Ud*turDMcQBhp)Jvzt=LA?|$;W?t^t zLsMQWm250_%$D^*90Lolt>IbDu-n+WY^ok&!GX6`&iAJLr;JxE?jXs5!S8zu&|9Cj zGuaW4_qW^7bHcpa0$ArDiKh4Fr1B`TdJs9A3+b^;%!HO$_*%=RGUAQEnmCD5ed298 zR*y3cK^CV6scXxB(O z&TcMuO(R%rfJkW0Slct1)Q3Q=8a?uA@oHN@CAN8|;er+~O9!6j{|ZFx6M5D$yb4DQ zomZy$gp9YA&3R_~4)85Bbc9nCUs-~M{gPvvFlWAfy)&iVcn1cXn6S7}8vlI-bJL5q zYQne^^!KVDZY$jWHj;c=AhC59< zTvaUMjr~6q5U@F1U=6k#PS2@Yq_TivK_d6-%-SQqx9-5tb z>pgT!i?M|brsal%VthuZcgCH?dH@le1nuTYH8oJuNTd$hyfv-;J-246cy4Nf(5iV~ zLrRqTr9>Z8YC=+}HgBdwe9@wRx6R$gQCIq+iU04d>oiX>%Qi-Uc;ytgH?MVk;6{}C9B=jotW5%8RNOEk>{EZ2W$K1M+0 zo1uTRVt%UkAQnU7AF)2**0j9RwE7sVOS$z6)OAfSV8cL42o6ve@do$dF>YUe4} zHVh7nSl%o{jRv_n5G`|<07#QMEOO{ul$7-Qg!g}Hl`TJ{H8&6Q5g2J6dX8V5qs3;J z-mx2jH`wk{xyi<|jRFO;edG<6!AlPc7LBfIO_sxR5v{%*H-^+*z}7nE$oSI4%PPfh zEUPc6b1q0iaZuME+Fx+OCuCn{X{*TJj41*3)!# z(AK@AtkY`HCcJNm?cTmk^0#1g!k4|uJkKPb@ZAFLX188+c3Auq?A8qWxX)4r$1*UiCp@)&4FgV?p4MqipfV`wZ3P-j*)ju#0e6d zsjl79nUc=w$?Iq15K4$KtjjyouF2xMEfdRlZER9hM7w>$DT=`=Lpn~@j^hA-!?w&h z`Mj_tYM_vD>teOVsuex%NoK;;KbJ$p7-A^|VSP@Tlna!wV`0U*hLK;U)o?6}ve8^M zJ2z|g)`Ts4PDuwZ`+cL8TlB_80R{VjS~k+i|_tTny_hg@G#S9$}6TpVZGb9(57oOl|~Z& zCZ;;CWpiCT4C#BD*$)FL-!^xA^ysnBjqDUzZ!?+U@1rg_@ZHS|!%Y!)i3P?pnRozJ*-VedEePH0+Ym-9jFku)^G4w*$@@`3^ylFY>*u!?-V|*`FTD|wwqw@e z^#Tg^A}6Y_K!zUm`Zh%EwVYMpjDRp_+69eziwkKaF7Z3a@I#AN@W7!2~f0mB(P0RU7m55zShh!HLdDB=MG;vK-Q4Xni2jiZpHzc_Mfn9l7&}Hm|AR@IwWQR@+KD~*SYn^4kKyZQ)qw)h%GW0$wW64wn$o` zH<|wr&fYtm?Ku4R)~QPEQM-1n*gNQ^W^0ewt453{qE<`o5qpm=wDuMRwMT-M7LgDn zp)EmZgBm^go##2%bIx=9e%HCq`72lQ*Eh-M`@Qea{dvD%Z?z;C!|B#~&+u;Yj_DqG z)h)%tBd}GPiN|SU5CM@X5m54lZ#MMOeRJ6UvQ4rDaN1wYt<#n4*-)dqJ|_F67_!2? zW8RyFc4~eG-5sT#8u5AIK?kjcf+8bhtvld~#OREh%_A*z;bIeuP~5H5?rLm3i(to6 zNsmjwT`;C7XptFk<1^fTCa9BnGtBqS)u&a{5-~iQO#I9@>TZN*hkOIuNA24ML2#U& zSpG?1+)!zjw<_>3O^G4yDze_uM^ugREFDC!Yq*^**}v*J2zlVqdokk5)9QOc$yxIq zf?&^btEW-z#;ObLTBF5OvfpE3=1&7wR(hB#96uxTl14gYfpaV6kA13Hu0gU&YR%-F zv%nicsD&o0rr3Wu(?N&rTlUw;O;B~&cL=cE&H;{k4!@pbDcpB1!atg$TM)fN%xcPq z^7n|`&Y%q)U%tNY012YT`79H!`6{{kmQkGUFig@UPF0*YOFvna0h{kv7%cqQbE(69 zq=0+=!=uc}dE~Y_rd}3YFQxJEG*3W#ra6X9;I=6sBu7_bgyA` zQ5rLXZq_obSBb}HnJrbN$UN{X@e{?F>3nHWNeN0m9{B?2+3_G~y;AAA<~2We^Tk)B zOtJ<^dZXMEUmh|56=Z7(!f{~^d_^0?M>VdFgye-3|BBncl{h1g0z5E%~-yVZ^+1!BY`qS^mU#o`h6a8LL4PUF+{Hf=!E`(pzyFtY87hDV`Ju14o{@Yy@ zKR=atVjPvKgT%VYBO+Ab=GvPu7H*YymKn6S_x<=(D8$2&j}Lz#Qgn0 zN#LkFALnPpCx%WQQcP#B0N>(^*R#z@qx0)@-Mu_M8&Yb56P5SXsJtu! zV;~XrYWsawmy&2%u7`E?lY}RyKjUxvh~7;9SrYZ`JKe{Q64?fMBYVqt4ARdngq5u_ z%ibeYP7HeU9mTW@LFTK{OIlFEigLvu16l~vP?(@w>`*jb5U1bM8b`QUc6hnTqtU*> z?PgC8%#p+mPEU1-L}l?9PK_u1meupN3cmjm`?TrN>)Th;cjy1Km&Uloq%_j4F!WY& z7s&7H37LKds*jC?x3Ui?)>cWEd337z9rm0doLL%F{p7gE_g!>C66g(!gtnCY#xfHx zFNmnORa)(?*N$puOSH#jBm9s2sJEV`3}*%F{J9$*m~2(Mqci;&+=7)=!nk^rh_~Cx zHPs(>xJv;r`PT7*f zdESNjn|2@5=NHM1dTfKB>jXcg!pLXPHe$I3;yfsb+G-zVZ*2dgxz>MH7Sa~gne1_z zmF+~vbfT>nRgue`BzmKx?6bjL@T=hDjbpaAquH3cck@5Zb3Fm`wo# zT45@Ikovs*KX@^po6UVETMyKY@QUO(YTj#1IntsGlXVPB)LxEu3=V~#u*C)UV9Pn} z!_b_d>?4H#n*WSK+FzWX4)OW8!fS%+>GB(Vkyufsu0~0jCBEVrH60ZFidKdYKm+oc z=o1D$#K|bt74BFUVOmq&BY@#QyiM#XiF_3SV~)eyh9PYwt&6JkRGNRJcdKtnpam^& zv}*acx~P(vUo<;k^aFZ!3DpXFb8dF9uU3sL(YG|@M%2_Gx=D=Ah^pi9$cile@qqmz zu+g2Q>^b>*D=p=R)I_jF1*EcJRO}E9V*jlzsp-A!X8ux{!|w>rQ~kaE*E7!&ZvXvjlTJEqy9A%JXq;!m{i>geHNZxK|$%mU!f%UBR>6<6R?FuZ_ z6#*P=wVBss7g_8e9Co*R14iec^tPM}!-Ge{+T6;I@h@AMW>zGpT*%Tk)0W5mx$8LZ zuC%~0LD#V1v&3Rs1yr^moWQA7*&jv^d@yn7^diCiUNo@aQ)A0kF&sHm4Z|^j(5NE0 zjj)^KjmWcI`H8;KaYeyqQtD{H{O^eqUe0zWMf!vdeS0dws|j*wOrb;Z{4WSvtW07 zfPg?_lIMDo*-|I4C!*=CHqT`}OIqB5rXSV=TDM*?G1CGKCDwMv>$34-3ZcnyDTqTh zHx2&8(syV%ZtLFR`wISwigHYg?O9dYQ)bJ*y4*S8%iuQvlSzBWu5UHhetp_~YiCn0 zZQL>EU+!brn0a&-eOBqI^f}*lJH6#doowq(ZcW%YoZ;jWZ!!JCKZ}|U7uIy{dVSyR zdTP?+k*bjqKU<3SqK}G9NoiB8uA-vmLr2%31 zQe8marR8_1wB52`SN1T(bka`uI^VrOA!Gs4(R319mc2-f_>&_WV`c(lqq>nzVJEJcQ;n01l-Yd8Ub6b;+LqZ zs!b=aQ0eVeN`;*gLW>_S*Xq0m{PfxmBK=K*1U;LL8e>E#Z}46}$;E4wOS7(@tNyww znTcP(FawN)f~ih}pHxp}_4yDJ4Z1*V4|lnR{{Y|l$b>)x6SFINe9 zfk|1-4C2$e-siy#<}~Cq&T*_ij7SiRU3#w{O1gpZL&i^i+E2Up#a@_yHA|Ik z8za~^G@r>lyW#8fLZmyqE$oBGRV~LaAiIPK(b`PIHQywRZD$lkQnjJXad&|rDrtcllj6?KS4wJ&$J>n^dZT^f#Q z3HZ#F5Pg$~n*sQ*DIbONmBmLp??-DLw*&^@nvSA+)p=Xx|T3Cf1lkpbA0){ zKcyTxNCuCE%fS@shbw~HY+6eV$RP!UevxHx2lA}X@2Ac#M>SEG@X%!;vJizh>D6Xv z!>1T9?g?l|I-i@&v_UpU!(c1|vf(UTfZMO~>=lyLnYf|Fqme&spQp0drgC|DK%;vV zq?d7Hc#R9AJP8NU2UNLIZu}!72bNEIY6@4e*SaTtU+if!^YmWm#tr!Ej;zc!x=OTE zGZByrt(}ZSCuR`2bdefJjBMp~^!17dD2HK+{MrZ_y25sQXbOXDZHdAz7kwyqEMO}p zhm}oD?+HBr=X|}p`sQeiI=MPcUq^j=VO}&i71R~IA>BS`X!7oRrjz1H{q$N9y5Y%U zROcg&ynB6)bzIVs;WuPC4NJ(beUsU9No{|h$kSu9Zx`^2K3~XYHHOsJ7X0cs+A!r7 z%sC_}An+@-!$yTXExHlZK3Y>2v`b5Wen23w&0~x~bncVqA4lUdv@i z#LqvYJPqoZN=&ui_3z39HQ4gcdzu##z2hSht>C;1M2C={m3N#ux-LDTYtpnXknScF z!Cv-`Q^^O>&pVtcfRnpGVW`obF@7cqac3X5K`V6dKpGG9hDEzTLM@NS3DaWzZVEY+ z4vu(2zLsuLWC;)B?xD2V(~T2kyaF@fwVhLw`AC0a|CK(qUB3Qxr%c^Ph!E-6V5~}7 z>Vdf$DiW3bCu=o_l#go(WC)Pzt6p)nZ7?}mCndj}=?RC3DQ|uKvcvw<%!Uzs@;&$lB3N&ew4`(4NX9{MO*moSKb)vmo<9-TG-~^MzV6d&hms{Szyo z>|R)cz%xqJ(ZLWzx-k*p2zOt&ZCftwR~8AGoeqB1ZVN#LNVK5|qZ7iISujfrpO^UX zQkPRb*O-{-XGwVp``wCxunbdYwN}`$M~8JnIF^`O{-o921||Z1OQ^NJg_VhX8vE{I z%SR|F^*SuhJ{a_xSk$4BjIK%zWT){OK;+}`tNtwHSPV4p|e&&o`&ikY%E zK^)EX$vEV$zoF3i_Z`sMP9*hL$ll1fJY!4flO!PC4dl8WyV-0wq;NlSWJ*(b@$yfq zMw6QcyIS4h(SqSqh-GD!xG-<9OZTbm4fUWk1Iw3hXb3ciw>7h0{MTBKX|*QWQLaSY z%YDkheYRw+@rw7DiZHa{|v-w7^epPVV}hjMQ@X865uP zoUgBsD9Ah={VY-`&peiRGazP-5-tgOZ1aaL3QXV*%rm;6m`0ZZ+N_E@&MIT zm%80ERY0oqKgmRu`E*6)WMb;oFk!t9Lpr0TNq(=iGa8Z=Yy%}PNyg;N;Nu2&Uo}K; z)?r5EB+gTt)8-OMg&j)4r;`LAh9vzva`t=Z3#P816|n-4&~JOZuExVh)4?bBt{Y3wPD47pJ=?R{^)xar1qPFa0XKeOCQ z+k5|GC(*%2Ful+>3A+KJU8Bv3G=Vs!!)>io1Qqr!LVYta)2I#hliy_m(-l{vC86&NmHNG$k{Qh>yO>@#YoIB{ z13I^Ycr6S89swZ3iN`cFFLyU^bnKkHbdWs|s0!+3rmL+7 zY?*IXZr2tD)Q8k-L`7e5-pQ?Wu>B8@Kwdu5I>Ce~XWM@QXxaNZeV$KI<1D+C* zIfOOejzv)^Qd&hE=qwiPk3xe?pV0~n8F1Q0r`dAU@k^BU?w>jNu z5U=6_0>N9-|Ey4M%7~bWcChM4?kNc$pHS*RA3Ku*$X`%XF;yC-tNY? z2BiWq+O-U2+;y59wHz(WU@|B|xVYeJM6bJ?WXHP%oVR{>09EL%QPSw#|Ml)I%LB9< zBUSC4`#-CQb^e({Xj~(aXL5X9N?6?&x4ouH9+us1?g_SlF%r zF~^;&BqR|9Cmc=)df^phI@(SSvy&uUl~v!5{Gzo#y4`zcE9Od4v00uJb&da@I!cO} z+;Oea8Id7(HW=p0g0~yf)aMuqQi67vqDJLzwx^irp=|3KPq^IVi1YN@^h3Dy%^o?M3|!sk3*#&n#?OG~$J3b3akP!vAVQ^9sIBxo4vY z^OT(%Nj=WD8B6{N2N+K;nldScjeK?O2~-uyv=?&Ng(z5W(<&DQhH+V7YxpS(SyHHn zc-Gb)KQ@1{fV+WUCq16}RIcHF zLth0aq%ZKCFhC=Ng%xtL7;M~d<%8B6R;_VaHUDTROu2dJ=;_0^yZ0~WJz3pqco4Ut z`;aECGn+cPko^A(_8PrDSI4F`j&4dh?wy3%df*+k@mK@=ICf~Vi`S9t^;uq8^Gnf! z9OUq$@XwIQPp&vHH7xzQC-CGodXGdwXY|>c@)*2rReN4F@oDku;$|y`u zU$^>-(r{S5na?r~iXS;zf_f>U#JdYsrK9@hSaBZ6dLwr=6*bNU%Gt__1H|H4O{8H+ zKEX9{SmLUe0FOp(7F;6xX4wbXTetGG3m4^Zn~(o-HyZ_^F$JL=Dv|P)Ds||HIbAi4#zWl!XS9Mb_BR z_J5(|<~Znsyj1ADkrK z1cc$c>hU}#a>LROVd6s;$FTB$x(%+S^15_p|IyIv2qPXr?^5)TtL3~iRD+M?8s`|V zSTJ4lcwbb)A6%L`9mWjsEcF|_;@P`p?I*dv%#wA8GRl=VcV6F(%35IypZi=bL9H1v zH#wy%5_TBy(YH`CVeDQlZ>Il){Ym1 z0vpt~x9WGV;Ez-d<8e|wV+p$(#2O;cS*|N{>FCt*(@I?7!=Kfg_fl$~dCYtE&y@j$ z$ulHgLZ8LRnr&1CY2}pNJ$$O_t3r5@gZ(%Wi;i|{VSEOH`Rp&Z6PnVYccmQ=$0R~`~vfhweN6wm@A0gc@dWCq?J;S?}wbJ7UQO4O3e zfGSRmH7OPb5P}s7IT zE@X)~yfoAA{=%#*{&)ZHqH1#^Npr6@aj&y2I1{0>S6N>xAe-kN87x6J1{~v{7buzx zH40rNrtsuf!k5dhJ`6AXUZZP<;=P-AO)`3$vo8AxLxyPQ96Ktk1`G%a1o>+zUB;gp z;HUY9ih};YWT(URa^On60hrs5V>99caHJP zz>l8K`}W^zWiioByT3C^GX8fbMYLkZ?7Gyk1#BZ}cXpNP+f)pEU!Fut>ry4d9;3vVxgGy-_Fz0+V z0%LLpi-JL09iLuU%*4VUz1A!IVgF*=!ML{fP;19xH=iO>p;YN)-YYpbET`_~D0KEZ zOvkj19L+X-Y>XKR$(E3^Ia(Pmb-U6Bk?jpa7ZkZIZUt{DcyS8=f^4nfS+CV6C%#5z zqfC-78kM>FdiI6wtrZ{HQy7f!CdQ!)OgUoEk7JPSErwU2t&X1Lk%e0i?t2flXsBxC zP4jB7Hs6M>nCo1EsYtENn{tYvDkND2sf2FL-Ffs>MB-3&!jg!u-O@jryL&%ZJGdzq z(O_<>a7|Es=Wf>6(6-v{buM4oT|g20baJX40Os%p+oJL0^+$2{2vLcZu6}mo1a}n< zqmysZ{VL3n^yuX6$pGb{B2;fOq-HXez@e75&6*rzFLydpo z{9f_N>feoaM>FWT%RYM?H2gp1kZKiBu8#PZ{*yh(`+0oY3J$S97qQMIp0O z6R7+dW>>~QpZT)q$ERFPnEPuXm0LBhP7}dnL#9zuf~&~C$>rY!55tG3dzS*3)W;Kk zxyYb2=rzhmB42Z&y#^B%No}G!39#Y+Wx2#rq11)Xchv87h4;=oWIgNB>vU3FRMqlZ zuoHXzD0{6G)1*zT{Ep%stPq1frSL$>V_mI+ZuCSdZ)l+P@#6i?6P1NqV|#jv9U`KY}#hLzSB~iOS)yhQLuV2Oo zod40BDmcT6%XUwvF#l+L8mU>o-X<~YxZysoRm`v+BlP(=LhZl;w7{J{TDZW7hquEn zyb$2dB!tGh*4Zc72btAM$?Q-wbNi zD$vWLG{h(OZ*C_6BafI988NI~dJ2v(3y!%St$KQQ zHJat9;`Pz_fGZK0_&YDl9qV;TawZ&>Sv~F_CI)H+zT15ccjOrpB|_w^u>LbHaP(qg zh6=|b$!_7c9jPq@Dlla9R;~fzxr(eal_9c{rhQ(VABdNJX7Xk$1?OvHj0B_^&Ei{i9(y z)nmIv_>DmaDUeFrU(yc4Tk@?Ae6p)Ka$TDS9q2)fTKB#!YT2FlR)<|!iy5GO8Q@jf ze(&mI!K-@7(a@Jm_O1rOpk++=c4*1RyNYGeb78%kUqGJTeubfa-oKleQBMU{wNsdn z4n`Ac_Dx@AcxG?y9TgO*caBu>v=W8Qku8fYTQZIh$L#OJl=EfPSo6t0GsMIe>-X!| z*JnykL*F+t2z=>h*?~5YVbj>z;SuDRTv2YVz*P&@*J@*n5yq~It|cBV_AbHOR)q!O zAN@W8h(X1Fu^|hkA%S04ZV(1d-e<8-MPATZjSIvfz3R`Mp7keoBYzrLSw8=tcNhPu zQ>4LnpikkmZD(@1=uo8TN`U$%EiwFZTA1>9opn;->DKifN<=Q7=gyyvNZvGQ@H&=8FUbLKe1{@{w1gX6=y=1jrGgYu}9j_w@f3RU&c-paPS638j==qs6g&)tv z4P)m;*AFpqO_f4L%_O!urpb(U z4ja9T(6Co2NB8zdviBF-q@|H!DAchNU=5=yW!nT~DJo#Mtp_Q1cdYCiZZ0)Eavki+ zMRxFdru`Yj#~2O0@WiE`G(6l@$7w@XD3Q>t;6wTz472KL<&tG{>;ao@GW}5JCHI zS7`{~D?Dn*aU#h8RivpVs=qlHp#sy(d2Ufy#12(mG_jA9ugiYF9HZurZw1!_W`s`< z+VMlxEJs^IQPkzCd*BBN5b_A$2wrO-N8zU>i2m+9`(YxhM+pkA_q%ty-0NO!WxM+f zfo+FB`Q3fRU*L-MZ)r+4UKUdPsQRtpL#o7`;8*U4zXN%^+kLLSYc~e`Mttw6~l&$ZY%ceXK=s_`ZR1@LIdZptipP--@~-LmpFkMJ zC7Rgk%tPz$W$Y(pMl_{OWZ6&#ODb{9C0i;SEx8COv`5hk_ zJw$y>VTO434L{lmdc?au-K0`{5vjBms9PtZQ590;W>i6GCooZVmx=Fss;A{7xjre{ z<3zRaFF=Xhr!)pu#VH=@n3z+8D5a+OP>XMcjo}FPY+F%EZ0s z_pH~MSl3HcZw|t>(%zEODPq-^@I9`XBVDiU-wtt7wiLD{L~Iz+*K_}YFBW!%i-ME|C$X{852pojM)0|Wjbra6V*@UC)Yus!7IfpNOF~-_s08z0ETEB#!E(fgymRdbPD}#CA~rJ zmi?&#Sq*ERQ|ST+#Q4W& z;nv5L1fsTL0}FAbnRTPj6F*>u(4=1#LJ+5@x!jeQaYPiYe_@O!|!H^pZRrxti@%xSv*vNa#(UykR`XXSTWv185+pr ziq|k7&I=Vsv6}@5W30MFULB{=Zh>sky!qtdX0ovGcF`p(5KP9d(OQjcFT4MfcRNP8 zne|UVfp^;eU;4?30GN(2rEz)_QBe@dcQBT+5XW^ z!^A3WOD3{}cYdu`DZ5W}er{Q&#+|Ngg_g;Q@a=A&68fLf@*&(iHaRkPges(-afeiG30z$v0W#%03QaETUQ zz%WC`MhtYmrZ>t^-!mLi?43pWJL@w3nJRMVC32S*RA{c>EDQ7pf7XJuV0J#TmS^v7 z-q_IMQj?uW1haiFaq=EZa8xD8h_F$Lj_!57jTX?@cea*Y3#FhHkCck5ba?xhcAp5r z0AGFkSLRRr#cn_B=G6ZM>V%gt+9$Ok#H=1!J$1ya9OKL*%A*ZPHFKExV_eFAHWm6$ zEB62M;Tcsxdieu~c82aaJ{F{MW7fn9fvDxLl z5K!MQp?W|6(G2Rgo(y!*2L|(QNP`FG@DU~M!%toD{zSS?4&5=UtylN5gcO6q{TxWF zu62(LlLni+))?!J5h>oJ@&WScf%M)V{9(o|1wk0Xm&~Z0<5V2W{Bd5#X;_BR^T@+3 z+uktRVzhp%^?r6|2_s03VpR3yg#jkYyQW;Iw+9+m8{~|1@)h)GovkG+#VB?0x~=;z-MPRsww*n`9={wbuP6n zH%zr{d{D=qBEPB_(^A!AqwUX0NQA}!%wdZlSA-RSXn6mw%}u-biogWm;74edo%q7@CRbgCC& zSp#kT&V>ho4QA|8Os&3Sp|+$O89#wu^I@(W_ao#3e->y1KjbL+fyXUAIA^astIsVrKyL~+ZmAe z^@7J|ozeWztyskKsFERQZzuamctM)HKd5t!a?RM@uOoHd96G7s`@vV`Q+Z*>$4G9&P3!}68i;FyqpWZAq-H>gd{Z!W;Wt#Ep4$VtVL9o@Y zYH0O_y;pPb+|UXKLTZ)IG4ini9u$*>cnHlv1#X!H6+WI&ln`&37D{e7=3dN?5VZ!+ zc`C8109K|_EEAXU!hiozNUA0;@bM8gk=LQ+L~Ku*`oU##?ds?RZ8iXuI0_}m=*Y>F z?Z506q#|vjQZ793KD6OXbCL>~c+6OHWnNsT=V|AiUI(z;l+G-Su{54^TaPK?E`r-l zQKNXIoE$3q`aptj&FtNZ(vB1X>x4_|@xa_z{X7RI`&(){_1COl(Uo_; zO)DaJqZ~AN`v*BWADRPWy0S?na@;fr3JrmzA@F-KTIosp?d}x53i)32nn6@(5^8V? zV6E4C=-b{4L*FuG@wa8Sf3a0%RdJO6#qY3TByPIq_fq?hHUS~wvICgmmFysc zJ{@nr8HcIPz##T@SZv6e60Z3a@NMpu{(W%!o!-pATTqA9nVaWxb=@-orRwIFW0KVs z`TrE;`Sn9~p4C2yYRj*@oB!cq7G%(mOAI4g<=TAOBg)XrAC%u+b#Mj;^*@KBda$RQ zCmH}X-!wFrbo^{gqD;BRXw<0NRvsX7qD8}dLa}Nx8=oy|5cR$9WXrw*PSptTJ;JU_ zEnV>c^XXj}@Aq)u?@xZ2%3pzeDem4EeCW2K1=LzTR7z#=m%j`>v}s| zBt5WO<0bhSAYEV@pEBfn?Mi$TMv19ifYK-+KnD~% zF#=O+fqWaYwPT5s zs&Ga74tY3E;3jul{iiAuGs5N`#&V(l4*cVRoSMB%{(XIL7oV@M9q9S{`Y^qz74i2+ zb`gi z?tEn3uWY+UmlvIVkN5iiTvYj&-Ct%A!vAQnhkzLJHYBeutP2p-SNUO~)n_TzD!tEh zri`toukrf1n7rV#u{a#ZX*>S?tD}hAR@lW*-xzt^q7TZ~3ff_A;?+wkHya`@XCJs8aHT>=f95(ir1)55Zn>VGbEj)vWG^Vm#9<(A z-5r8=%?(gqsu1h8h*uf`Qfo1nqNfWNz2X^_`cVUZR+phZVQTSHp=qAM`4e3kZu!Wx zKXW3<#af&XPNJr-Oe-H-J7mj5wVnV>tOM>)^F)V~=p2+(ydZsdMBHx_cZ?Q$`v?hg5?F1v(E>m$hGu>Ke0+2!XIbV(#*1rdVj zQV?FU@mjv=r7#nb^#;XmXy!zu7Jgl`kQz6*AE28 zQApQOBl+6 zgMRxj_&USfu~m`WPzy4Qq|t8&wx-`ufQ_s&G0m4IcUt<6x1MI>UEmJ$zn{&%1ragt zmMaFn23nRo^!Xm0GOX}Ld{9vl`?0@%N2TixaN_Mjn|NZz5UOSo_Dmem53bFx)biKi zTxqZH+T6e)l&RFMuZ4oeNs%Je+iMxp1sD2GzTCMp%xCdbMo;V@wvk7GzoDEx@@cT% zMNpRXa+A#Lnjm+P?DhMdkpjPX`5unLWW94$j%H7T1t)tdI8kInQ>DvWUUu3vlcWxpiRFzssHE`DX3Ztc}<$cHWF6|vkdI? z_EoqaTC8WssoA~Xk^T{t*LyR-qkp^m44R==DQ_UwDFLjfQEMW@x{OD`NT6D< z`xb?@T&{VKqLE=MFQPgSTH+qY>d#~GVJuOK!QL0=D@9aM-7L#vd~W{%+VVBs$ag@j zHM95Q0_+lWc^Xg8>@_?l95f5~@gwWSt5N8U6VYx*iH=WKz}he3U%Caac%%9K8VIwX zaOr2CB;If5oqs%hU2*s&;5+~)h4(s2XoXr*i2J5m?jrU(YvARpOa8~^oS>-%Gl5EPrEa82TJ z&G+@NC0G{p(Z=b)ki;v;+pE*!s~I;Y{7bF1ld;~;r~pK3_z-~6RuMTJlDT3oycU+M zb9QlcSVI9=4rsf^JQwvX83SJiM@7Zm2h*YHSTa!Zo#m?Y0Rl*W<=oOdRUf* z>5vn*L*(5((18M4%Vy!5H~a-Lgl+rIXSi3zc4OHqa>j*WJhbCgRaH?+!N9uC( zs5r_a5MVr!QdKztHfGoPWi4s-II2{K<5c%(WTGo4K|?OH5l*WPf-iX>2b+9Fn%ovW z9VYhPt?=F~ccF}`L==S>p=TAknn?N5=^4Mys;(LdYXNLh5^iN)DN9^FQF~(S)55lW zM)86^uR!t{BZGm@1D#YoZ0-g1xs9GG25#KXcu4g7Mxte=(MXfcW?~<-}LM!ms7W|o~j*10n zC!l?gGxw*CPoioWhh^3C?_S>xo~TDZk4PbPwhyFUL0ffDPxtA#xQ4Ddi&er;ERYy_ z6v%rz`b%`kG_)M=-?uK?o8+22LAvg;<@xl*kUWx8DH?$xj+v){oTF;NIp9~%UB*O~ zh3wyBh2aES8F-EI;UOb6)7~y!c{bzh`Hwd*Fe}B-8=4`#ry6zcv$=IC6aODTydiW1 z?>ibQ=iZ7}pGmD?gYT$iEY(wFVg#FmH>H+j@}F>p6zG2V4cC}{IVf|8h_{9PL5v6G`5ryGurQ+Lz5Ixz!GT8&Z79m&0EW2mIdyPBq znbR9sOr&$ao6TmA&Aw2hXOL?SFfZERo*L;H1?LgE($R(K7<9LX8m8)Qkk2JR--&!l zS!f=>Fo-AGqM#(L;J$2+&>`zfD(J9yk1=qg{C0Bis}V&1B%klq-`5a>pOsEq8Iwb= zTzCfvB4gUSidH)^(X5#}UoJ8Y($qK^JXvb{k@8NTxMNckzYJe-x(yL+(CYV~pO%BO zr!9!Z(D!;~wDT7zo^;5NrmVCwp5PUP6%n!STn$Jcnb7YtZ}SJemn9!qs-<-`7D3IT zrniPbofC-1&wGb_aN%7+`4M@}<~w^gBUY$@^P-exu$jb=r`{gvQ7*o&<8bfeJ!`V1 zQ9tw>IV%_AZrc*WSuPIIdhkKE%c}Me2q{~^h;=eoVf`QQCRhqfkK3_-kY!Z<@aejl z#WhjOR&ij`xEi)$fL{$-Gm3SR)~bW_E?igKtnFQjGh(04iFTLAn%DDNDpndu zz3U6yD3Gxo1#HUgr;r;=3a-Y8xz*0>kJxlwkE)+{>X@3A72gIQyY~ctecyZ4_^jB#xcdygt1gF7SeePT%piy9+_ru zVuTV5&)ESxzB7Ih0F9Y204`{L(0s~1li+z=!RJ0-x&VW0S6o8X0T5>}*&(?@kdt_; zuDJA`yS8_~4RN3wQx?tsNqz;w*I~77D>r7Bi<}mEdJR!D!5n@8)J&fl42`Tpq<`*YE(J;4*zCzhj8T^eb+xQcb1)!4A3_}b;jnFMl*(WK|A>Tjd zm~(neV&9L?WA9CkgK{doLZh`?RMJ*e>yQfLEvf!Y>CFP{Rtl&0ZtUPS0V?jEvfViD zTFbnTPk3P$*=}d>rVQF=dK>VoVg@)PgMOCtXcy}60q-my;YXZ!j!MZv>v5^&YkmrS zY>^HM(E@nTr}3792og0?%oQ|`o)8sEjmRXmq<3#J9(%P^X}4G?FE=nZ@f%oNu=bo# zH7UPyouE@){8DoTL}0 zj7QM;PlW}ScAV?dw+YihI|aiRq`8{+*`m6Vhm{85XhrI=^}eWnP=LvUUjNWQ>_PE*6M6vwI z+yr+MYA=O0RgRZ^cmw_`GrN-cf<$Bngi6fo4ISUz*-sSKibA=alsmi#R-Ld8oqYFP z9oJ2u3NTPeLX7NY$vz;!!1wI}+uZE>tLo>}$C_~MsmnTBh594ybqR*xL^(3MQTz`` z02w}%ePDD|d;eiCE6>ZbwNkS`-u@fEIf;}0ysc(`@xn&06+eh=UaYA&?meZ#%RBW( z3eKOF4*a7r@|mOlZ?s*vpP!su!oKHbydQ&XT&Sy+?sHU(tPt&<>2tRgxF1~dqTi`8 z%+_SO+fmLsHD}+S%dmdZch{&QuuSMUSPZzG%%r}RYFl%ExFYFoV!1Zo$<%!sg|jcK z_}qgE)B#)d`(+a)%I;#{?$^C=9U}4fskSD`!YWT$6Wk(ZSw~wDj!6N}$?`c&f!ass zOJ3Q+?e+xpsN>1!>pz5s6gjoRJ0E3(F2@%X+uDdqhdlOv@RYuOX)6kjLO4=KHd8C> ze~BPKfo-`7W=}e8mm2juv0%${_E+d9tC2w8Ntwm*ZIG(_ZC2HegD2F zEv1GQin}`$cW8@yaS75kNO2O}ON+Zpu?m4w+=4^V00GkCkf0$@iUx`X=*{oWoAd6R zcV_N8=ic|`y_x?^lG&4F_MTaLt+l`Fvp%vxrj2N7Oe zPeE1NhnIAxpiC?8!Y;=gU^a%X8oJFe7-98V(f?u15AF;&NlSN3TGmv;e>P&njF1SE zVzR#Bkc=(iKDx&&lebU|6F)t)fpZKPWdkP(1xsUd6N4pw-5E>&zEC0fu^xY#K* z&n%M?4EIU8{X4OSBxYA1MI|luVLKw;y`#|=r}WF%gsBg2!`2t9zsVxkspFHPx^7V- z=TR2GZYY0Qr-B* zx&)FF)+NK)8gmRI4N(TxHZ{X%M~2SH=hKn|nQXHYJ!!}7(S853y(5n^ zrd;xPkTXm7#Cx78*K{)5$t3|b=e?XWD-fA#QrTr5Ip-1UBFK^Ir;On!6Y4hg5RHoaU> zD?C$qOS#wlR`{OAI<&wq6RFsfN)&zGliFi3%R&KCyeb z5!yUrg*UI?kcLaz_#?wNY+xMYwbEDNB>9c6EI%%Z1Cl3vbtUC;1(RB#P*%^!K@ywY z@caHIJK$%KM}}8naHjHs7T#J#i+@f-zs&WCRI43(>X`m2mlx^&6^fjyhi5}gzCJTf zplC78%H}J*ZLoR~?vZ~)w6CFCj9F0jN{8#12(rNs&TMkdxcLP5^b)H#Z59r=iCp=~ zlt1EUs=E`I1S1-~CkR@mSA#5L#RiA=rh5L&?S__SV^$`x?a5xo=#E5< z^n&;Y@J}~uW}S~}!cK_znr_DH+#A-5#_Yoq>MOr>*1?{*eaeW~*8WFp`35%Cp(-iP zI!#Lrodt5;s;wiQclBRqdp3=}cl{td#c40}WL@*Ue=MlYpkY-Y+Zeg^d^d99TNO^~ z@aHpH%$Q5fO!!8{F$cbyfMfMk3E^_Aila|Fl|6y7H1z5CK6>!DkD--j?6jW59|OvC z9x5obau`$pz{E8oENw@8G*v;C?I;j7*k+OSB-Bfy-@8T&;eDl=zNP^^Vi8Pqq9;>H z-pcukX(1R9rq6khwGn7; zpE^oaC1=hZ(uNM=&UoW7qy|?%8W*6RK=MiMR6n4=h#%yj<94>KBXjYyYgH!Ka3kaO zmLR@|R8{g1@0*Cqk?CgGI@^n$?F>GiOr*o3iXTuvaOdkZVXUr~?DL@4AuX<@e7lj-j7O6&Qqw40hz&a=cgu6!&pTTVrEP>h6J zXe0EPuebH0Eos%#Nj=X&rqkrU>ncEEaX<{2J)~d4;0bcf6r*|LoymoFt`vnE^ng6g zFeI#uV}~fg3<;D9#YUh}b!TkC`a+CqDmn<$PUZ6W?z3OF*H#w zhOyN^RsWOt3YnJaXZ}RWF>w?jSCf+^k4gHPRk#YDK^?|2dw(l=BS-sF3q6%EEnM=e zg@XIXshTD()hu{7AceCQ;(D((!y%sr7}1Fd3cESUc9dftb=+bf0qa)nU~aAhTf5sV zh?_T$eZtL#w!uZ*FfP3!dEKMZ?)9axnN|p2uZtSq6a{+=!31>5PXV<^gSS}FbF313 z#5Ck1^f|mvMO%Co2cgaLUQgIYj!kt6kMr!U;{_I^be$#&eDVd>!1%ZKKM#e80`>(T z?vXG@SgB6NB`R3;Ko%lAS7eR)XXTR+FE#UTx-G%Ia@$w4%Z@Pmpj?U_gQjNb9Ruh` zD9U4iqDeOTZK*I9zrZbRwzqEUS?bmY%iM|@j+WoN^-4Az0)ulCgWR}=iJ3I6X!6a3d^ zloszReDDyi=p3I$;a^Dqm*Ata{;dk3s z*bn!{rE5I1OVHAVt|y?72fgmJsGt`|m6CwHI*nGu_-=-6(33LBd@b9q(x!938$iXa zLDfLcfrADhQqyJp{FIet^{ODtY8P+*(-}4vYVS4FX1tW5#DmXMLd*QxR3H3WWy3x6 z?ixolmb^G9uBT?h1Pv?qut9}YREni7?J`c)^aA+iW$pF5Z>}uzJp?6`7i%4HcH)#z#ynq_WgW$E)mFnj% z!P_*OxICw-kFf+AgD>8ZK8Elc$N!&bYuguCuuTjHOf&B)_d`m5n6@SWAuJ3{fmHAr z8c%1P2y1ICPk+26mAUVC`Xof>N)oH=IH9<()^aSE74L#=t*S4y_p7de}b$I*r?cnslGU&4VNp=NLI$o^q9!>s5SHwfURq5s~+M zI$RL~;v;oKy@s!P6W14t)ObXbc}tx&Ag4DtmF=(8Mx%QIUoHoTPUoGS;tspo+N+ZOhn>_PzZc=8mk8 zutl{3FhuQa6Np1aNY~(`6n$*Nif#n!St%)=SlVQILKnmpRr&ijP~$0%Q*@)BWf+2Y zb5}rZpMQqD=aA$--01SiXVKwQanC@y8-jId-|r=zelK54l!>XzTOpMjup}o9_eXu= zIm^%-Z9rvmjQs>AduNK<2-<#Ideeyf2>g*3=@u7XJCb#^Ftty{^*!1P(0EH#*wqVg zbQ2pkr6a#H9MY+HQN@WTv-0T}x7@xev3G2V6lJ_2ms8EUdU0sKt)K`C)xi`-ICbA- z1|<@oDQYYXqMoJqHC^Yz3Zvr}N`q(%%Wx7a7(hu|+rjewpE-`fn1SH7-go13vGc~a zc-~E&W(P0-#8bVNiLi6)cy&n$>(w+mTy~S>3r)WmqPaF;rM^mf3#lfn;_i?uICzQP zfT)Gc%x=pA3y=*ozOJLL&+6`P-a7lQngRR=pyIz5r~mK&OLj->Mk=0Ab1;OT@0wN- z*SC&5Tv!3E;aYzQt|zX%{t|Q;ko+b1!OAl$7ZG#djCV)LQ^D?o8FSZlT^bZl?EzDqaQmsPp#iV{EbRUX-c+VbG6D z2PWdgkoK#k51b8L&G%UIeS^vtqL>uS0>@5&oeLfcPn_Y!fa5NkxR{6(EBuP#=Pnfy ztTqq_0$1wNRsw`=Jllp;xVJg(bPk*@MC_M9s?3q+qv9kMDA>bkLE|lJBn;Dk)vwkz^<(s76 zKW=rKSy+22`kI%;S_LS4!{K_U@vVW!0S41y;iTT&Ix+ihoRS#5wm2D1t=4K2lCl0#nc=JQKrN6w)=u(pDL!x!TN3>`Xfv$jwLL@# zz=YvTD+l$EVNGV|M5Xmo&QeyaaktT#c49x0+R=d{U&f;otv3L4Pz|W<%Ow%xrb}!A zTC`9xz^CjPqVPB@89cIIrP*Y;>+jF}KH=qjQP%?5qW9JF=|AxXPaBZA0;bUddKCeT zMWYV5!DZi_(hKm?AGg!;b}>=lLHs$&?zqZoH4qq`YH0&~2fNAh0RmgEd!bXzI=bG) zecXmls&9-Oo`E)}KO>C3M7jZ!7{=Zde@Gh*k<=Xe}JaD#T>%h(d zz|IX@9GJrphS13tM4taPOkR0l8Rbs+sA;9zf5LAaKxoMOs6lgNUvJSpG(H+YnaeOF zyglrM+@{1dy&!&LEh2$C z6i?aqyVOJdTB{o8yAZi#FFvGiX)bwOa`2bH;pWU- zr?og7ySE#3Y86&4GqY8Mjnj>-rsM_(1ZpI-Rl4JfM(ayXa3Ei;vy*wuilp|8A zX}(sva;Al*ms(i};P6Xg-)N<0JfBCsBV4=uayHaBqUSmjw`3(~?d9o!tilUI&yrdW zM6~^ZL7RdPvUMMlC7N|4*6D^tH=VoC_+=gTtcX|z*4|;J-U44OgO*CwS33)faATS(D7zeDY2W)g)a<&Vf z?}ukxxqqCD0SlU zSZEnT>MHdwqc7E~#Z&KrW z^U(dO8iD(EF8G%qWFKEc6b5pe8II8WC1_e!j=+}0*k8>R|IWTK-@2X&rT$CMbtWD$ zp-B0yd?PVp(Ti2(9MLa&Mj8*hu@W#+PY1j>N%`t-@e*a$p%9Iy19 ze}BW7nrp9~+Pg{HEm7@0L z-4?+V#<;$QZ|avs-}D?&`?&>oztTz5brcv&-I;7Y-J7b(@qVIa`@6I}dk%!{PMJ27UVrV*&02y6lD>G5E)mX$_r zPJuQ;r#k?Wkq(gq=drVd(2gVT>q46X(f%C!)y2IroAlV;<}Z?oQNSZPYjZL0R={Tz zC!iteORn5|+IOeVIFLNd6TZYG9hv_9QjZ`lo*!~QYUch7W7?NgsQw}2p-hH{}+Chi)YS zEE+@J%GFY0f1;iK62PQMF7L4d&P@_)6hHUNsR2B?dovsss~@QQ zT2HShX$2%@%DFCuwm$i(#Fq|^P5}PI$P$=TTfc&+#bj3N>~QISWYXqJsCjD7zB{8j zQThV2Rd=(j4M+=ls7Mtej_rOh*l8BnixDNEZ;{z*JTR#z6WvrcL>oD+E53$@lmMH% z{LzJ<&&RYcplT=8yXi6{IY^}r^&BU~0w!ILK^guj-<9gVu;lRX%l9`+$n(mRBACeS zM9*hK$UKc;C{-%XqUhIHZ({d+()#=(Wo_qdk70Y-ji>2;Q0qO~4$}6OD znHTQI?6=xmo9u-Vu`31%jNRWau4Z-cBz)NJIF@{!oc7Sob+s^KCCd3CNkkM~)gg#h zmb!ktkuUYz$MWPIQD!>Z86GgNfKJMq1m7f?P?<5sKl3X#fW%N?h_!&Dn-9PZ^FT8Hqn4R zSFP(vMa=pbvaokSn1icb1bd%t$x*)-u5`fU%!jb!LyHG)odPgOf}Q-<(A3uJW?A7{+@E6ln1v@;mt}NRU^1rFH1u4*_9-rJGhZU zBD+oPF*_Lkm%jv(;L>i*jb#xu;!)xDaDIUFk<#xoVEYlTNI^NzO1YzUys`m#>;`QW zCq;^g{6PIhvK3m+g=A@2CM0Ql4j!F;qoZ=7d{KKtcQJRCx%zM#jueaNbSR$oVmM>O z^r?@!d%kJ6q)X=?l;80WHJ)-F#O?%}e%4BLsk7S4Y4u2r{+c%L!p2Q-*XBr(@+m43 z;q^M>jf}1oM2Ax9-{n0OB&xl=H+~4b-evtX9X88v8kE73<9O~U4t0>JvpBjbHAn}>p zqpsWpPRn}1qOvLPc<;`2`XRfeg$tSFAscDVSCpTsV_?NCTTj-tJj!n!x12u1CxCAC zl3%esslCE1B6E*wR4UH;N)BPY5uGI}(_P7X3~~x#!EZiNr4>vwuviO;hA_(I%&qex z&oYjeGR9Sy8;_Os*AZF5{=}8o4OCT~L?Vl0`d~96lj>TK!-mF_FPZ(%bBoAHW9gpz%rUFo(U z!XP;*0vOP85O*baXfC#THdK^@k21N{yU@B2xRoacr^*kdlTBVm3xwpQa=m_$v3IK8 zJ=T%jr}XVC<>@e&R^vD33{Uw;!aeOER%Q6gOZ%pLBxI%UG$?j`0fR|QUQ}ZxW2!6k z;)+7Yo4yAZe7`^jl!+ltHEoGa0THY~3SDimIMXZrlSWOl>81mdU|JV8@M(L#I3!Rp4UiEH$Iu~a$5^kSoAj4U9rP-A4 z_K$)9O-WEu!9z`M!&c-I1KngdX~8PSldep84f+D~&6&6BTm;QTWpqvGgQGpG$M+jC zYG?E>M5Mnms{Fg*nH#y#wCwtWm9S^DQ(FE@7!cypk}Y`u>Z74sIrZ^fPQ>zr8~I}} zQHvPb({Yd;u~E>~&!iy%E_!R={o@l8A}e$dYM;nE)N9 zOgQ0A9<1iOF?(2Uydp6*Mp+Z_h9q8F;vIh*r^tNfip;udOJ?M-U3h1B5_}hzb8U>?^*p`Cvc@Vu98zRC zpK4fHUzJsmVH9OLzAN|INA&%@M;$0fL!U<*Njj9a-NP*l_&s;cW;^UK{hfFkKh*|( z*bBC>3K3ncB!@0=E`gsk(N7He&(EjQI{}UT_6O}FuvmL9ug0Ds3%U7D9*1Sd{k$0B zBAZP#Q$PlIf`3RRMX^I)*RW=RP-xKjx=)&(tNkY)JQHIy`#Vv~ugM$E=4tnM{I>%RyNX|(LxX}|qy5wG^~VbFGLEpz zuA8x;$_|zNm$sAqXCBQxpP^ZS%@0sc+9bu&nS{CTJlK1E`V3ssNX|d#IVnEhS=-B$ zt&Dc>l@pPwny;zznmyIqrgQR2#Vz$#+Fk+VYiQaIdEw0tFE|j{R4B)%t&1P&81o!O zT&gWs{dU?8ds4nf^BnDH-h6_45yiF}XBPqge}QOMqg1#eZnnZ@{t_&e-;MZ1p6RA_ zS&h4>5)}O8JMQrWdMv z{evCk9<_nfnf1$5Klz3DNJ4+_tUfa)r7*XN;Agz74DG!+L-AklxtqOvSR%)pb)k?C zZp+7W45foc7siJa`S5D<4Di)^xzQ~(sD>wXyUy1il0z@7}p*PA^(~wy3XNWM0FkiPNq&{9NrX62-6lwp-}dt!(f}> zIeE%N2XY{XQ@2TpT|7S%UsfG2W)GShkPe3u9_9d@#x7LS@y^32v;tiT<+dAIkCUlm zpj(i%74?+h%(();%( z3Z{S*a%I2{S8rgT2QEX(ec72EZb#;e6pM>zcG2t)o@W@R5w%y~yDfyTLgybEsep0( z0{+F7q0O~hy!nmO@7yu z19s%=6Btdo_Few`bxw;?lp`6by421eEPJik0_ez>{Yp>{Hs zaG61(eYJIS3tNsu!e^G_)`9)x-By7Sq}`}jZACs_0;r;*;DGVu2Tw{3($gHzqL+^W zUG0qKW5zHMwI~@H5(QuCUp>2#*n-q^_tOGUY@D)Jw+uNI;-GC=bTmJ7Fvw_k1u`rn zTjO}f%}z|RSZx)B)Ze^1(|%M{g(}e!aA8B!DsI$hV071y!VM9}cA6tL?&6!O#f}!J zzH_uT7X-4@Kz~O6+k-2EqJoGm%cu~K6^QZ7kjf+pYw_Z+lOmzL)OnPgzjA0;W5 zX!;n-Bu1A~cBx!>Y(Ls03#2eXG*T>jWds%>POlqtk0qXH`%91`zH4yJAlZ71hIOLz z?*hCVm*`&mYBgH9`pO_(ZQdNc$m)c<=_fgy2B%=`lP!Ke4X7@!z7DT+@fxwm77fV8 zNQ@N|Hzp;F#2tyaj8x;fuha$8anQA4gQa>!FVYbZvD$@*WPCn#wrp6&o*V4ljPe%Q zK~Sz+%B4rHsK~*UN4S11b6`2vYxstrcD?3gP*#)2Zo+58QuoTwb9kRZ#G^M(Tw8Y2 zPerByP&M085zn(p8!{xahfZn#b$gpYiiIaKsdG2LI-PlrE-aSfSX8`Oaz~`;BXr4S z;Bj>eqqT99@WY4C@y?QL3^i7~35HZbIZVY0@GJG>ZVT8iw_yX~4DbP)z4u1rkE|g6 zAxl$54E=qD_JOF=qE3%83XO0&ZJ`sA{L5&ARxiquu1p*JvcFIFlCo(gG$64woL2h zJ4Chl$%KYK&#{&Jh{fEtGQ*7B!>QStO%S>px#Y!COe3dcKMzw-t@XF_zXbfxVLlx=!60$>`>EQg z$wv{SE23nQi#d7bFdwa)JMhp_R^gInQRpya(1m?OxHQdF5TTIKhAX+gC#^Me7NBiecAkmv0Pc5xysr2hMV7U#^sUsHMHa77_2&)x-X=Ks zmmop;c`sOYPj%S46FqQG$T6?zl#|l(!vJ7HE&}dJ6H@B$q@FBX#;Y1uwgG$fhf5wm z@JT#H+!A-+J(!HXP5SV32Vsz~5KX~ofeyx*@3Lm3UK@71{UwM|_PgFVzunAm+8trK zHFS2xCSiYt`37a-!#b{iiLBpl&Aoe`i@nt^3z;~&(F6|#e#sAFOu_ENs0xsFF9_hI z;c*WBA?_Vx1s-KHUg5v4oOzY*E_lVEf4_e0ge(n;am+oUF$^7V+^yuAkWegMy+(4u z8;LOp?vW$LstjtLtZ7kR@qZ&FH9o>%7tY$8Zxt~i$u zFf!#dnRe;RKUZ#b`s`UBw#8wMFk9Y8Lb-k3aZ6gBHFKOnh)eMwAs~OOcrvtFpnX-D?@Aw}AGvFo*s1Q*!uWcUa@~HYj>Tq! zME?7woh4|x%LxW1_G=qxUnTYy5q~#(JG+_S%_%L#s>?W1mo@wk83Bd*yYL%3VwG;# z@}0yrH{`Y7;SMo(dcN9POQ*(5JxAlcWGjaa()mC<7H(_XBq6;q(`-B}g8LvsrRJ>~ zeuUKqS=3+HMx3^Mr@es%e*8-?ex*G7(K0~lSd@8d2a}?L>DKQJ((ebG6lTXdx0#ET zR9loWB?cNF3v$nrseSQJI{vw@Zo%PPx05t_46x1s7Iv)b4p_uJM*rHX3EeFA)ZnT7 zT<-ur;NMcrD0H#t^`eCUx5o}zJ~^4KS_?n_<FN|1)!gfvqIGL@4VSa=8+ z9-1E(skSQ}iq9)&i{=3Oy1SKj0-XTVcCPGEe`_@Y+tXLCurJqF~wy;?r zU-{3)uI5$+h>x+rHsk$~)YsejkKyJDZ`vUoA_mj^LF}!IHYH9jq#Wzc>Oa@VLpss9 zS{9wb2R?Pn)wK_#ce*}=R@zS9iF!wQB^X6(nM=J9&3Df-sQInd!=F;#^!Jhwbo3r? zU*}ukEi!|oh3LdIq|$s!c$eL8i!t-aHliBxr4QK3>z$W6yZpCjG&G4vFI_g&=W^$# zV5VdY06#{?>h|mGW($mZ;K7>E0Rfu)PN!S;yu`%Q!3$4O;iIN8u&>(ynNk4VK*R3y z{#cuuCriOpL1s&+zPmq<8WOSkZc$-zu*lC5FO9Adr2Gq$M=hF(Kz09RjO4Qy~4lc$n$gu~0tRyu_xZ#V*>{`phpL zWT$2P)-Sfe^3L08Dx-zMkF>5+p9o*_nYn}ntqkT7zI>zc1$_+YKNq^nM{VH?%Onwg zEK&M-Q=yU}cFa&F#NRoiOeU=>D32I#~!#}Wl zf4d#IQes$sYM%uW6!`WZ8}=R+p+3KJ?dRcY_o7FGUTHdnN-dL z4NpGLl*Z}c7*K`$+xu?W8wcp|ASiz~X~M0gOuDt&CvbDo5;nJNg% z<+;Lc?5nnOoa8yI0D&Prvc)xkUCwiVg1o;4M0d zm>d2Dz*R#6&otW4tb%TPdwBKSU6Iyvs-XnR@wZOk+HH-4-`tYF<@`}y)dg_ZyLMv? z_d?FB^;m+$Fd^_RYG?c&E!-xZ#%x-H(o;{@FOioP8N^~47KK~{<()}cr%TDW>C9*D z@3d({9R(9qxSWXfkxo9fNh1vog1aY6*c$vIodY!OToD1h^$m4W#6U>NHFWmm5jOFRNRzJu~yIs6oSHzC1De4wD=U`;@RJAu@ zK+cYuXKBT4>{x`BEEvm#8sUa9fo^T?DYm%1E#Vi?CB=2pJa8$c4V{Ch%6Z}o;XkQf zKdArhU=yJ8XWe4s4~})RsN{YTS9)o27*<(o-YaDD%kO~du%{=r=@&}Hu-=b7_um5) zUf|ux9utPe#D=@Fxpm(-hjnKwy}xW(4o?WVcjDD~O`CTuIGHGN^_k9$Q)P##amD1AS+xA`b?c)YC)XZ8MzdbEFuQ_myU z=v2lgoYZ-u_Fb6B}iX|MU@Of)_@cFq~AUjubbSO z&F-QTN%-#DxBtSJGZ#@jJe#noqugS`Du?8%n#TP!^KS+QaL-$Y+jr#s%1=}5m)5ca zg7Y%W#8>+skvh30sA-^r2p-H+VZIR{aZl9{0myfL1_}lUlpq&7Kr{6m_ zbcek>p-oTFwXo#afv$xtZ))gdU;Xmo_!_u$xIJn|G*?AZeiEoxaGcpMHXC`h>~KF;PDr%G00rJwa=`Ob=}^=JblVbF1Nlvg(bB4y$_AgtB0lzA2I%n zWG?{+73vWVf<$CQyEMV7>TjtxLxx~_bawKroF|aGv?;N}*Dh|?I&fvL-3aV0hooH; zrxxE^X~ z&h3C7Mm@>uv-43{ze}8`Q@_81G>5BGKURzIs@V^9eD@S-pp!VEKzAnV@ z*JMM|ogv)Mhs&saJAHY6aP|`kPe;Q)vQbf?Y6LAxx8n#(BwA465n+BMas39eS)~+d zc*EMoFyDIv(#pp-gwLWgEAQn8Tz~bZx)t>0tiw=wJ<7NvMjB>pT$3#bVPh`oaY%hO zPqt#eFwA!1zav}=Tzm15fs1t4_mXU@{_;)mi~)<$Fe_*6P&V1aU@obeniRt8=}&)Z zjy@{N{3t`sjc}FTV4_lMKc~Lxf%xUX#}BHTOUaFmgTw9Tf}9pVLiqoi11#Q;0w!!~aUDDcIo-wW+lb3@b)>75H~7l?_0xtIdZ~5p6d6 zCM%vojA7j*=1bY)?jr`x3|v~{vRAPvj~oCXQO97C1tB`}4oi$saR$>ScVKq<4(K}e zC0=1?ij{KqCN4rBFgu>&qnnU9N&~jS{N?nS*^A)L(syw1FmOtY+yQNRxFFRa9tuh0m;9Rvh$dE3@!DqOghB-3?%j1b2)d z0f=B>i;UgYw~YT3pVSAuCm#vG_OFvsr%GUnLkBy#S7}vvbavZJgXChRRfxRQtZfXL zty!WSVPTsLn-INCx#mo+<_RYS6A&#!7>sT?q<8D>!Ncx5b?TshfFJILRSM_3#Ul-x z%=u}~`Gu#8E`lXFPG?(NEBi}c(iLLW`~|E`jk35gG!&pn&BGze-ISr}LHJ7}{^IP% z-pb}(*dAlm-dl3ZmA5}a&Mt)hheN7!+p8jSr+YEEY_^+Imsb=G1w8vVdQv8r%l z8YB%Kj$57Il?M1aL}|1vR-F!GJx=F^*nAr)nf$*rrBTLArhm(ym6|TP4e_797k2Wc z`;zsAWWdqM=S5zKYIk!_Y%rr0D%_IW(nxq@nb`8~1?SR)G^3%*Z*{%G*}cKKYD| zYv(B;ghJ-GvA zTpnssdtvcG;QZ;{bXu72=U_vl<(BG$3%9RjFVbXc26jo`N*({X3b*Ty?FpPp77cqD z{Koey#Vta$x9RoAgb_-tI=^L$v!dQUPy$%!#1mYm6#4q&ZjSUH`3A@2cHRDbf9|E< z?@#lmmk0I%OAETuHECphZ@Egz%RlWaJgVOL!P?$!>rO<<74?f-f${g=3ecMQ`<%yR z@_uVpHesn?@S^-Z53bz|shYn8O5|yu`3gj+YOYTe9-(e)?J9pDoW8v3_FH`Z_NGVe zlHl3gN@H!#**tzCBY`K}v$lAJ+5s%gLPJ=e?VMa+*FJF}vQ5RYM)~ft+LCungNzfl{Sqe?E?P#M3j|^lFII{}h<2j^?2IvY6;_)%GL69D*kvbFndhz!worWVG zF%JW?T^;USAHpABoiv&J+j1n*ji zTvAV`*F9s_3_0P2c6Z%e2Zw&E0ou0G&H)e1d>Hn=cXzM`AL(P=c1N-B)!04a*tlBq zASiEiZ`Zeh6~09vY)rPsw~-p zSSH8K>BZ^H8qHMEk-S64^*45nC8VN%3GxYZxi5~D5&e7jJH0j3m23@iJekb7kP~FA zM_&M+wb8^-xfIJUB4*=PggnJHu}|a)JM_<)uW_NBv-h3`xbzVA^myfdIL?LGStOA= zA0#4f(;I_QsR{+NRv$SV_>)N$Lg5xSV&gQ6%xRJFVD9*l^Xf(Ls$@Y{VIT^K@wfD< zzZiVgIAZ;UW8Cf5oj2+%(J$GO?|=F6AIOpZFDQb5Q~zOSPa60>T`_TzeIv<-Ut8H; zb!h*JmV%~cQoo#+-yJU=dXyHoEc21n&70H;Lnk35e*!H|1r_8;3#70nX>jN_UH5TA zign6-wNv9}q84&dIJ!oM9`xqi$-3T`)|%KbJG0@^*0Dn>B?#|=I3E(BKl1^mFhe_i z#IiEw_ikBneP07pc?3$4=mYC*URjIQEF~{GqLz~Bm9j;A(vc=K=PoJ_Hx2w1O}|0} zeEGHS38JWXK^UQSN#Z#fOcFLro2(8yAVjh5NbmMzw1qwXOby|Nk0z_W6GT=gj^I}- zbD*)&c7nb@ds3l`S%-kH3vHu;^CAj#Yz2M^%hMA48(eJgw$aIQ+Zqurf|H4D4#z%? z$Z*}*F4MAVB}a0KsIOeoDc|?F-aXqp=5)B$+@E(BW~moZUOPF*(e%H1GXoRbuDdLW z{$n|~9^ot+Q^N`HvO4b`&$hEV`;+uOqtMR9+r;G37@B$8R}C#hL-!o05zZYWRF-do zOl#rR^~06xXI$L2X4iNv8Mr6?!&5TgHsC0K5uhp*^CJeSL+~7c-~H$2e?UM$1n+XY z&jxuZTKTFuee!d}LTEzBHQKg2`*tw$0p)DP{pVa1^5&&cCnpE5nt!?Q|JZ#n3!CeH zLGYM>u-B1*^exLXV@*+swJa%)C1E$C2U9FM@!NNbe+TgsME&x38n7#1A0K|?<@r2q z>M-pWF|bziYisD_yV9?p1c@{w=ndf^OXefzfAocDrgcmDD6*-hm3*__J>3s~>RC}V zIneeXFIsw$24Rs!BTrNILz>{lPvReQFDxup+Oq{W!!RTh&wu=0(aAr8+v@u@RVT|n zw~@a${cQPjQ-W$zP=M^j&%!#o#ftjBSSWQg(N9`z5{z=8H+wr$-1uTvDElw)A zzIwg9)@p!B@6cChCGxkCWLH-3>&U;|W(I&(Y+%>I?0Y-nUCrxXRT8vz$Co{nMR3_P+ zS5jZ4E2f0W$KAyp5J?;r_g#3^V@($ynsL`(!Rxbrj+7w#xE z-wnb`Sss*f1MzBgs_ zo~uE_Sw7zGGCTZA`D~t=j8o+ywS;;z#P#`$>IzPmAB?kzY4U}x*?K#s(C1wP0g^Rt zU=w;TZgCoT3dGK6d&QXI6PAned3$hz-^$a6jorb(+d5!sUHyP-RoK+fBXEI zNH)R8>K%1W+f<{^{J5M>rgOi|Y5jsikdvIi!IM!?kkV&)Y*N!mE`A0m*jMfdg;&L} zOy^j4D}ZBIL@w%WJ@v{~1|!6A%+9gpOR|J%1H;K`fa-Odj4TO>6@ZDZsil=)1{KK8 z)5J9`PTvsrs*H0?{k7k^t_hb7o6`r8kW}p(dO2q3ORmZ#PMc4q5EX9um||v~xt-hO zVZVY+FI>AUYbrb__Rasq8kJJMyg-m^4L$&CFrnzWF3&s<>aLGpUb2aY-5KVVJrx%AR0u5Lkoxco zY0SMq)8eq`;hzV7f%(QH>t-5%-{f0ZCs|aowY#svHF^KYOJ6S6Dd4A-U*Rme_7Ba} zHzv3?iW!GX27?sHI@iw1Y(V#&Hv4GFyd6n|=c;MrJarnE*V)A_e-}8H$g_!jQjRd1 zG5OZ_jNy^yCrd5P=X`(gwb=h0KD6LSqqv>rFV#hc#6|9HAO9r~-f%F?oM8wDeq>n= ze~x$uXQI^oE%U4+C7ES8w27d|-~NG4)_-H{y`q|GqiE64q$pLuf}sX!0-}OS47~=W z1OzDoX;K5C1QZCpD@CNJNKYtY=!7OgdR00B0z|3=0+HVNZ_YRm_uO&Dx#Q$vCr^3U zWAAUSHP>8o`hIBHKF%H=h;EDpPUxw!F{5Z_M?%o=vwONF<&hpJ{YfLE=VpEwp>?B~ zKtECusS;kiF77Ta&gh$`7T`Zb0Vmo1t=y=#_H0+EfqSW}oYbj|mKz!r@Uz|4bu}1- zw4*}ZQH4ngy}|X<*VM$T(!5JsIs%2%0+WNOMlBPK5D%-SAc)nJ$MkG7%06f=34WL2F2#!GwK$ptkd+z}I z%~9>rHMJ!G!=NF`8%d1JxbgN0;~o=}V6IMLv(A`qze@nAebu8PwcwYsgkuH6Y3(+Z zq#H=Z|J%1Uetih~o_%s&<9Z4S9b&R3c$-v`;mLo~M1+ePYs^$aIiGl2Pc%iewETsA z=a}gk)llF5TNJh?<2?B}uR^f~Bx6V5@&C6^(VN)4e_DIBS`1sl!N&zlM6db_`wwrb zUMAJ$9-Gylw7%kWP?HpM`CHu#U@pqrAk|?Xf6&c9m^P8$wvOiJvqi^IPgGM+ixmNT z7JOO6Z0l3~e~OD6#IT~&wPS1D{4iKAc=#nkn2yvSygs^b;b5xInWMl^l*uJcEe=rAT8MckJ9~rM%Yxs@?Ud9$@ljIHm1K{#oV^Sk~4B)8HHPRtO{rGsj zfx2#7vx`pV-V#m?M^j3ynVgQ33h^QS*vAuUMF-b@9bAN&;?K0%^hQwkl;gxk>+C1l zE;aG^V!xf?D(il{n&tT7Rt%xqZ>^i<{lrXU>5+H1?vEtv>OuC%t6(bbV6SXPk8BF5 zNh>fgLNNJ}K|<=YcJeuerEB9tg&qTvZ{MxGF6OS%GXdn}=xyvw#N-y&wkG7&G8UCc zBiNuYF28^#Nf3Jk(TNJjq3(;x?;u7$j|DaC&N|(Qy>4;W_ugo?{ph!Z>Uc|eW4c?S zk7}3$(7NJL#i2%PX#QENSmu%mGe4t{I5;fj%_dpJ_?hK(%w=nY55!N@x0c z$;5WKLebD-m^(x`h|LEbuE%N`BCH~etWta$#QlTbYGR7tI&UKCV`XzKW0HHP+!an) zhN~QvpEowSBrS(bz#RXu-g##H^N1X`)Fpwh54oIcLL!r-FjapTv(rK=l!>-DVzD%$ zgN!KA7tT(zZC(*i5{>g{oSD~YvhH;|;uhi%ZtR0X$k9u;an9PN2|S^@x|1k@HN=>V zPBkVu_(`zkf?ZqDmBY6Em#=Idlgl)IQa;0X+AZW~W#8;uc;Ss|E0z8_F9ut{5R*|G zLT*qqFgzl~fu^E&n9jT6^&{is%q**If$8%7mi~Yt`!&Iro zPnDJX?nc73<`^TJ%r2XV_8SoyTShVtS7w+$R0vQg9=F@KpUDx+{VvWnjEohVIy9>8 z5eoF34Lh>9CnVH^wH{tlS$b!+uKxeVO7r~RC7sI%&kYW~h>4@R{uMiS{TD5}c@+}x zX?`U0c{>uqkoaKbVk<}LVEyjV(FGH!sNAWF_lTxwT_3sppidJ&++`O@(ZPxD#AbtOPK3Ci~1bSYps_!h1J12^6zzc#Uj^CW5g`YfxBxQ4* zW${}h7n!>Xwq3wA17o&9nlAg9uaA14{uWD?yM6mp$(`q0UCO&xmaPe1-SGPcQH`cs zcOFNGKpVSk7bsM|R87sH;6dAe zFKcqGOdx7VpLtc?yHIsPeBLADA5Ed5Xf&jX|LN}d2gfK zaQc*U6e(?{CWB@QiW`F9Met`QiGKqongczM$3p6cvBM@uC>lXo*yJ7Z3rrll&y7G)|cRUX|6u)-e!t1ODEP<<0B;x-AFzku0_zZ=zHLFV$pK)}Z&y&=s za9UAZR{r&4?OteB0x&5`qAHev;5df-2kPh;rCuUaz=Rkb=Seh>` zLxo4D&3{rz14$&bENEGOW6Xp1rLw?y^eu(i+0*E1Q?lqtR@ckkrDVDS!Z67&e7Wh~ zqy(<6{KLta;_g{r@3q7X2^=vps=V)we9^)bo8cKpOoWtQt2W7w>m^$?)NJhGO5Y&twfUsZh~SAUcB zD9eL7%k2l<8>+&vdq(b$#Gv-rIesQ~qngeYdt!)o?X%*#PV51vVKnE*k86qrjjL%0 z5k?Ly4`(j$fH+1)(EyJyG`t5oFAW7^~!UJP`I zF|3kgo1MGfaFo=JrwaX>Zo z_N{O*#b(QMqSL!&`i4#HosBLVbBWt)Sb~nI(Wfzi&SoDk+_Ij}deGz`*Me{Ge$Dw4 z4!ceXnJos&X0_czhWF373TK04HM}JUj;y&6zhg&p7LG=8T9zK=Ik|1d#dpiWKrXm_ zn>XqoXw3juT}vW9*Gx%oGs~5(CaJu~GpH$vWK;4CS~rGWP4-nh2%9pmHonz1lloo3nQGJI<=*VaLj^P|pIoTpv%j~DX24VQ+b=iS;Kh8d3> zzUuDtduJt1ESUS7w-;Dn$TS=U8I+@KUD=8Rsn^0~k zuRRpB8#P_;Dp6~#|qn;ZDaU6GOCqmq-FzU-lJO<7wC*1 zd=bVr>{_c9=UF?w@j>iooi$?Us{8*OBuu~KxFm_a|33x^N*yl*XoCbzSEymT5sS->)vA}yIUX3by^vNozuo=p-J49dM~@32KQ6eumW&Wxr4B^)}XN49e!} zfN%X=IE(t>F8(LO410)8us$Uu8xy!LlbR|XJ3og`1F@HM$W-Wy168a7Lu@$bf|2hG zt@dmFoEm(Y`L|l1`n`gPF1y$Njzg)BAx>2&{mOXd`V5!isx{37eZvL%AZCZ>Gm7etB&XVguxWFfMSv^o<`WIiav_(;S zz8>Rgomu&(BS-qS?+5SW&pxQ*?lRUFe$xCE$4PfhMuSEAy4KbTL9Pz|Y59GnHxUDG#dlxrO@v9&Nt79Jxn>Yb9YDpyGNF&`y%!S`=O+5OlcI5<@kkCzb zzl-*rZ(S{3W|yaTkEP-0S8isoUn-8KlJ}|4vq(YtLYgSi^ckdKyJIsvhHd?5}+CEvLLvgOw}} zS3b@r3n{Dx25;?^SMa8$$~+I(_5Mp3-1iu*3lPn|b87{8zT)-x?!JM2VW#`7r{em~ zdW=`-hf;y1j{0+-fk7&!jo`8~38brNAIA z5lZ#N`g+L|p%`sx5}}S(o^?~FsO0rV(XUkoT@d87T3~(7CnoUOV=IIs=xoJ2^&0mWv){cyhd7{Z8wu zg^R#*C7j)B{X>XyZQ1!3fej*I3B20nRll(=NLJuoOGY#E2AF;Px1r|AKwfXAP)B{W zD+bxOQ)A1!%qXpv*;0~X-(X+8H!j+l07Rd=hE{x7E0@Lp*`KHlsr*u5lUFA?vt%)q z+-5dCvh^}MKaF0`@~}~TI@lVk{V&_5SAqOQ@o&2NNI+_BVipj|1@;V9?E^1qCOmrn zT2PAT*Ym0w?15y)Q#$X1zc&oT$3Z2YCClRa8m%Nfm1c=%?iWTG4wWe# zNw%gjW|()zLBs+KigPvKn5pcqQkF{>_=J$s%W+TR*T~d&9&Z48xLGZ}Y1((#_GSXV z9C(9iYLVKq`ytks#B(dN9OS4F`4n=~(8#rVD!(nqEgtukt>;#PIBuFqFnE#|`F3-e z%D#2CU#3B$Ze^nI`-;Pd;RiL8SUHiwF`@5N|>D#k(dmehXV}DpS zuxX{u%Kz{hi1kqj!g|U6ExWAHp|8olcm&D-ayD@N_7<#955kE|7b%}MH2$;oto!3|NDKjhj$Tj^8zwzS7&oqxFtG`{A?vG^>8eczt93=;iGxhFD={LGpVZ6YTtL^?U z(LVp)(pw>XCYFYP!~24|)cj~z>bGzI#{r1N0xq@0YFB_tJZ;G#+a;8j?20H4k2K;` zT+{%;A)2Ip4M{FrtLZ^uBi{J+q{&>P4f(rP~*nWp@AP-GPYf>pO}RkpjslJ9DU8q z=H2~T2S?tO0=ZJA0YYUMU@}CdX!Dq${qv6mI!Z8aXP05@GReiRB6!p~LdLB?-Rz-- zTEOLA?@$2kGX#*N8=|Xvce}En7W2HAi!H!t4mp0v9^4M!4ZRMl8{Lvc1xyaH0lZP6 zAa>f?R|M=tA7+*qZjALOAtL>LC@ByOo?dlthG%FxA zYn7Wkx)JAQ!(Dq>oWRi@XnYjlM>BSB zB&X*3?uLz=mb^a9m_C*Q%WKXl7PC6phXjtjxCp=9?v#+rlMm6KQa?iXtlAe z+BDGw#qH%uBBUA6ky&RvPb=tlukRgs?3)c&$=D)WqDF95O+)00CJ^E{#CfP&q|8=} zKA7pzaU7dIB~1pE8t4bC33faftUI~Pbt8zsuO%GeW|zliXNTa@X*EIwxMt7QZgu`+ zn7Zp~K4Z;lskS$LE}BIp;LpJRd_JT9m^X2!DXX7aM=#9s$-oka;zrDlKbZgo=P!Zt z)|eNhHyqXKF8WAF;?*vWB7!_}K+lDwubVfTjH)m8ou)O5S0B~X5tZMj>ONbu=pwu6 zNnZ!H(i-WkI+zZuMTv6U7?j#VKYI%e{8;~8X&$OKHJuD?_SFXktzoJ!ReCmczb?l9 zkA1KXaiJ?l&jg#4_l^%X!B0hdZ44}2Tfsh(bzSzHmuHd_F$^uX;rWn2G)fxHbz!mG zVE^*5XW|T(dS!X`w8i>prW5_Ua`$*qe6>e&QuwAIE;BukF_UOw9f}?;I_lVVZjx8?546FBA84xy!sF z%2QTAl|I&Z()^s-aeP`zi(q-VWJY|A4_Hi#KI@l`c;1VMV!IA|4yvNI=goXBkr<<( z4A(CmOu0ElIJvt%yZ&klX7ZA|!Ht<1K&l|A1Woh{K05n?=SCF-`N#vd#Jh$h=B|Z* zdXS{xP*J(AtF+bR`Xw{Ykg3N)hH3k0*`?3=rWrS0Epu$e*_z=?Csbeh;P?kKGisA2 zLl!)y!jf&OEosLe6wfDbX^rz_{_4pJCE@BI1_7-}uR6n0#ZTpBP@%;Mzn~z8hm4c! z{NUD4+mxX*dp6hyJaKC8DCs!5>5LSmS%xDzA5ZyI+aiYK`8|GI^DC__B(uit6R}nE zz@CpAgsVKnfLoK79U3W?FPCa4Zxv=~y_dBI%rMT?KgX{5Sk5L{$&NYLU2w4s&#!JP zkcu5@uF3f*|9WB4^5C5iO`Up@{)e2subp{lfSUr*LO%paQsh_V7IRl}*bE4_%6aQ# zCozwl_CB9x0_*PHaZWi)A)dZte`he=p)ODs^Z|e0`AtFnY7jm%(G3dAGx@(H>2Tlo z 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�u@0izr5rsj}6q5PFncr^kFeaVxb)an!*%J;X`b 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 z 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 z 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 z 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Ⓢah^938D(5esF2f8 zkOXz|R7lPFx@*Vfp+G$ilCnd0jK=s%%dLlRj<-yh=2RNk(CF%#AK^+W73m8;>FLXH z`3@~}`4t|!4<%o6)JJQ20~Gu8`y&lVZ~MZa8UBbhN}xPX0kf)P9g?b(c4ir0K< zP9Hq%^5VB-J<)aP;w6)GOS-Am(R>U*l4czKgEW!qcVoP%>V$n$Gqm~sWp#*05>nkB zgb*b`$>P#+0#yy-E@v{#&atH4(_0Hmbf=X6^`;rQikssdAZ?Jrguw7SP}YGZa?ifS z1BNa(3pZ<8xNUNm)+GZ`?&ococJw3hJ`o2HTL6vAabEMe~y zw6){AFg>upQ^Es|UUidt?-zJP_uLU6q7uP8F^%3@oC%W*?{rq2fVtt%9lqA5EF*#l zMA049d`F-4d0ZwoYw0p1dIhrhEt#QhXEX1s)qOfhEmwezt?6&m=UG$RCAZZ_x#uCl z!tm3yhaZ)+7FV+PYUx#}V;^I;SXrE-zFDQidOh==Vc|>0zZR3fV67#zzkfT2#DKZB zD8nCvU~se2#vm0;ZD^vCzXMyv#YF*do9+ObfpXSYty|Zub)*Xrv5j<#Gc<>vry=*Z z1*CS>C5lM*8s{Xrrs&Q=p1L1?C!2H@2~cJI1Xp+uSky%754(;52|#Nlh>9)UaoI-+ z^}f0367n#aRJwa%2P7|9f}72L7^ArV;>-Da=t_LX;TA3^Z_>>|UiTxt608CtFYlXgNM9NdkN zMH-)J#V5f=9(;FH?^K^}C}PX&(;G4hy`aUOLN>;}i5e+JKP=`|XyoOPiWM+jq2%Sq zE}%(*1J?FSTY~GKvg&OZ@y2&KMkLaT`!PNFv8w8t=)1v%=XSp`ktzmGCVyjuPA<~T zPI2~dW6LUW}oovn`>vD2N zQj(Skn4TYtIa%01(ePpNE}4_J+ZPvF^buufIyJkx&)$@{`KMVt^e4gXuX(ov4$^J7 z{pZTNF%=f4_VeSnx`py#xS3zm2&u}=N2L2`xS})|u^P24tL!NID&|qSK}Pkt864nX z_JrvyqW_2Uk@JML+1kt68YU?j5<;dyyR+G(#1`Hyc?cuJ6{5DgRfsXU@pi&M`3-WQJT}rA!+J4%?bZ zz+O4jY>r?aHr#94>8Yu<2ax4OkjClot?A@-m|DYC(TBP=i9ytLV3rgj;ZM>2y4Ad- z@Nm7HBi2{la7#G3`1%E{paPH(jPQeDTwDPtO!)i}TjkZsBa5kkP)aNBd$wo71s)_Z zy9>3fuL(YPRyBNj)UL0bx;A}d*qjou>Kh*)x4Si&M9R?SqAEF@?9uixT`4Eiifqdz zN$t^QC`H`-o9Z2m1`4I-Ac_6ue9}FS?(uDD2`2D8kxZa2FJW(LBwuJBgBp><&MK;- zU{dDY*Du&Mp9-OzMx^g#8Du+tT6CH4@C>{&MODdI`K-Zz^5gjn(@Q%z7376+%n-Qw z`o)hfs`u_}X;(oT9^bsD6I-tsVtJW3OsYE+D#xxxaJ75L0u2aWerCjqBl`2&`$C!+ zW~k(U0EuWrUVSj2A>YA*X>fn=taR$$^_dXFpa~WJpViT$4(ho1O0_=m8Ke0rMJJ zd#5$NthE^1z;4THzn8#ai#$=WZ%nPp9ZQAnqys9yobR$0JFt8v!kBNwV5R(dgSHh$ zRpb6U)lZdy>{lkp@WjEO@j zm`0dLkKm-RLtGxSl8kZ2iKb32h+wpkHIJCDQ(olD3*sFY&?-GxYoJ$6HOGr7jvqL? zOCE)cCpjLYr2T9s&jhu!I*}ILoOxyfg)SD~Ms#k2F@rrpCiQj8SBqG_UKl9GM0VVF z#=kK9d$`-=d4{6IY2=v4pq;%@Xg@b&xZWsAdhNs#sxinMif8@ad06P`@-Lw zY`T|t>X5OkdI31*6%9{>!Be^T!k{7_MK3Q|4d@#&bb7 zb26p#>7rh*c%0oWJFRG*mXb&VTi|8d(=I1P5C$LsHrk^K9L@Rj#N}@YMrh4X!l0v} z5~kOzsl5Lq&OsN4b_MX?G7@wFbJXqlF>I#+%^C6Y-x%YAPiXk4W1Il>BN?J- zm?AtXy!yj+e6RGXNTcx~pNFlH_=G=r_ghd$H{8w;5!a)sD3}u~2`#(Bh8-=_me>4r z*UyNKDYqD_aJ?b6{q3&MMz3fy1HyJS@jLiIw}WbIQ!kJpS)-2oY4LspT4I9(QFA*j zUnq(?4|73m6cEIx>!r{!n<&io3^nEHGw+7`?j#P)$|^@AGNBGgiMv}ysZL3`nI~Fy z3L*DH*iUI)1#a!@*EHR{{SPy3DOSIo@kqDNy-g6ZVGa6nEqSEaDI>Ql@bcIJgwn(A z*bbeKDhIMLz2ZPy*6I03X~svAAYDvI_I=W=&Vv4zljqNvuG?);#XTG(WQ&+g`J^I4 zwRbMbr_tGIpRcU)>~4c<0e~{Xygz{L#f%*?tTs>pV zp{0ROR)-e{L~Yoqn)H)*#ET#@6`3s)k1mu9|ARL8n74KR_zAsltEF?yh9-A%oip7s z(VwJ^ha)mzUu!A*GRN(8dDDRq^wN6XO$)iwK^&TdNGkd`x4l`F`l7uBm*Ug69BGXv ziba$}WYF<^@h*tdPF@&c@F4m0L7yF0le9;XWroWKOWR$!EzdWlztXVivtAhf1n>Ai z!DYq6B_a$V3J#G5pg9_c_-tP#jy`K=vU5wd*wQ)N-eHuFI!PmB>X*|86>E9jTD?`L zyp6MQc{6rPfkfuJAfe?i$Pda9P|H9p)=YEudHwlr`%mJs^4aya0d5v<+4<5-Vwz5o z|JO#c@4wv+-<;l)o&{A2GqZiyB@9v;6dHK|ISC}z7Icl*1Mmus`LP|$psnY~O(;VM z=P+VgiinL2;o>UC4#4!nBTHINUlbP|r0V#pbK-md3W?iV+SejLe@aB) zUa7E@x$KKqYtOv4FKrazq_pUVG6^KGp#~$&0WuhSYFQ-Z2^@{JZ%Fy`nnl;^#nQ8Y zSd+P8Ue%RuXEcsdfPeq|e}J;<0erVzRxC18z58goI?A@1gzr1#&n6}AHG{LthPbrK9ca1bGqwKkfYb$EEI`|w*gZ=ue?pRLS#976d@bI*$aq;}r`t_#F? zbO2Mnb!vX1z0rfmA7iiM$n=|eY?-S53#l}i;l7cISsG`Ff=%VG*7J)0(BF`Ex>_Q$N+q9Z$z zjRNGPFV68X%*+4ntnhaD>tEG48{RmG8{l!Nv&CBkGjkv-cMw0fzU27n?BJ~3)5vF$ z7R;XrP0a*fP7B^q9d(8}RKlnKv7yJEy31L;x%PXP6G-SkN4eU;$!+*#n}^GWSWSE@@pU^CV8y}O|?YS&FiS5ARAagYgzx0e!eRMo<$?7ATR zNQ4-mniAw2JUOx@^jzo1llA9~v=oLI8q5Jsg0>8Yew;G#ir*;a_;?T}q?Oye?>6LKSsmFhkSMf-lUD zGw;2Y$OBahXeOa8&yKTUSvc3odW>_@fO7B^p!nN3y>Bp5_CBCdut-hNe z0k+tk)3enbB(MQr>ok0kh6UO)F@ZiZ0G22xWomza{^CfCXMv&sV{dMdRcikFVJhW@hQOBmb8FEW6o;Y~&L72L=h<*NQ0d@w>BQ zb+>E;gA0K$u?jfTb7D(Y?|E*u&nd$w$T{6y5Kst2_Zf-n{TyA@wigj!xO^o&N3o$F zp}RKh;TxH1IP#KK6UnMp8cg_`!#Jw2>^IXa={_$je{ zNAQ*hsN0lX2J7h2T22d_RUs(nIIzjyaCH37T=kgXAMh3cXlKIo%<@IbZD= zP&OM>O)D{28+<*HAoap0WJTq^{U?kZ4xt^9a~PL9uj}yD>~c-Mg5r8Xz#c56A#Y5< zr?kIT+>yvKYEL@nDr6V4V~+YDAAI#*r0HVB{fO9r0DS~zn^A+}`ts2Z&NNA50LE~) z=X>0ZFPEL7j31)8UQgLZHFIHjCluF~hM%|0_N2Ic2rG_VZJLZYUdaXbyXUS;Xx5gA zVF-ppE3=0sFV!}Qzb*l#BSS%ox{jjQ=p&DtzF$zEH%MI)m-(8K>8+2@9%vgM*~%+qoS# zdZ2?|0(>bS%Q$(W5DD^G#hl!qIM;$bY!&&GYm#EH`;nY87PqtGVIl#Wty?t~uNaw( z`kbm>KNFO7c3qkPnX7~XHVx;*n=a^PvgE|n5t=GA1gH(1_(xW`>%9_al9OpwW8$TV zG-{|w@W=sGiRp{2G>wkLD&Vug*SgKAeitr&>p=q8L9MrJr?07Q-H_VzAUC_J$o zk-hL=tcQ(Y4cpwK^1s-T&;=<#a=W3^P_07Qo3?!R?k8*j(ZK^r=+ZCFhx3asPG9U< z?F+=|pnF#SzO=Y)Rl|o%+mhn}C$ihQm}i$sBXtHamHw1FG_UaPNn;m%HX0#dChy~9 zIEup%*mun2HH+{Jsep-vYU`=sHvttPk;0@UUoR8-F%7+TIJ=y>o zz5tPg?O|Z8eqHi(A0d+?IMVD)r^hm=r-YfzZRwb*7)!>|(>`(v>*pA7anKxjPIZ5% z@@omA&69ldF3H0INr-(}#LYmVFP^2cVNn|JLPMP?CE=KoC&zVfq)mr*g@efCn z`&gH3dl;bae<$q+yiK|LcJFeqSmeXvcK`*ajW6(Dh1bhr8M~D9ABQJq6Er;rOyZ`@ z-7XR^g0~H;w6a(TbWV5nD^sR+*oqI2Teyoyveo7r_@{0Q>x65#Di`a-0TEF&)Au7B);nIHwF_%7n z;Nk6XI)V7m*qJafrko>S7dIVPdrS(erEkxx{QhFBMr0?XRuhXMY2gje&8poL1knWV zc^b!u3<7G=XFXD)N%(dPL*a!jr>=P6PCRK?(ffGbeFAxSn3Z~*r%I;Rdk70c6y@jq zVOzYPz;Cx0l;v6^(wy$fEE6>cF!e(XyIcnFg>Uz=c%A4D!!Jkzjsi#Ak+eEOfSh3^ zkDp1aa&$-Pa5*%OUwAF}vJ*%s)A!Dy^h^>+-s;fp*Z*KjxX+I=;~=f#0L|l^IaqPX z_+Hg$NQP}rQ+$@1hq?VXd++%N{f<`$_R^d*H)flaR14%IzleUaL#PMIpOk|VY>BtR zNL(t@*lW#9)|DSKTbJ25byg`>HLgLm?f|jJZPw8fB8$>qN*PLi^m zqjad4I+kViD8Z>A&Y@d2G1K7OZ3}X)Uo7<0qUnMAvvh>4%9Dr6JR)WD*QNj8#c257 z%%-O=v>&|XDztAj6}eMn-;bCfUhnAZ71A-^uLpgW_V=8^~oNhYLOFJi{=%XU4=(Pt*lWvX;?mmlk{CcmuHMVjtExa7n$xjEC{rzii=#;g?Q1eiBoxPKX)(C=`JC)o3gb&F+cjU$-WFFH713-n> zN_?6t+j>^Ax{I_FJ1qVguF*tW3RhrXnBM%MbjmD5yRsJ`9R@U6&zg3eT>MS3Ggx~t z#@UpPdnL0-ms0JJEv~FSeJp}bY+6}vhzlO@)Ey6EQt%t9&j`lfqc+X_>@Haa<8?Mf zbCc<&f`2IkxRih;@ROac`y7A(t}PmZeCLFhR~Xp%J?h;`<>p9$wAh6csj7~y1XxCV z)W+8`#U>-J+@m=s6S+0)6BD2Ji{HbvWvJJ{M=*kgLv+#f$HlYw%)tk}<3-q^@TT>B zHaY~ze0Q1ofdJ_P655l#m)Hm(noo<)-$)drLl=o3N#BTrawR1;5GSSXL_+r$=OgN6 z#^*vg%!kZ|oHpzY;D*YRp_NlAb6iz?&^7LbTLM?)9<__12|b%e^9wp(Xf{g|<^>a= zzXP+eKL)I=a)g6>i6w0l1ens_zncs5PmL3_B1axUc5uRrt*Wm)6I+c3SAtG{COxd9 z3DMgf!~_6})TP)Sz;5DUDA_%B%$+Pmbe=j60%kCaPV{ceFN}6r+hsoJk3N9MuZ`4W z3<-)R*GHqfgZxibUG({T;4T-H)3gwg4jzjcSA_o)=!Lh=(ZuLDV^v$} zsH%d~TUXzo9p@R6N5O2Lb*9#$%ly#K!S+BubhMDUk{zk zO69^iJaIx-%)A=_AsIU>e9H8ag)$C{4z~6xVv5^_({Z!#x$IXHjnx_N4j{GBxFhBa z#A0p>NDN^cGx>siK9`3-fYT;P6F`~9wkIZhj|5p z9+qebS)!JFFcN+ffS_C4RGi}@!a@@u->KiUm8f?b)X~JO5@&rd6oDF!R@pW!eD|}l zCGzSw#+kv%ju6qe1`Ga;&j0*-JF`T8;(4bwlLa8h0^RVvfan!cQ_GtSG0CIPpH~VZ zdSw_wNhaB^U?Yn-^$p~KvuFg`((EPyAsI6mPBU$k_69uCBkHh|nX;qH)S!N06Pv3cKk^E4uf&{c$lYru2 zndOc$E3Sh)ofPh4t&|>!XjGDNk7+yiy07g=!NGUgHIYjmp~P$`87i$zNlr%)#Uv54 z5LbX=oA^d|U3a2hvE@Kq?bhdLnJE=9l3kjI7aKuyOmrTqGyC+nR7;9`UL^rTa@zNl zkN_2Nc8ji;mB*hnP+&~+p~4r+{O=UTQ3qY4=E6A$1U|4t$6=D`1uc&#Zjc4 z)Ad^MU|fn2w1g>lJ!jkp6s0t9-S?;o836Rg0N}u)Scq`e{(e~OK*9@^Uu+N%CUpn@ z*U@AXDKKP56+1d~^f~L)jiet$JEAM%Fj`1E5lt(GcROu^Q=;PgdZ2&xvo(?4pN6#)tLG z!+Vhs6|_w(NgNFD-_`1Q{8j^TQ^mcwJ%1rpgQF@&K+I+MWMLCJNk&X(&6kNJO`Yd& zQd-_S%7c594hTG9U3PhMs$ezzRwV3R>Z^XTNvcL_%HKMHmpe$XV9fB!Nc!QPS#+&k zC*-(Ah-^(h-CdKUwGQQqXr`22P$G&`asgCKJ{J zL17G^hKZd>nNmaO(G>k3ciz$zjBct7!IQdl6R6|47o4v4Wu(3PfNpGguU0OOM%rn+ z$^lJ&;)YE4U=4YjDZkjv*x5Jdg1p-~D4y>+Lx85@R%1c_!`w1N`k-wB@?f@h=^$Nb zwH!14^5C`KgLyk56|u^eON9Um^jVq*s}W;>;g3WW zJTbDC=+3-zEI>II@YA#=4dJkUR{VUgwTK`uCczVNwAM2_arS=zbF~3ay|Yx;YPmZh zC--iXm3%iNCH}sGi>$AQUS~OCW`|oMA99Dk`}JS;$=4nM{3&qa5!B7EvyiO-kO(X1 zh(!ZFv-8^i@A;;GM>bxRvlMLk59@&pAI%g_kpVOYf8gF{{MKt z*_bW441KAM$uuM7(q%4Vlh{TZb1PIt-y&q>p4(9PX=X;MncL)kE4OYgm1{-0hoZ=$ zB=_9`FsEE^4|ONd_7-}$L>H#ip0%l7t-yN`XtV85ByeNs`Pd0`@5gydiTzx zwGZ$jr_LV;h~JGY5k&^mzu!e*)Q-M-*JQMdc)EGr zs~j8(%RMB(Dw2yW(VuUjmM+?zt&Otg=55}1nV{`gxFnBnswT-zSW%#qr!lSoMcJ^v zD&s)BR++`29$Pj?U!M{c2nm#&@@$Bap{yrlSHwEK*Ki9?MFfk%Nu6ksM8ovSY~i^d zD_r|TVERC6s?^j{Yj1e#bjzpY;BSY2hV1?acr__(^yzu*Gv~nNE}-~AZzkGCdEuho z7yXgms;Zg*=#0Ax6de$>jgP#)Z3AEFdD#yY;x7o}a)UT}3!THo6_zd>g@%{3Xc=2n z+Nlnx3ZEA-^4Rv}$e>)RMsHn6_Z%Lgo*uso*t#@CjVtFw?*8p${Renjec-AfL+g+v zpTLyzwfKb(XPqqBx9}y;XgE{*+dQ7=9t9Ns$SvJkG4PQ?V^KHcQ`267V;OwHR>a7F zo+K5a)$K$Dns|AtjGFA41yk=|t{G#q@(SSL-O4JjO!+M>O=Vh6n8ximv| z!nTL3)hlw?v~@53MK&`FEvyh~S^$8ArCx^UK$ zdNK|{7sO72qKzZ`oJ*P26&8Er^bT4M>_FuE>_A9bOPSd@K5keaQXGq5u-c zR!GliMac$KUzkinIJJZ(7fBu>nh50Q;hvtT#mVc$CuM@yv4&A z`l1pg`NeM9BrP;oATzI&EFsm=IpaCwBe3)yL~C5z(`u(=hV3w?;dc#Kcq@SOMqe0i z96Bd%NTRb024aa*qMOUFunr%K|IM28eK2Ho2jr6hOky{5vw3G_GUY)V_cmv2coe5) zN3ThoVMoewM02$tsT+&spJthkGa9g5y?Aj{nm=_mR1BT2>viHNT2V)hoD;{qOg`}I z$`3zq3^b?d4M$1xhFC>>099ojPKG^O9dVA=)@N5*fglY*e1|TrA06 zemU}u3BbEeb9eP}-KP0w0H3RuAWYyKjDQX>?9=K~ulWV>9fmFH-{ll=GRwqla%PEl zUQBy7Mu(IKjExyNW8qF>P&pK+<>5<U67n_GsGC8w*Wz%*MD6qxZvj%FT!;Nh|y_O zJS0ej9$x6XNm)U8Kh|2&4g+{+%O4DlubBv$T5y=KN~(*Mq`pMQ0{{TFO}6}7^4b`6 z{#Cb@OGF%{1_qpoE-{>m3L4$MU-U|uTv2JM{(dK3;+@Y;KMhjUz~pZI00NC zO{1g)r4LPPXY>+Ur&*(4Lmm?})UI*4;h7~0On|XgaHz4g50kl{m{#FH(yL`3BCa93 zFg0U>hNL46nWgO&nMSo4xkK8hOtCPM5(uXx97ujDYA&0M6F3ydq4?MU(#zx-)r;5d z=IkN7WM$Y@&D&&XA9+qZS}5|`SY3~?vmFhn3p5{u_EF4@GWrNTk?s-8;fKl%?|rl; z?|sU-!6{Ie<`L}VO62swnv$p@(an+cxq1AmsciboX0P>D zv>Y$3rEy?Hsib366V0)d+(WS+giym#0YQDXt3PQGTS;Ssv0`Er{o47!>Iab2`|Ey3 zu_K(h4tN9siSLh`2CujFO*&-W8@<&tQc9t?GYQ~3Z4=rfVQ3bF;WMrENhC1&bO`zl zE!StPnj75#bG~ScwYBy}@T7RtfYi8DY1(pBYzgUa%>7KYGn?LaWU4urk|vH74M>F` zPy_y}t{cNwTMdUlrSwym;`l4Pb*=sMW92L^hqu)F*EuHo@8oThASeHSod&vKkeSehE9-8pDm2}NPo@E(HlQ2T`KM~j|3@!6Us-L z5RoB@5}CKybt)Nb{ZrmrU<^2OIowbkKks?Gc;cLzPU%HM7&)<(7Q7mDfr#q@V;@;NjKWOrP*_;U~yU`#xoCuh{ zqKrqd&EwXN-vyeP8~v_F4@9 z+vf~I{wX2f6@R{R=V^06MeG>I$S8BGSg@23InA5d^>!>B`INpFdlh@8i4(x)*P|tN z0ZNb-rLCbXwLuAmuTG!3%t1wG^1+Igt4#S4)SYA$wu1v*3i3uYBlOKs0{^rnHnCNqVpE;HfwT|9YFv{gj>w1`gR1!7;LwK?O$JFLZ&93uvkBwN9eW@a^HO zB5eC;=w53WdNsGl$YzyEMvE0LoSR6zKiRo*0&&}fAa)d#NTCg%iJ!Oj5m&Ql9jQgj zLea?Q8%xf&UC-V)by8x&Jgc17B%2VgX2u(}D6t$1IY>tJeoz$5(=)$QHWkks9dIqr zc6H%0_bC97c<)vuT1@PTP)U8bY;-*{KT3-MqrkGpbJ^)&0&v|Jv{2B%3;(XHW3GoX9W14^Jfg3%&eJ2ZqF-1$u)ZpjyM;pswl9W#8o@ z`mK|}c?kD_>SqHYIQ!}#zFzM$>6|59+95G$;6FfKW*}`ai1o27opQB--L<@wfRiw_ z@7^TMX@Sy+KYnV%cV8x>RK2H?l1BZ;x3|BI)kV-@G?Zex$^)cvJR)-AxYW>#dWE%M zZpmtTUk}8v&18N0SILIw3D>=Ot3?=`-P~%2Ui!G%mv>ecek1U>Z$(5@uqHbro+r!J zucl7YBN_S*#MnfXY@{Y=nLni>Q_^Sl!AUE?MjkVU_gu^_pV1YRivED&1_+GB-t#JGt=FdYk$t3fh zeMU?!jZ9^90M?MV|6D4z`+DNxQzH;j9e_!VVCqT)jsNKTqWkXzc@Or7^Ud_dwpR7= z+zVmH4NuoolzdeLEc4{Tv!kJQ=`a@OPKSTjqJ&gWda9up(N{2)VPH>m?nI?J$l|%`dHs^eorVe&vHM zXITx(r*9iOER@`->6iU_%fTEL2Ue7V8(9sA03m1GtMt$h$;L!JWWVQ~&QE4~$6ak@ z!=0~w#(j?5*z8z4nh2%L`L!~~I41S0iL%6AhpOG%%W0T27oMRXYmAx0&~~0Lj?3t~ z8T3abXthK8qmG7jjmU{5sRW*yE^o&DUHoE4XTMnn%zgh(rBSv$%WFg&nW{5BBiY^W zYiAC;Bxe{ewW#}3Up;q3Y8^p|36jurvR0y7S&Q$+td`N(tE-d;7hTNF?lR3+I>Da! zmC1qQ2H*AIP9NwmmjO;z{?Tqiv=l1l^cVgX&wKjczX$Y_W5 z2ami=;kJ2fsH81gOf;FYQu)*(pXyW|e2+<&_(G1&#KeY3P?rJ1TyH)4g9C~1f_pZU zgq31)EHqay!wf$KD?#D5OZI5s_t5)!VH=-V{LEqR|9POwP^GbQ>>EyFiTWw_k8a_k(tk_dNr~Z+tnE} zvG>gMBUsy2PbfH(1rVxZ;(qi6()tT*zFu9 zDrnH$iriUZA~h4z=X>Q~>9fb$p>M1;ZSA`0<5YzVrh^E*?%6>48Mff)dPfa)TQ7Yl z4+8ow5+-&?nEv%~jJ=1HbgAvvE#R$*2TRk*SX}pXqtB~PvpFP}e~jGh647W$ZY%4` zRm>%?c8AM%WWTFAz)kBkTld!k(X^|J2a=Ct8+1$Fm0}sEKl3X}MH4bjxOp1##V(}6 zxHp{DlnETJSC}b=q629(|3Kp?$icGfwHhK~tbLTbe8O5JPmHAA9ANfuST)xr;dV-G z!W35fCnedi>lYQGjFvXrV`*T-Nf4ABtJ>p@nzM+T&1uD&{33GjZz$$j^Szbi*^}ec zegGtzkp8xJh}H-c{-ca#5G7sO05>oK%y`ZAi`_d8^C_d=d(PR}`yf%N<;p7elpzh# z#VTL_O-em|{pgH_Ru{^P6CkiS5DBvV&oDr1Ab)q?ZM*7ZB)MlSQ z_o#KZcJ{zQi-bFbl4Pjy@=4aqihYQ%B(R2oRzICXhND5V4*vm^4O;HY%u&NATQ$-^ z2$&!B%yUN%K0(XW?vgw=H7x|YuOoQtGfTXc`+vL+lfEgpfw8BUis>&&tq6Wl?!`)* z3;w6huu^Y`acEQqM*^VUw7Ml+YB0<%82ev2tiL{-m}%L6#T)TfX%#JY8lp^>q>v~{ z@PC2XV^Y8HG~3M(-1S2gS#q0958q@pAk8GORRABOMW%YFe_yp_E@ zl(*hSHjBD8A^eb}!V~S#Vav`T{Cc|n3FPZ_vC~m?3fW~2iJ%$5GIx#1U`;1tevC^0 z-}z#-p|yE`bK*&UCa{+=FnR^$IiN}@ar(Gz^xzs2ndJcZf>Q@?bayTAB0@_==GxSz zy44PVY~3`p)kGGjG_Cr<$@$|wpbi$DUPvaL<5nPt>yfK*ptA|TrHG}YmmD5Z=rADz zk&r|XBOju&sh{gtuoprvl3wmS(v<}uR041yl|O%d9v!X!HgPfPXl7+8^+ZG4=53c0 zsJYQ&t``T6LvA~M+P);GNI$FbwK#`>N*CQjQNcHgXQzJ4;FHP^glE0A{xO2iD;f+g zvU_l&DG>+z4?upet6#7B0E=dDDx3<_u1M){U9cnOPEO8W0S=HPu7ajQjusXxP9DDP!dxcXCJ>OjvD^-|T!E_Klq$gt0I%q2}chB?J1(ocl$-#@6C5F zMV@{MbEqPloxs^b8UlS{Kb5DjKK_C|z`A4>1P7>T|De--<=eWIO~it}>jqtb#vigt z=z*ETAG*LO())@UU@QWPMGNAsE6P+K4!kga{#Ew!V3x~!mlvcC0-cr9GAobI zeOeo``EAGiTRHk4h0&ueAHe7-s}cc@Zt@uclP;0%&=Qll@OGQZF7h{vT4q z<-ubKNx3~u`lWxXJppY8Uv&PaUH$Y~C-+slWX+p03G`yn|46F1&rw?CH@dE()sKjy zS9fwq$VU{GWq2!umyzbfKR1AD5)0sZXq?IuBX`7X*anTqZEHoU471xj$iOq;CJxPx zPUn?)O&fOLFpH%E$!VA^0gM5n02RP)T-y#~W5hB>!Cb+dD>*AMz4chGX*)0D`0AMx z>O^3TOwL97rQQVu@yu!vT0Ink( zRJ*_?u9f`wx1C76I8M5N>C> zSJ=Vi%dGs`_&}B1*YV9?OP<{pAX{xjK_S3bY6&T_8N9eu*)C9jVu|9q_t-J8&L6?0t=?r5?% z!Edg~WNS!E$7c@|iZd1P@zS)?b%;r*lH&186Ir6{#^4j0m&W}MP-M8ci*VTe9u`n~ z&ibk*iUk#|et{XHwsBv~(-)o{l#HHb9#2u$k;lQAc(zPQoIZC{-VY@xbTKyI(1Giw&q1hm`(L^nUG}lD(eC!F3chIS?k2VM_u%ou_Z1299nwLm zQJIvUuiAPY`%Vqcm(ydKpIM$pP5nV6t!b&Q{rsUWD?yLYS-Ge zW~h35Fk-#$`2L>#Ubo!317vkMffB~$5I(81xTP}4&EDviiqU6tc(BIf3bE1&X6U3} ztT0S?t?pyd)!+S02dPT+Y#_dvW#u2r(*Csbd|5?V<*Zp5sSSt2f?y^QE8{Gmi%3!9 z*BU0S;RBrqlyJzK-0rt*BUJ0eq|R-t;8;BT>(>6%s$*XxJ@0 zuBl`5FO5zzL=_>`i$zF$@rX3W=#>|3b-K%iMKpc$rC#~8yScU!dgG$(xZ$I3lA4F>=!J8{g&DPd!E3j$~xlfh*rOQ8fR5sutt90u&!w7`nq zaqtZVl{@Xr_%@}=W3+aI;wGOUwy*H)c9!=o6cr<3fpq(3w&0}=%iSX92|CTCt;HAV zoHY8Ca{ETfx-Fs`-#;{`U*Q8w$JS_rD zljulIEIm$(I_bb>UkM*0hl>*EtbhexPFQ zcHKVFD8QURtqGO{F%Z5|q?wckOQoq5qkMhEwNN;5Hk6N9CxPilOcQW*`bH6V?Qh{m3llEOljT{^*vf!T&IFs!95Qx7uy;c}ee&_!wF>8zK+LJk1A}*iX5;wv4 z(WhL}wFGhI-0C6VQF*~Yeq;y&l;$(cy5Ui5HTQt1M(Mw+2$yODB%_g*g!t$DYv^g4 zO4+QQe3YUVhQ^I-`+4%l?QOd(t^lBCsGOU5s6H@wxgvn8kD|QJ zdr?H05Ig9ed@uwvL;6~A;!*v+Uw3R$5G(sYi8yUv4x?|Gys(_k3|ASFz2&lfGE0{J zMAbmVmf{r)m;fMd4x*Cawd5F*@8b~oQ_#7>lva38I}4rWY73^Ddg1!yULV-Fgjc4< z-&=z{9kQ|9B6a>e%3Y4dX7FF3-A6F54L<$vpT6d?RfwAlotrLDl!bjIfV<_39pK>W z=YbD`4`>H|p>X;Md+0kIYQ$!#j@RV)?y}RPHX6XXil-6b8i0HdCQ+LHnE!eM5dph5 zjw&VDXxbTaXgI*jn8#6UcuaNCd@!X zf^F6A_GfLd*cdSu*%1E%ES2zx%v- zP*z!3y_Y@*jM(08CVp8>}GAVs0(J)O>N zqIHE&x$}7?Q=p8AsJ+zt33Ziu^24M}hs-NAeRiNmJ7r~2+*4cD?&|RJ1yFu+kTv<} zgn@04jO=kVI+u9BH?q|v$ZQhVhUfw7Z8@GtE~jH+poV zTEUXCFClTKjWQ22!{Tc>VW5GuCP&WFF#)_Nd7%p#I~H@`=cS7NvEfWWs02!6ypNQX zo*aKtYMWxW+?Wce@IX!RjW^ zjrO-P{hO_1=2AQ))Uck@f+?HKU?!E({{c>JNFQ9>?^pNhMFKQi9=f@`+gyL)^SEu& zPTKSw9IeR*Wl#}LZKlGMj>&UMrK!ED3sBq!P;&y%^^@!;{zAQ^n zs*b7tG07(yBDeW#LS$WqgkvIO5Q;#gE=N%rZJ3tfLOV$-({PIIh%LEO8PCFR?@}#^ zo&G_0gy_0p2wNG9rHnKsr9HY7Hh!gSjno)OKFF#BchpahfvKV~Dmo*SblgU!MjeH_{pR1kS7ZiuQ zDx&WVjJywG(o8Sg)9W`Ez-dF2v^TL~i0Njtm@jTW{+Snlx=&$xs=%@sUOKW@)!O(MT9@0)HXQWde_3a*C95;H*boHe>E5mvS}$(ghhwWv^dfk80 zWhe0AKH9A-Z=nNNW42@ShaaQY&st}letpNR2&m7W*&9cY`-YrS5k~xNQuR#~@gW^7 z!$hM`^cB#1(9fvywvX2Vh+vfG8XZ6)QT0}UD_2-kESZRV&+`T#IQk&O-28EDBqws+ z^yA`qze8T}_^Z-6LmajzmKtE#x-ie%=nYd$dD?g8Phuu+O!6AtsUFS=8O=F}7o;yZ zoO_(B|KS^@&~~&$2^DJ`kuLG?AwcQ1_U@<)E)fWHDVoonLhMX-cTVPZ4j++cVTnwT z*NwzgK%iw91Z23jBh&v-&i$C}p8svqY{^})sr0U2>AmC`Pm^?e3HQ1In5z~gb5^Q? zpC2p2-KV*&FRUtF-yG}VFzZh^%>Gb&4<$`_gTf8EP}iG`9j_c94vvz?Nkl9gih`4M z2`tI+#)z#nDeT4a{`_wI3NO7=HCjJ%q=s@2BQiEZGGZ-_f9o@1BO_Eh@~zYIMKy;C zG&_8TKNXbbD|2CQxKh933c6$l&cj-nb{3mVtD89f^SBCmRgBUDCqggOg1chDa0J}i&$D@hAGl!>@_v$@cPwta5;c?C4M1Q7UI8I66f!4NIpfz zpu_-RPi7#n*Kj(1uIqUDgwXoB?9;msC=RuPKM6YIQAH%$M!t@QN$J*qqv@@qP}*MX zsV6`d02dl|d;tiDgBOD(DJ~E-P_-r!H5I%g9Lyb3;opwG=*?Uk_ z--G?^wZ>iCh(IEpLtKsT*p1CzooP3JIvqs`*4$iTSLM1}L4}afwEQhcwP5R@i{2ae zH9DMPGlAcP7}4sWgBStXS6wO*sqqBwe=^pfYtF9IU)Smqp0E=cgyCd19GDmGAzg?} zl%7m$2=A-+V5k(i;vS99yU3Sd12c+ToU-Yh&{Z{y-l>hxvu_}KOAc%P1M@&5Us4FR z>8%GXKq?a{4O_{#er3&7G>zZMqWl)iDiR9Y5aNhP#==_pIj@w5lyT0as~-+=6^k~i z;uO&Ax=p459D|O%7q#n-JVfd(ZC$=hfz6*)NA1L_7z!sm@nsX6LFc(M#~x)}@J+=% z>{s|IIU~4{Dl6GHIP>)h>vEqH^}@j^tv%l26!L!noY+pO`kV4MqLXgX*y%q&f=kJ{ z=Ur(swoaKVDc%JKoXjj(F#1<8;*QuUh8N7 z=utn3;L)ywS2Z-bQBfjY*l)PnjVn)TPv4;TWxw$%6m8?85i-N3=aew1%;=`6!8WJW z@D$i(6gE>$rUB%ose!r-{X(W1YMjMw4Fkjfc6&U#UFG6|wLME87F2$9JmjDtMxw?& zx)ELbwOW?`QKzs^GHEaqYykw^ndye-<##D!YhN>F5|2&VUKK0s2a;)lZUB*QCBQfm z@trp9e%rq6xmoE5U3mU~3?n*QH}(OG16Ble8zNgf^!gs|=HtGVnD!Lq-6ks$$X&LE zRJzsM$SX}I`-LmhR+^+CW63e%+@;PYf~IhG<;easRvCTZ{XZ|tj0I^4+^$m!}gsiS)^<77eNAUF7HO#k7 zeYU}On)0;LVtg;*+aBvK#+un)FNSng1xK&zvn*sFJ4( zWhl*cE||R!t3HEIYCWe;ge9RBHN`OF$s$qGZ^`Nx4SziSEV=O|^C;lA0|0P*qX@Qo zKjU16p*70=WG7Oz=RS$+LhhQ$-LG^Ph0)EThKvU~)r*HH#cd|_8um@brTk`gc71&c zm{uZeGQ-e3_jq3PXP?M@nm0t!^1G4%EI8Ixy_p(0Em~)-a9R4dJJ$UdSpv9byuI?4 zfw=$Vf(j~Aos=#i`O`3Na&mpa9-WwFG8o6@->l(9xMd4n}YEFY$1R0kn>2sDirjZEu2@)f3VV zVG6YPxpF8O)w@hXFdC8TyTYg+Dhq{v8uo$`xYd2sUNl2JkUTp0gYL3l5TK)uLimp211prc?IIgQ&OoL(~XMj4Jt5SCg`S@!ZZdw6KWP(tMaETIjP&; zv(yt1P{_<@fls2Ygaf`gK1(zznm1?y@Fo!?>&86;iH;{TPm-F7c?pzz2m<0XaCTm zfx70PbsqRms|iWgn56zZZo&EAu8wX|3I`gM919=6IvplAXL}Eb0T*MMAu)NvG~x#0 z%99f8Kj5PCYAD5xlN}*&8Gau@W1c&vfU_!?lu20D#C*KOng5;WwV!r+P`9cs8)lBh8G2} z*l_t#scB_o4jM#_JJG$ADCgsYWQLueYTAf#DrjwERh%B z$#$Z-#wJu8>Wjh}YPH(6DtA&POm|p+Ulw{csc4Iw9U5pRWgFNxAKxLPdXbVC{_USG zZ`Fcs@XXqcscFC!!`?9Tlw;0pq_-Npj%zgIqm?ZS?zksr5#>=UaZTk@O8vebG}-D2$eddef9!Je}%VWUHv@^MbOfj2zOqf z-uOsr%Kd`}_x(mq8}>MIV*&HsruURrr8EG8M2BK+Dyq*Mx4Cl!H%&sG)YWdlj{xdS~Uxe zaSwj;bK&3R`nqKWZgH?q08vkXzGL!QuCe6a=jGv{2LW|e4n?x&_CSMO?|Y*6KYZLL zix~c4R_9`AY?kADC|Z8PbJm1Kjjl(aMd~M+`4VZ9W`Z(SOT_(aj)Exar^%PEdix4M zspu<>`2PSIj zDo#XKmQhr%(5uL70H;5sLv09{KZEi`k zG!*GpNVXV3IY_D3s@UIB*C6+4$b@4f)(>2HTFFWDG4KnlAUGUoT%;gacC%-p6F5M_ zR?)6MN`$306u#dcdUmg^E;`*GTaTOacJE0qJs6o=cI@rmV$M~it$Y;L2uK3&O$|Dr z%nswY55iQveZzf7>ZVD6^bY;zzxRYs`U1kU`ff^OwO-fJ!E30g&fPL|RTei1G~V?w zwZAUGWXrJ&WQc$%xsU2gQxOjLhI6&-Syzh4o6)|~Rb+4oZ2+;gl~VIMB5T0gn&L7v zV}7ce43|NXH7?o68M!YQ_l!f#hu!RC_c_3+U+CFW1oc2EnTw>t%i+2%)_#u)5}(gQ zVR&`CI<7Ij$9;M%G!A*vgC*lnZw9I3$n~!aYQI zF9UzpZS2vQ0!J=Stxb}S0?b%=s}c3h1vamjUbs8`@A6vQ)2u|1Agky|BGw4asWNG0 zj1K8PRCc&g!^Psgnx>Qc%%8?$Ks6}VtAUieE#hqE*uL|&q|F|PdC*p#fTJ(D>kq{l z9zQo;IdJh!*{N%OuRSE86kGY!jSe7c0}#n%+~+r5(Ho^ZBMXXkZ}Pc^xh&!egKdS* z9Sa&K@BFO`E_;5s5@Du-`lbIKK@l= z3wR=|G?I6E#4!e!(`c5GU?yFGjNkSe>Nqfjz+H6xcru`I-?JYQF0Mxor|rG2e;1Y| zd2TZHSnEftjI(Yr(h-YN&B0I$DWhF$aR6H|ZYTf>P?8hB@=&tcx3}n-YqKqQg(B?# zQ>Un^J2BR~wW@MEOzkxG!cF#yb9p5s7 zUa*h?>=Ws$t@#9rhKkyV2L{m&1!Ci#?(XFjp03&N=IOTTh@Cw;n(bTvD+A!7UH50| zvi%v!#*Ce>rP=QfnS9aUveLIYne?DRBSL2*c=z*~eaAr5)fIXbzpeyPT!`}0F6LeN zF`53uIYi}u4mVED7D4SO@IYcWO8m0L7LOMhQdV|?#dv)zp@G9E*^<=byC5}?jkwm< zik>A6&2svn7sc|)L^^2P8?!j8{XKp(V=pi-0pos95{2AACFe{<&({tw9Lug!tlJY8 zc0@{1bA1ghZ1LZXEsyu8DGH;rYmB&bg+ytCUV~1Rzp=Y|&PFHFzY|uJ=H_5_d)bZ& z8C?J|Djo-RcO?4_NEbz^<#O{%A?$D=cbgMNtN!Ao%5`E&v>f@3dfTNp8fVg1-5apc zROqi}WeY=5o<9fR)IC>bF~cSQ>U?JjDGx9JxqhA(z?iM~({M*G?m`;z@)3A~CbmD9 zD<}qR>^|9w!Yzh9=uaf6cq+Nnumi)CkS)0bjnQ9U*cH`sOA8!Gb9mrc($q|PZgouc zq|aa7FS7dpuwS0u2b5*Wn{nufVW9K+7eXf7U7XV3EE~Dd7`48w)n!xH(yzrHi0Ol*KU2TiDhcp(c59U{8&`PT+w4>%l~fGZibciKel>4|MO*Go=g z9u&eoIo--My=LL{TyNBwxCqs|>_8Zc*4>Is3pAVw0QTi6N|*aE;a^0FBR*qvixc!y zUplDoQ1zWR7Djm{b-{wX#27Sh*Elsubo)O;c=dky{UWyk;c{NJuJ5Xm2)fQI?o2`kg;G8B*?hw3G15{vgEy)Qu2}M824R6KyRc zlCPQoISTaH3aKjy0AX=_bN+5d*|DPMrk8yVSZlJXvCIEACe(yIiyKa=R`)zo)#H z8ewtwZ1l9w=q|0P(V^f~+PPWhAa{uu=T^r=Y7~P)HG*r{LH=W}21&rnjGwA2bml%(p z6&g)Te~j99?*oEt9s0Up;+~`?xb!yqIp+%*OJM?)r_x!5&vJqu1cX!HM2$FQV#)I^ zN#YbOJQSV4tonpTW&!BvQgjs+d0Zd-;f5Xh8OR zg(GjutdC$L9E)3BEW8c$jW!!F3p(mQMc?j+K5DHq4JI%FE{9i{h9Jw?{{i-Tl&$3FU%8>IVZh?w=;W!{7w{<9B!)D z*_8G;Yp1SZrG)!h(J}Yu$M3$!6U$!5@94dM zaL-k%yYD6BxoMpVu|{!grkfk@yziJPD{+Dk$Xysys$K0VDlM+UwsgAtu{1@dU)@ghX93>X0RUfH=T@P*ZUqCFFE5+TtYfqUxfieL0VmiE6Qu# zi{s7L7Mz?vQ#cKe3}Ry(R#rg+yj{r8{%QlGiEFPL885j=^`78x4>ePdQWO-hx9M|( z!&mI7WYa^PWLq@!kg*s$^s~(lYqoaR31V+3jrHF*?hM_08Z@ai|MF2(Kv3rw-H^dC z{@&PmC;Opccmx0;lPwEe>!5~DE0@<|sL#2a1av6|MzYhQP-(WbA8=hm&{C^#^Bun|>qP*go!_&9b`Ggv$^nkCh__5wh-(zWIwvMtAWB-Osq?Wed zbAi|6I0p!XehZd{C>4-5IB=)<$>Zh2y(idWgN*GYm>UeX z5R;hG8-wO%+Lk8ubXjH5Yx``eK$ULja-W^Vq^VjH;?5fL9Z7srVz>sZwPxhn5yw+ky|H6*-lt>;dQ@>1~UY&T2U# z+1$d%_8?kP3jvKg82Z^g5CMm^dL{qmP3Od?Vt)nAIeL zRtFV;W_8XRs?=k+uY3ZIxtN*xbGfOW5H0BRV9<70`Td<^Us`YV49cgm5kYCIg`k#3 ze0!Ll=g|`etU|s+x+t#%p55MruqN$27`*EHC;ic5md&7-5B z%E*mmBSn&!(^I~a_F!*k764+ zj0hD%MUnF{VkpGSY@(cPArW$p9FkLomopP`$eEl&_5SYr>*pWX<$2lV@_alW_s8RQ zyIv76tIV&yJ(J0R1(iRxiSoEs9QxQ4K{6K4N5jGR*&+L4$rcm-Ir$R&vQx>*bfgk< z&efSo4SAoCFbmO&-;VOQV*N>;SD#Eg(em&+vbR{hN6^iU+B-4w*#aiCU?WRT{AIn#}s9G=VWeKJ>(SFRr zR34 zYX0JEK5w41S@dkD=|@lqNP(e6pd+W4J@}S?K_+6RNh|)){x7_?qTFCu|K>UQl!5+q zH9+Y0bSK2GQ$Ju0zzpdMD zHsTt(^iZ$=%X>dC_48GcDj6%)s4HamvZUv+v~uf@qRV4Qml~J8TZrQsrQt;qAUO;g z@<&bQ^|aZs$JYPN_(h|?|CF=jFUYP|`X+PS9lDaGZvo?Ozl2}-A0RQoy?W$8!^*F^ z+l~*lTmMvZ+NCgRB4GAl+j2Aj<$(5>+o5UDV6^xBJcidJM+19byD!`OnT$9DFVJYc z-Kq5JJ*51>JZ&`r5C*dGriX{7tyfJ~Ch(c^@RcVBQA{D_d%GoFt5)?tbE?25!7my- zG{YAU{(S7jKK_K(;CxHDiSP#q!OLlhE#o-$@Y|%H6>yT)2vnIRFODpT+M4GS2l)@Q zuG>GVP$-EO8yVK6(_tE3JYgR~;4%(&4c>mvacH|y93MDir=TDqi5}HaQ1C8%S38Ln zFQ!$E_X4m2p-aeFR@jGo38B96?RBxrii*b)`>igY1`BPY$|U)>Ki`@9{L)UJNRHsS zY{I1n_7H;Hmz%f}mv_fj@Dt&NVX&!BMs_YpqIWxV^X02I$Ihwun^*eL3l*403FM#_ z%*LNmQ^h4*|N2{`7y+kGtkVj_K(fL*a*mdr4+>QWQ_E<<@Ioi}p4>>*zs6p`O9AI& zFjz_KBn}cZ)ew-aAGBdN@M3j2=ABm_j!zOQDZbHL(}OEk`QB)n>+XS?hk&GPK^S?h z?RMX#4bro<{!_M}oLr(g$Ji-daMV2LSI;#Ug?^5)!hlF5;>90v+CfC`=D&ZF+#`Gq zI~~5h?x$0L5I%D=FGC*Rl`fqOYrcIcOSi(uI+azxjDRj3{8e9~gmp;3y}Zr2_%dw)sEP_cqvee0+3GM;GwEkj zh@1b#+do*+Bi8c^Q+O9>`OTQFmW}nRmi+f^-`J(HjaSUfY|Je9^a;Iyfc7w~mUZ0o zy9Hwe-yUY02(u-!GL|qIDx|spHveQQ#a&v>{`8f?&J<=~VvXFLd-vHP@4IVQh)O1z zAigx6k%i@iARjehTChc~U0*zsmESCA$k)L{1bf*?*}R$GVAMB~56&DOnh@fQ6z?*Xs9c9qv_Sn8K7EJK#*MGGXR`YW7(n#aBDua6mQ z&@31|<*l2iB-RkVR;+1)ts#xohM5=eW%if#OLBk;smwwKAZ;%3W7kB6uA% zSpnNm!_G&|Y)R`^DS1?>2U>DsS^0Y5e6T9&rmIeBABj0Ql2KJCXZa zlb*lpzFxa?;(e;uS+#FsZ6{yERUU09yBoMZjLzNbgT?%gw{_fh; zNL{l<%38!l>#li! zu$Qm;Z!*w&CG_}7Sy|%%K2O1n^ewKC1 z>v=^3#Z3_h*8;0d1wQ2Enfu8AYJljnR`SpXlW)nt8m1D;-6AxD&P5eo??3SY_w2jl zS*>+4s3mVvf1W`2Zm7tesa7DaMw))OL1aB_Pq1*Z~W9`CIo21qJX} z?dySBs6Pgk070*n;pQqq;Imd%m2wF`HQ;3CMVHn(t2wRWe& z%t8`0!Zk-%-1wC-cGh_GoQv=xp?NxzETA>?_%%kf!+&{D#}RLVEQ zV+Pam3LHoSz)PFAT@Avm+pwU+=$-hwe8YR1n02n-xvSWGl#NyyU~6iIcHN zDV&oc4XFT>)Jp~jLcDd+&*l8fF=f*@w5|f-zE7BHKMeq%#ZF8A{%r91y?m_ex!~f( z?l>rkPUw==q8SEtNc}Emqs%aQB20P2ygbo@17YRhQ90UJ)m}I3z?S)sWA~I}9*-iq zGJY{L`16cV;t=@DAH9bWL*9U;uA&E1qIBOyHifFSI_|H%c_N4TNFrBgdPAojO@Hzi#?Ch>1_ zVnWueo>6qQ5%CN*P9^bL5arRDSBNHxR%rv)6X zsPvG^Ora<6tMHqe`R*PLf@6PF7MC{@9 zscqA?=s8QtbeT88OLeFQ;#gQrO~vBF2%PREBf0Vx5Hoak?@kf>?WN2`Bsf}$ zKlq7?6cUhUsNUR#nuz~sy40lNC{1M+%OV845A$nAD4~&SMiUpd^6SNok*v>x)_M3A zt^Mi!Ro~_g_rcAK&;Gfd&;AFv5<$f)DA}0^RRox%)c9&{@Ko$!-YfZbSBRv`stcx_ z%H_bng#0{jC@<>or2cZ+Qdy#)APX2+%4mUu6 zyPpafl1um}p?mv5_8sg?!9+JPj1v)*qncajrw90cf713bI;@mv00u>ZnTaC7Y zX5|Kb*KDqRF?jp23JpCg^z@S>HU7K0@v7knXp6M8(Pq_# z%)JhFSkc0S|5zt)v!b#qsGppdiCs1{oYoCZfe$`V){)u>} z^XHcHiSK?WOc^7)|E6mMoy#vtKz;fPdM*lpS2JaA+HtYa4&KGH9swZfURmsuFBZIT zQ6xPkL{I?K^!=O3Y5SjAd3pV$hP@tId7@C#hF#kMv2i7-}gzW6?5^$udC2$&-rr;$S zUZ?8t*vWvk98qvgGVnBxNHQ#f1#-k70dR@R<8B{cclVc4mvtUe$ei4E_D*v^d+1So zhP~g+9rW!R=VWizY7prH(slAikrN9CpEr~8Z}}?&#)+f^3P(vudHY0Vx95bJ+!7+1 zS&|^aMC0IvzLaLJpbqTpfW%wN(a1qw3oz=ANmm4~dYQ1+-Z2I8GC}72M}g9?{{hnC-_AB0D3LhbRoyNla5%unck0(S zo9gc>zZA_FW7tP2y$I)m1e&(M#@vuqqJkAYru>-*7OV5D<{_uX4*$2}KeMcFc?{e> zM+T1K6KklB8%I|kya<3E4uh%nw|@ns9lTrvl1yrmMx<9;A#JUg%K>aJ~jVgb6-wUP06R3`CpBQx78 zy81s0G*sJ}>E(k5!#P1|)}3?ZRkDhTRTWq3|TjPAyaR5cO6Sf!7-c24I#@0DIjxuU9D@ z3*x8eh@#zXi7*JV@LwUoW$y0+Wu%@zCiSLOpj>U)M-tvzLmS(!y`A)PElwI+#rWhL zaChO%DE0H?pIcSUK>$Vt-s5vEzX)#YQyhe>#;e2VDcu%rm-qVqLB6fdYaxnR{1vV# zfz&5~X^v|hr%N{9cEz}UMG1c1kmhq1s0`S2swJhe34DEYGBLI4Um};@pi6S2+1&DX z8E$^;d5f`8S4HKL0SyJ?2S#5*;{2RJDMLLvwZ*O_e{QU%jn&=GV zLUdXTA#D^Izh&tDyQ=NRCl7YZR#jE%nct+LgR7=D)jqlT>l`aoMn@cG-d9krko-05 zQsgb#-_sw&8v8aPZ0Xum^)OQm=jMI>YH?_9Ollv)2zHxahk*{0V`&x26ux~I@hPb~ zm5sAEidL2&S!Z7bU?e(QdLPHnJ@*{TJa6w7EBLyR+xL8GHGNy{X5!Z0@;qjdM+ulh z4MHYl4ULs#jWH{Ak}Hh|UwrW|y$~8I>sy+V?sQIGK}OZbAa2>BC)mPbEh~8TmPxZ@ z_vKn&vCB0tI6+e1ALI4Z*@UM$6q=TmK>tbWrGYuH8zM8WzT-;BO02&CZ~M)!wf>2T zJv<(;`M>$`?vqP4r+>I!eP;Fk(e$Nme;H9Xjh3xVD-&%EMhWyp-OqR2ww)~tQL9E; z!jT;znX0UAg}sz#Fio9C z2D0dy=akk{NqM}aKko;UTfRAQC(VlTExbXrd|W$gZ~8J>_Bg)IsHuygsOfGY9hBhl zmN6@*aE^&|nVTL?enj0KM&13uJ`UTxR?W~nv^iBpqQUr6Exf8z54LDp$ikHQQ7V5& zg8xcEttnzsCNzCD#cS)+De=umuzq?DwP2#s?dKW|$yJaV9jTa!csVg3My<@1=ay*Z zAS58MWezzo@xK4ziFT)=^$PcFM=kkXpF_wJ^f)M$S|Oy%P_0jF}@w@ zFb;;J=SR)jhJCP!R%>gE{95yI7zJ!MTB@Xhn~gStKfe>J2vG3SbXh0|HInX^)yB1aMi0p% zqYL=Z*xuKA2p9OQ_~G^c0j?+SQ}xFoG~f^Ix@lm0@KGJ)WeQ>79g|xYijqHu$CA9s zm<|aHcrzRhMSF-E{7VQLm4Xg}M;nVp(Kp0^Exev^vr(ab&rg=5WSqCReb8 zj44DXUu$UQd4b5fo%hEV*AGYMywoYBpK84Sj{mw&Gd3$p5yy1)H@Z(VIJJ6);H!ZvaD@N)6gIZ}7ISjGceNM{Y)FQbHG77y+Il^@{pRg`aQ{x?r^mpdZ)-t_6%l8{Q zYIAn|Jj*=1l3xLV$!({Ugxv1g;Q>IH9v9EFCvXNkb6PG$qA;A)53?cr2*;cUANmV7 zOE*4~djIleDRhxYr+l1^ipuZ`2|HLAk^VRo8h~mTV&-s`!4QA`V2KV-;9dvIRI*wv zo0^Lxm?&-NCo6Ryi5XkC|p^;1VJh$QcfIZ!V^sAG0Y*Wa)`;-+2eD>I_GY6J)C2^saC zni_udxf%FATAG?i;N$`mjyr3G{TBVj<^x2d@-xmW$`ubpCD1S^$VkTZcMI=tgf1Ga z*T<-Fe%UJK1q^RmBCm-?2InmIGo%z_#djys-|Yf9`^;a$yUyW8oonp-Z{*pI`iJ!q zwuo7Y1Kg}!?QCDf6W1VZUL+$IXghxc-(ec&eRMQCcs(R75tiTQQ(;6(p_)x{T5u~b zqr+wRdHSq!JGxaMm>wfIr`A)!qd4|;Yy6evl*#QEyyob1g7B%=Rt%xeKB`gSOvU*) z5~(ZzKmeTBN!*W>s?o|i9{F(2$gSg;e4dC;ZhuxX#hU?K&oWx;ad%LZ^g!m~iTZuM zG!1H*z;Zzoa(@<{<-S^V=UipVR*VSE%A9HlXa*kzaAw8O!8fz4UsIX=w@5gI3+4Of zRL$D7m(uMv^^2cBX^hqliyV8|^)R^&kX+wMP}dB>jaQi5c?*G!r;@Jl^Dhg5g#nLN zG=ZA1YtNFQPiZw!yKH^Bfr~ULpZwNkA%UveJJl5tpp>M>l~=tO1_sk3NVF7k?m&w~ z7lbn@$~~_;0DaTX_AldIHq*mfv_neOywp1!afrdfQ8LV`Uv&N;{_i~zET)rBE9oz! zCReoQ-Fqy_=6nM~UdOozaaP_w8TGADQ4e(ZA+h-WJ<$h_R($=r8gyRCPCZ$D#5(U8 zXWz+^2IElg8(RRPHTw!Zn)!>Q@0Fk63{UXxcz2Pj`t}Kt5^W;pQ-n~2H+{2tc0lWA zDf0`z1TkN>vOb3|K64p~g22hX3BLq9cW1t(tC=&ED@4YDx+pzTv-T}yU<;`4X!v>j zT{2Tv(e^|Rn1NZ(0}Slua5=NrcN$C;22axUTda!7mFaj*_fb*Um6Xa@_Jd;qF{)$A zmF#<~e--52xw(&_26Rh0=S2M?ttg9QvzMzr#M7uXmNkcstv@(p(W!tL7VtZQtfmsI z>+dp`d1fV=`y#!TP9+Y~0CR$n0t_dX5@T*(&a(VNTIV!H&#p~Y-3B=+N#9PdSD;|c zrL?ji?)6?MdpvwH!7;IN5od~e#==1e>H*D49Q6S~6D0YluFx5?0s^&>;L!-6>m^$|8Yu_n(Jr|bJ%0TLnFa=<7 z6o=BSjsqI6=T45HNtN9VfA#C>h(mU^qq)rnfLsZ{C|0HQHGgTY(y` zNPUpnXIHaFDs%+Py`vo#Mvmpa6Z-@-NmI|86;YMiH_x6@y>LA7gK?Ifx#+MF*3?`? zwO=F2d9B^-qWR}Oma>r}tGSXI(x1{F+{5AA1?+Y$P-ZI9GvTmk0HriT+MfG{`1IyP zf@9YAcE!+Rc zUrZpjd12`{zYie&p_Sz1tT128J-;AQO6;_tCY?XLPHt0ra+0Aj zN#JtCMM7wy{{uX_KVGHzcAcPdprDoL4MgZ|Nb&<=+JSGKosFukVB&l&cu)ay4w$iZ z)kscXjm8SgMlN6>wBQ3%sJL|0NBk_@RpSEL&XCkf941}g0vH0}g6dh5pVlv)lpzFv z;l2h?^HPCIwdhVid0D=%n9O`gK$fC&&?Rw_|7F(yA>;s}c=ahx-5(ef7g`g1L=Tr@5?LlN<-T`Vv+dN}Q|2 zBe;FB)b+)GkN01#C3lQMV7uV_{}FqCO{WEPNp%3?ja2sM9!WnQPB4 zdo}s+0tZk6oQ^PC+lX9Qv32)jdr-2^%A>nDn%)*|iqQx;cyHn2`6p&_tgF@i0y+^< zy@S3>z22R(@q^_`m*^tMW`+cih|p)^RoeoEH?F5-$#|{4=Yb)bg}F>Z;!t~QqCHdX z((1z=l&W^O7uJL931t22)_MT(ZayCH$tt}3@;ve)6rarHsKV2j%5-FF>GJxoGMD3K&7ki_76aL{9Nbv2+Fv!3h0`hqKZJWxv7F#iuV`dwO3i-e`=Jq!lVLuRHO6NdZE3Zz z6gAnfk8ooW3HE|ZluiS*bncWakDUBeI(e(AYUsm8?x|F^DL1{q)3F)c#_t@i)N}IS ziA0&x<(f4Ap_2NMTmh~ASHvM07m~|ttUU6YjauL%57)pr&o2}4<4N;cW;ScDPPI0ur3g@CRkE1GlN!v1I&j2(LI|%RZ7Q{Zp_2{ zAB{70p}-Fo9wD#Sk2FJzMAB-WO4(&4K}X<}=x=TXHuJLA)Ls~IH|%Dog{*i)U#c;G zalLQMoX@7L7sY`A#_uI7A?2fa?C<}H5$A_x{Q2Ax1J9tUcQjR{I=7rv*AaRWG4t?Q ziRv-`OLHt_1>vvcHwH-HQkmEoTu6fE~?$7ogN^WQio@w4q2`?mB z+#j6#jl`?U+>psV5%b}8VU&C081wY?>Up8`a)*Znte-OaH*wsG=KsvW+;AEC_BR{V zhl#RE#<9y1y%rB$1a@)`7yhbX;YUB~^nZK)A#P}@KPg)a|22vHeredH{Z2=NSy-3) z!xPHQd@mIB>C3ZnCG&nCuzVIr>)pxL+rO)>W$jId~Ne4Bkq>H1FPm?Y8Cd`WHsY?=m!^;oN3g1vudiA z3z2~o#LHe^KU+0)TodE;?dDj0^}0RLBGhGu=1>(x#q)uB2A)2?3?N#874QiA%YFlR zFU%+GIWGc%^G+==$=cWsPn~7@hXSj6@@}8-9vJkN<#sV5DJ{athj(ss`N8RHEN)() z0wk&(3d%?_IsR{8w@7%{HTXBw6nGF%gN(4Vw41z3AVMvI-3Fbfc57*Z!n_1?AxIXl zxkU=7a(L)LT81s+ZMQkIn+{z~meotF5yWKXe6vNL@&@jU5V%}a8y;e{am-1kRZvo0wm$GpzgC_v2dr?Ki^Pprs2N}Cg5 zP)k%8_$>thSERhkz26(-Z(A!@F=$?=3PQUm=Sg?wvc|M~F>4*t(D%e6f+AEX;=oFh z$jHK=n9>IesQs582?MwtmazwdjF-T|GZrYJzkH7rrPg$+#VrhU+`0~M%N836NCD^U z0-c|4;mwaO*3Q`eO0%Qe!E(4RBqF$gX9BuTq6D-gEI@8# zI4HjS{oBN6r%3jJQB-r${(ScCQ-DmZ_ihoj3^OBXC)7{FA^vn=wA*#-b)sK>=pNv( znf0?C%8M`C%CYe91pYR%X&7)9{4-CM9n#wHnxJo`Ym0%S;Be=q2<^Dt61P=}hYcS> zqf2zKMzVGhnk@hVj<8zNek6F;|E?&{z>h*10jt6{hI2l5FZ#lcn|Zl^t$BUZp<8$` zk1wek;;G&|yU(}v=R0i-e}u8bcGCf!T9i>Ne1A5BuPL@N8cAy3j>e4up z+)vG7M*CQCQ~>aQvz?-VMwy>v!DyKm3mkgJ{y|cBp6-AzS>Uz%z9Gl3`GQ7;>5yPN zhEE2LyJX*7kQ>fR%u?fuCzV;YEa3pu&_Xg_nTn>g&Z<@Fn9t1b+W8qRR zw0ifbi89$R*tjOh!ULIpRO&kINS3i>1eZEJn%}I+eBl=& zJLKPavr9EJk%a12FFMU!EF?+pyYA&d7a5Co-02~HAF7FeMfD3Rl~4bp<>3V7-9wq1 zp?rtdS#AqrH|~qDfJ(<)Trv{;$O2yy0y?mzetU#^65m>u`#iT3ujK>MaN#M&p(t1Z z4GyLjZJjX0p^KY(Sn{MLJ6_W*$1{f6ijito&=?Rx@p3UNQ=|2}sjb1i12b;U8M$+W zYRAatnnOA(oJkoS5Q0fP`3ZErfnT=f0UU5TSf&Cc;r9MJUk)$)+U8tlJ*6vGuuCZ! zC3uTd{q%Rbmm-*e3vt9)b{)tiezV~7B;rwrnQ293)xjuGO;1t6rI>(BDKpt@R&7)Y z(*+&ldsJW0_TXwoMNc-HB%;Qr^(Ctu9K&y@uX+5QXs-L>_HQk*%a1|^J_~#Nk{GbJ zvVP}M=Qko(Q@4nao*j63nAQK~0kN5O2J#IVdN?BX`|$@cZO5(`u1-zde{Qav;qrG~ zwS^xoC)G6iaX)0YokkPiC#AW-s$jw#Dq@g129mLz^zfTm;S|X+ zb3zaN(Nuho zwrzmB!AL=zAdpHJfQ2T#uih4RFg~wdLC%8K^5N9(!W<=3coC21vYl>pJnD-y_(`NQ z3h692tT?tARd)1OJI4B~$I(@^4NbU@D&{@ZDHYkg(b*w|`gM`!RY?-DHgAo@@y(+k zg|l+|3nLTpMKenPI`szMg7^|L#3f7TV}rDS=#7sY?Divh!!X?!IXXFq)&-$i{CVb^ zgv^0LAoxtU3s94C6kgPh8Q0Ln^BH4Slao6h2k08Gmn>Zt1HS7M^M7SbNAHmF@ad3y z*$;)+Yziyy&j_h8PkQnfWjXvD=W)67JwnN6_FACm0v(6T)ngSa_R-z z8D7cF7D!4h+}rM`Ztx_)xDAc$I(Own9&$Z+!okv4GtqlToLIkzSGh>bwshh$g=kU z92lTGr(KSGJ5W&aAXKa5U7!P$+aBDZT^0 zksUs9<#!aV&YoT8!>nE*8WuIm?PL;=m%ZqM3~uKn+(y@4?;=+HTJDBwfrLKWyGoW| z$4MLabJ$KIbSeqI(4;e~wfXqUN8#~THyu1b-yE5?fmFgaQjH(cCfMKJHk`u zv($o{_DScv+%<6K`f5I^Z|o-6KWbKtW3E;HK7O*RKblT2{swrcciNF3m06jU(-@ep z^6H-W=^v}u`N?DVkJ*opD*($(mrdJ{o>!XFOYP689e>u<&MPm+@tU-o;HE9-CjQxlbD$`XrjT2K$%4PB)UtvBZRR5TYW@)cTx`G@VfSl zWb?Vdza9JgE*h@34Ww3|d&J|UU*lk(t?e#PFCjkPyT2~4u+6qhl9fYg!8hJI2NA75 zS3D|tUMiSs$ImjhWjn&mz#9!G3Tmazo4%yS%-?pZi_c_}R541XVt6UyT1xue%HhE8 zHX-RcOOG3FM$I`{mcr&mQHo27YSIoC66-#`-z?(JT^{PgD~fj_IDKjXZ_dg>{B40u zpn6eNGo~pAf1Z587Bfff6RzG!iXKUL4FZM;Yq%&BzPTt0g8`APmY|mv*4Q8wU_{V! zvAo|?%JRmvvLxsUrl=dI&)|IpiL;~yivHaObQG<1fiQoY-uDg8nzu^h(U+p7lWD2dO!&r1UEJtMd0vEr()&WC8bNGQZvqN;lEO-al=XI~VO^oGx*L z&zJy#%wY+N(vAJz%j!10A|>2a)lvpI)z#fNVtc?5}T86gtO_hA9_3DPUO9c{MRX-K-A&9-=U;j1}a~f`YeDS$s zoCxwdyH?qgKhT324dVwF={`v4?uJ_m6urni5Z4!^&@#wMZMX1?e|u~{XSRE#(th6j zU|%XYZgujh&T{LYll{s3_C;4%F{{F|U~Z_*`@_ItYhQkNyFgq+wf7fm>e#s=dGVBM zNTg1Go$4_;ykx`^LHCRwzby(=ubQlb<7wJcSiz|E_3 z12e}|aPb`#PU$UpeEl%d{UQ1$VmiFq2-AuE_vqakkC;bVuuFsru}nq4w_{pGphn9v zaPDNSoad$a<_L2JH_w2qs|plV#{G~UZqrjB0`mOmwL z7YQ;)o;`D#XJS=JVc1zV#469Q!ubxbH5JDN_BgG*++M?)(V&0vZ_uJgSZ2{vBKQrx z6MpeZe|*}IRf&21V((HvBL>V;G&R(5%ma36m+x$RzTNJ3&}x4?nswEvh=`0ZHRGhE z=VmkRH1fYNb|(-45~$rWNDVS@=TF$?jjPj_9~daQk16oF0_PD$hRDnSy*VF#}XKlQ7#`(t_Ljx8>)ZoR;O$RQl&u$bx@J}gH z@=&nVz7m5?UdA_L_h*xne-Eu)jr-hXr$wgcz|Tt%JJ+-FRFizvd%~-JDWB*F%%CqAUYObHV}dPHHNofW8TYN3G0^@1;e1zE?rGo*6b9ytQlY+pF}fKn zFA=TA-@;J9Mniy#$x!Z}i9kdNhErtl^PM6KJb96&>Aa}wU_1bWkt(Z2^0Jtqc20{X zU8+53wr=NQnDBj4>@iAmm)2$r7CT!lplcA-?yh`k5N5`yJmWd_OhA%@S*dDLDUdmA zQ|`~=mO8INs;DZOHtbT=j5$F9yO{%tigLb}3NC0dPP9nIKuu&+V#&uJ@9#{Ouim(x zP~U34h=R(YyL1|WNuHQ@HfI~d9qdR9zyAT4yj`>9;+>H4*@3B7OQ$=nKYu6a6IFlR>+a# z@jX$#y)=Vv8vv)LN7LbrwqnLefR~PG)9nz`xw1S^QSC0>}$cs2@Y9%6$NIip~LOHhrUOk?51pGNc zKK(dkEEKWMSG(=8A^bJL@s#Srr>*VgO=;LZHN&9hRd1^1T$Ycfm2|#6dyj3n00dO?S>(DX0_{TRL3@29aXB>kxw8{iPg?Q2dTY_$hI;@dM6B7o78! zoEKz)q*BeCI|^7nM;Y|xiO1!BpT2k~+81)x!zvb;AvNcWC>z_L=WS}kT6^CLxi(-dZGk2NNJP%HHblIg7gW~ceS{V5 z%Kk;mKYgzL7qefV{517@X@zCPla5rS~<5MH`|8$hagLnew!w#&bd` z<7ksIas5Xfi5y3yK_Si!Tu5oTIRWR-VHqELvLuU6Ot7w^(K;C(oX29PC6ScGX77{0 z0c7~GhX5;J>jxU|+DgZh`eN}@P}Z)fde+ab_p9ik6CL{8l>qwDcCOQEzsMDQDBhy5uEV5gn;Jd&nSsKy=zkIrw2)MNF1&cl0xdj)5`LId zRggTIT;th?%FeDHdOLB?(b=-VE>G)MI#{3#SQ)6^``E@X+70j{DkA`~fPzO+AOkhh z2k&NIN`MN{6`}*Hp$iicxDN5&eI@0*B>`1iOM?l3I>pB)OlLOx@d&d-x}R=k*Bx32 z1{i&(jP1_WO!dI#<=w2j>k{?vxy^a6Oh7k89~^A5+L#~1dg^F*xB`>b;y<0K)c-Qa zJBLgmlkm5VBs#UVYg@FE#>9Fi(OB!n~7E- zTI#rEVbn}gVBpNgOK(jKpL+0fKQ%N@CJQ&~q;oWf?+Sc-{BCXLyB%56*()Iu!6siC zbZW#Rn$F6UbsrD_OlKDI9;G?z7M3uu$g6z*??JQmYNQ<8@94K3qh#K_ z^|j0SUG6cv(+1ur&Ss|*j@=*{`jWks2eP40B<#rDd%2+oY`?YYvw`y6e}2|~aHW4y zM%2y>NndYJkfs_D%;$tM_9r}4zsf#$!OZ`yfK|})W{ZgN3`s$g&a_ybhB%7r*N(7Q z<=4NSk7yxYvyB6m(Z=@??%Zf78r55DC6TpTV(rp&;wQ7+zg@#uZ5b3$?4hY&^L(H?p58bE-FBM5AJk6HI^2nkwx-Wo*Pk>zl83zyPFB0OYme z_yZ|rT_bnR(pR-uiKWyyPlK@0q%&7+;P-zJopyelt2T25_O8A0Huk=C%Lv2-lY_*u zP*7jsTEg~t#fwX+wdobLcBg{RYP?EKB{78OPwA1!u>@)!5I~Qk0Z;8o#?N3M-`Pt< zw@ZDj3goXfWHB!>6d>23=4Nrc%JNrWTtku^yFn@5d*N7;#!8QZld-?M-C!_Q(T<)M zzP;{y`)Zg$lEDOg+W{O&=432CSO`)3Gw^;pIX+?P5vICu;0|~gU&lhW0r=wC=i2;M}yAuR_@`57kE>^#b>3#ls?}67upWWoItqYglSOq1V(f_h$e~!PAUf+G& z#sD{T=@Ul(6%d8}M_5sudJyM#q_udiddFs*zj8)|_RP-wz?y-i;tt``6Wb11&r>E0 z3PaFVX7Ar(r5>WMHE>RX^`kn`z9mR5#+-s6zYk_g_mk4hy~|f7{U*O|Fn%nJTN{_J zrD|XE3NtgQDRI2;$&%G+^YX}@WS(@%w5U~$yKN}iTZDLE6XYqfgx3c}?ju&#hu=D38cvoOKgVxdD*cYM*J(>lW z&ek`QuKk1I(1%2SKF<&m2#>QPRg+CC^LruDnLpHwG!+A-s}^(9hEqz;!5CXyxyaQ3 z0CXmRzHwvy!q&gZs)S3UK%lgFYb**?+~mxz0XWyadsaCETUnN);)xO3@^W&RwO*~G zzudP!+gdpwkXXlI){+dU(8FQQm3NX3qq-F) z#x72LGJ@3OBb=&ERs^I-LT8Z~?MyoC2cSB_F;4Ibfya#9@VHpU(g~L7qZN(fgtVr? zG_}w|fRQ-ot}FDs+2?53mlK;A+<_>m?cSUPt$oP&JH=Ipg5@pCLG{A|mNvx@(6Vl- zx^_F?E2uSwgXyB^O8?n>Im82c{5o~aiT~EIILoeKk=H5Gq(WV*r0@&0(8-KW z9@;IE9YTh~saOU89A&O`sWx<~p6@s;4|mfL)DiFJhIFRXAWNYpyWpT z&)Ad7Z>Uj!J-G!L61cNabMf)iz`9!VqVrez8?smniDnc?`vb?M;Bf8+m#u!*1719z zVh1sDI+{)~Ri*gubauVHEf?{fhgOvnF^KjeKp783f+V{vpp@=a_X_n>{xLDJXayok zFI(YM!~~77QI{^cW*rj}HWXdL+sdomVXda#*(`J0cg3O2?FHP7$Qh@&6=p%GgWLJ2 z!FvPnp{3^@D~9GR3d`0SG|QHbzFCf>PI^eib1)j|8llmnCg6zOpa6S4zyDhxfjz-% zQ@a06l6DhVK^|F5ziGIma1Ty=V}Hv`s@S1jt$6RvrZV5eBS)>%8>nt8YVwCWh!`Po zM6mGnhHom~8x>L7=Hf$GHS|4lH{W{-R7RF9=!;be5+f96>w2*WfVLSt@NM#? z*tGj&0`C$JJ?X2a#mom1r?XJKh`SEgPn1#O3(Cb=8a#dc@N=wqf zYZV#h2J!wXq6iwD5gd!?+}fCzv3_&;)j3Ht)OMf{>4TNrpL5q~Y`k0U79wk?|K)y* zzJ5cNDa^O47u^nRS8WkpktF1z?Iu9l@fv#YtRJ`QcExw*UUnL-U28L^zUG_o^P)50 z%mt1wIIV1>J<`ce`eU{}{)c(-ziN$L*FJAX0r9(; zsU9c;n6R(_n6q{)Hq9NYyy6(6vlg| zr2Kfh!>0}E3z3sMlF{H80z;)#FZ$0xddit5wQ&;TF!&wi!vBd&z3WG{jKn6EOq|Fa zXTx8m1Zd8`*IQh9ko>wxXgt`Sov%O+4_G|Ud)!e<*4A$@8gFzTV)W(4CIAl-0)QqFdtt zS>90z&ik_?8mqrHiZ9MT6|s)_Hnu4=50duU?xy`euFm@}$@lHUpqL|bpq5)yOmUQ2 z84jG4faSoIBXd>`+}ku~jt~`58_onrj?CP9<*KyYid)m%TT9Z~#58xMgabMSc zp6~Z@T;NV#nsxcP$+RkXEa*nj-B5k7_0sZf&qRS8gQn91xjWw4qdyoC;UfKQC#Bn0 z;%F*jO20P6@&~gvkvj#VpLXx8^+R^p8AW6M;jCG6*vu7V*OrB6^t6?WUOZB*p`DO} zF|?)+?cre=%ReBj)cPG|Lwl@##9!N-Q z$1(^>Mh$v0YxwIHeGhp*O}m;Y&}813!Fa@(YG`LdFHDo~s(-R7n|fGtEuuc9SGY&W z0)`o5lGlQ8woQYXZtRO8q+hys_0Z?eGdObNRq9W+Z*2SrkPY#N_qORd6yfJ>S+l#A zX+f?BX3pU&jA+0zgD1OiCmv4ybjZUW$D%7FQkqKRWp_eOcIHl9X3)~m?6#0#W-;V3 z)%EgHrTK*D8P$SM;D@Kn{;S(X=5iB8oeq+ck{f;P%FBDd5kVyvi7xyRIkjGzXQ@X(O?uYnrQQ}uX2J!-02O^d~+ zD^k0WH$s3Zob^9|-&5H=0$`*l?)6o+wPk$g7y{w)9aVL-{Pz(gM2cpb;$5v3-`z6o z7$K6iq}l}|jgvZC-IdR?r8kjPkpMIzsBQ4$w)pUb&$LN1#SR76>;+^ioc&B{+}QZy zL-{86#Ts7SkHI`&j*;1~Eh1;>2`Iu@q;a82j6CYge$h zDg{zsd{!b)Z#9^$N%P(drn%;fPyhyw;P7@pPS;|q%2t2NboJ%(k(%^0iPF?+@XUAID@l`QYdc@-ZG5R%IdcKo*srHLoj{jDWN2k%rDlUrZ1_}p(x#3SyP zrB#*3f)`CP^HAmt(tPug_vD?ov)fsgYaG5m5IRFFWqA1%_^O2Dzox{c0E*G6R#>RGwn%gx_Tk z-G(L<6h9tHldKcp;W-$p#OE>rA4xo%=OSF73F*Yg1;_L(ZTSRxw&KnfD_q>N+8_tV zOi-%sjT7n%X2FQ=3xS8!k`NnEnBwg@wo_w+ttY?NR{iv2t8zYn3b_-zY=iwVwm^bj&)A?dGB_+0Y>fwSa=$b*i}U!Tojo#r=+QJvQQRPwtqS4R3D4b+)f4 z@pzEGq3ZZ=rwU=j^Xq}HV!-+@rv*Z8E6a&12^>^xoON&x=OBFqwBU-6dwMAe3t>bDK$KMITo22x(er>j^meC7} z9Q7}t3(I(Rc|!$&Se-HAN0 zPOafDDVY7hGedoa-b2LTJR`lPmEGQdfQeU4SMRMnJG)DAm)6$7J6LTdjqsE)&Q?0VMrm$U8U5Xe3ao$jps5<>W|_(}o;O{dD8r&CNv`<%t)y5Va?>V| zJG!JKTqb7$adqG|ZH&D9O$P+m!QL+0pw3^Sja_skGPp(jbUl95%L9#uShrqh7<7zA6+Ka47NNchboZ{aCnVTd_3GakCfrhr@hv4;zr z`gJmY@i^4Hv^KzaJyb|+>w_cw8tb;~@fLPTox)O(MQs z_ajP-;8Q7{3 zkIpZJGJxNUTbmBt{i2|zd%=gNrNw)BZq{0*!z?)!i(PbfpU#3@N222mfe;}Q-JLgP z1=hOg%6se>rnBkndrapEG4fnOWD>1aI2EZWnTd;-`?MX@Q|Mq8*Jnms9Zlre)8@x) zUK`y~b%`G%+GxHm5uF3Ur5wBL^R#c%ox9ZBv@(V(%v##!WoVkbR6h?Aq%{IC*D#3} ztA)bL6usVsEo?6zMwG?rkHX2 z0_m|M_U8&W;Jr_zXKK%W9Q7>zJjX0oY1`-*3F;kYkKDYGy*__@{_B3Wwqe0fids}s zMytJ+c>RtMQ^I0r)(~2ONOGt<7V$bf5B{W+&Jsx0y4Ez3H$sAlb{H3>$)FQZ;jACH znWG?YUr)oI`_DxzL{Y#I&O_#yhQw{nSF!4XY*ULxf1$vWQ=Um z>tzmt=LH}i$Y(2uVreFc`$g5@73kiy0q&Dw*kYL>I_}OGxhxa|5XNMoVsI4qw&mHh zGAU6X0H$>~+_a$xh8a!bvWq?cY|9FF0Sa2jc-{Hd2GC!9<6o8P^W zndALJIPmb-L``6)HO9hOmP~ocMP5!*Vwtuy5rA3x{<$CMVhy{if|Hfd()`6&>q+;% z_9hvFvE2fIkzy<)uPKLTw+_W`?U#Jjvn>G~(db{ie6*)aVRdLRbJG64mXNgJl5pMn zK#*`fqKy>YivUW%H1&h;u`#KW!gcA@bA}BE)tg|9q%_uDEyyyvyEh_qpWVw+Zrj=A zJA0su_vFF?Xqsb6An2~TH<3h!UgIEcAPv(r@z;jZXvHk<6veDNDI7pW=)F~krV#Vg zp8~H>RfdQa#PAh)ZqhP=iC0Fsn;E){{OPj8(Gy^d745nl@38{W1FV z7JjZdX2IP28>o2fQcLv5^&If8bRzpFy#(QwcVlmMzeG+v5w2gh7U423s%-X=C;zH! zy)+W2Y#>zU{LGqzJL4@@TMLya%>Yf?ii-dTxan!m2Pd&C``dvjLzfvK7x(g6pV1?AvXPu`jc&EvTb4;Ty zukyHN>>a#PyyD|t@)T1P{n6?P6W$KV5*{$3%k?G1cXdTxq?qb<>2q>t~6=A>~u z-pQnz`1ngLGr;JhkEK?HbM_B%dfrYd{gTjS>eaA57=UZMfOsVZC%8{nI~MbaXTt$( zx^BVLv$GG8<3$lw^!qu^ug{{UK3!Yf-o~uaJJinwss-~MiL?|&J#`=fCOR7FXy?y1 zA{1T(Q@ZMPZa&}*?^c-0-1XWzG5t;fjY%k1D+L@X?wokO{&mVhZ-gbI{s!c4?Ql)c_IB80iR#tSA(I>_t~L-j z@X)aB^k9PhzE?cDbPYXV1w8{QpMpbgY(&vWGUZdl1oS<9Bu%3=Eb`c(Zr&uzztpd& zuzm?YJNLMCc(|fGC}CkxZQZn>L6x)9!cSZtUE{n`aTuiL4)$) zZz5LqjTX{tXiZ+4qaMRJe6G*vj#O7W?KJM0eE=g_Yw?H4y{T82!gx~#d*g*XqSK4+By$ET75fqyGwOpkgWD>O{BW|mf5AQ;h&E6h1+FS z83M#5x7=Ef^t7h0&z@jjyC@{*uIJ~C*1a6TfJ>e7FHc|(%rnG$Xn`4+AtBM8uH)W( z?q8C}G>X&YlWSv>9=re0_XC&p!!r?$m{w@J2JEm~tM^2x<@9`>ZDN3SrDyv?`}Ew>t0c`n^Z&Vjhj-DX%Y`H`q~6fZGtzG zIEXvbTVAX5!-NNan5dfaqla}{V@(Z;!=M+585Y+cVL#}e-s#9*jw=>YrYGkRIq?(a zd~Z%AJiI(SOFxDlN~XtL-rUiRcjpe&31DUzBa};^Jn@d>_H7NVA?^j1J!N-?wZV-g zkAF7PT_9H)ysQq8kaiDDIO7zp{3*S$E)8|`&14JlROUNU6LBzd*V1_f>tS3GWb;;D zZLL`Lmz;1&J4-$1xO*z&CD%Avll<$i9i3UhC%up@0|%h%gSQe5Hyu;)x%3l*JpTXm zo(CyS`H$(<{&`U|H#2@xaOD!l<2VCrAWVZvhX@%)=_yyF)i;sSqXp%u7vA7 zl3BmhxM_DK$g78&`s-ss#nSbM7l@y)wR%sDas1F4!>k+D8;mg84YzjaW4tFR$Dg9v zncpu|`?1~jHKVg*aze3K#pocVq*07coXt;Lzc6;|7wq~}AG?l4M1cMCTONsbV*8x2 zWBE4=r<|64seFruR| znH?+`_}lmoThfobOE-Ej+Qw}>r>t0P!>9}8J2UvF{nH1OQBu`GnFXJn!k3EHXVKMn z{VFKBTdX`m2E|^wvLSXx}b%D_3~8@wKF}$s$Is0-M5gHz$&u zRwmwM@WwFN@MhFi1!dJ+b1S7D++9`;bq*}mwRSJ%SG7%Y_nZ3bu1ScBX>;Fufx#IM)DG4}6HTo*e<<>|GN1oSE}{F%P5iDiPof zghm{MXKT{q_I3_?%H?a#{LKPniw%ldq%j0ybR-097y~f9$a4C{5s(776l3RK4PBd& zPkw*D9mK4vEB*s)(2N?H@Y1S=c&aXeMC`hQc5u78LUrd%SZLG_p1zKRc;!I^O3J5B|}_no}ODU zl7%>zDFw~4a*llPy|I>!^vuH2F-z>;t{RC88qoz-A_No}eC`H14bWE2dJzQTvLDpb zV5TSRh^@{`FW!@DY?0jvlNO##XV6bGPX0E4WR6e2!PV`Xq>m(PAKqbURwnTPlUPX^F)StvJdC9;>KmWeY&~a&FE`RoZWXc8#FDS7YR$}urR@{ zOPh_5*iM;&nobXEgB*j>V_+N@q?kVt;&PhrBy|L+26r2WZIu(=9LC!V+XMr zfX>bfT8w#w7PKqFtId|*@EL<*S2H33K9mEtcz+pQo$^_OXl(r51-F-Tu|ut2F}~=%?-a@*8TH< zlke{GXG6PLGxzx+MbUU%RM?5uU2j`G*$k^VZqVPDbGg0$oJXivofEO zWX(S<89U=#zUe8Zc+_sr>wGD)Yb4gp3J3|O=8c05>e}gXZ-*4O-Rizv=4-D0DTQLM zY|en7iIZYCvwc6XE$RP!>=gWIsPQ>tPwUr*KV@StYh0ZxT6f#@z2TW6q4+#+gbw85 zWABRK9V#s*@!Vw85-CacMzpn`E~$+yl&QoBTlH)4DR(QZ%ZD7(<7czhoyoVbZ~t^U zhkpE|2k@)$E*PlOM37#aXD;cz`$b&G67>Qvd%<;w(|FRMdk#9$2# zT1Y)_St*cv_*%^u3}9B)gtrgAFqmA!A6XYnn*3S4=$9cf7*#rSNR{Tew z6VA0i{{b{^nyh9Zcbd-UjD%Y2#(>jyqbN4MtY4bJUR(;ZVUt^T-}ea#N_@)n)@&1= zPZ?aGhPwV~l#v0YxAQGWP#eQES|?sb$1w;7J(qf#ZBW<>rv+a}%n!8;dF4nX{LF61 z3;DCNPv5OdX?2PS^;mEJopvz%q~rz;o+>eEC?;W~cB$)Mb1Y(g+%qj0nv<~4t>=A=IlSflhLP-tL!B1 z=HXIs$MiTHYtH?1$sZ7+KCKD`I75W6 zw~#S%ki%$_!>6_ZuK}NK_ExVyV^O$C_`b=7_gT3voNY50pz2x50ncYkhdh2KN8#mE zNyqaaf$EuCX8HSuXm-6o@lV}128I6iXrR0P0|=O3oF(jjZ5ZF+axv|{_BQz;O`5^% z^?OTB6I8TINr2PW7^p2FW@`=!;D{bZyv7rDp;nPFmxQq?@XzwY>J8-MMKRNCDg*WT z7&l-fODMY>T(zYg2%jL#TgUzbbN(s3a)MKGEmvFq+ za$&7<@79T+7xmN5BOt9|!tr+1u6+Da`xLMD(aXtierxzIPO*aZYrF5Ouc_pJKCZP% zm{MzblcX|9vw;#N-&gq$;2QMP|6LX5R_}-u#p(Fnqa+$n@g&+P2X8ZB#okNF0*Xb+oS=R22$P&&Qs?RsTJexf>`P|KWDD3m*s zGhxyqwD6r?ViAl5K-TOPcW1APKcsHltFQ5j<))Mon#68@VWW0L0PO- zP+D=VLmj0|4n#(Jk;%_6+yoYrG=LU}!&7VN)-I+px`+;}&r+ee4gdq``GoUZcM2FQ z&UfZ^{+u0|V+=e)M3%$O&gXQ@8pLix_En$b!mc)O?P~0mWqf{XfZBR;wAOcJ=DKa{ zx;wlmJSV(RYZc>~dd=ZM znUYm$oIizfH&7HTm*W<6oRNx8V~v4E^?n>^&BSry{7yzF<#9w)Bc zzqdul=BlS9I*c&vnBepN0~>pve`$o&jrLo82a?ELS-YMc`Ej1;IdyVFGc&tI!+NcZ zF$*L&jW2x+Ea_-&(56DhFUZ|;eNMFZPXsANR$4V+tJct%jBuDGCItYGED|U^K9kQx zTU(kAfv1u%W>F)inIZGGUV|Q!--b88!#@8=ZC|2AqmdwQ_B!gHw(_h0G@fJC9$;Br z{gNIkxJ-Y)&2ULEu2$@O%Zv1EVzFLBHJ$Y;5ex+OFhhm4%v{}0>6iX4tgpFPy*l!X5f_z96a84zQ1>)XA3$im4r4^97Y^`#M4d)O+RPTsck);H*?tO$ zsiS-q_mZ|PyTV$sUUVLpC;z;s)}gcEU7DBD+tK(i{UAsn?y0UFCNz7Imy?i4n`ZOXt96l&n{VcWPMOS0s$#{2n-sF`k zlDYrhtLj-g$1Yq<^RrhTEGEdGMG*n3TPtTt+j~8)rA$~8k z=gKWbJS?tve73Yz|K|+Ol^MWZACzMnh_!KQ%i#CYkLG5TS{|Ee0T{`Rhw{}=yDyDP{bnU!{)c{|nHO!f5*B|ZnA zxW|)}PYx~_7d;kk-qrk)4~35K5k6q7d5S~EI2b7(tT+Tr~tGgw?( zf${wVs5wRY0nvIcM)Eui=(KxpgiW??^p=;F)M9kkxVZ^8v*HCqF4aSN%7-gRI8LF; z6dVJ9!045%1qn{UrDleX{}G)sap zWe%@fNIz7Vmt5^+0dz)(GYdg7vzM24db?%5j6~PHY-{I*r0ge$ux2Ztte%+0P{#VS zFAfHl0{GpocVhnCg>jJ5koS}I)1dc;kUWYaM&{|-^MpI?$W=QqaGQOyuGmFxODzqW z2SaN~N<*V|Qs-)XD98L!|AoGNd9LtEBKGed|)&g^G!?Zg#gXjTsMeAugglEa1yl3)0VW zvj}P}4^h@!Bo3Df>IY0RC1K7UIIW-65l`dV3^_Pn_&$-IXRLHr(CzzFCB6O}UB&Q897 z8q|no9Lr7RDJ4ZqRFvm+)UU(o@d)H9!H4<+ry^v5(hI+7fO=@iGN9R~VdHhQ>AJ0% zGDhG|j*c#Z+JK3eq~Ap>eoE-zc$OtTP-c$)Y|qK5 z>4&+ya;iejKs`<56!r@*uMy}swO6QZJ8U~sc;(YW|EDe|f}Plr1N>CJKqs1CqLNsA z+gbEoOd6dfX#Jrye*yHW32asZJMmbSG5pB>L?{?BKOm!-0l?veg~(RD z9A-0K^X9Cnc)6OGUJv)1+NL5TiNfd2x3C~G{V$pMr%ZiNf{*oo05!|2YyX%SFS_S# z3{7yZpzhYjf0{HRtJ~b)d;&Z!Y(I7hk---xrS}woM!8FGC1O>Vh{&bAVmRDJqFB3TJ%t?Qu@1tb3rm@4y6eIu7xWk%d-nN zEsZ3!c5o~`2q%Lv7kcag@iZ0aF*R_ufh}kNH>a}xZpdVL!_qc>>gK@GG$CM`|Lv4L z8kfwGACB)w=*-o^cd|7+1V>BB(ttV^Q+(U9Nb#mdejQ&?uOX&ro-EB+NFpc?y8BFc zdZL6g=e>@uDmaM~P`r{NT1SV%P5v4$J8WP`3@(x`iR6Z8whu@O{HQVgNbJ`UNz=k; z+UAze^-}|ig_eu=%CUbz|8J?<`J`{xU&Ta;;f_0~7tC1D5%l-q zlhsUMICoL}uN%|ARzU+H>gji}&6_loJ%>*ukoxEFl%6VW>RI`v?vB4;Zz&qLh<;?R zBd7i~&ai{9p-<~J=!d|doH(4RrvoID1E^kqyi`jwY0>6)FZ~s@m=Qfiy}cS*Idhvb z7WKw@==4Qq6i6tXOUW4N-rlX<+TFFqOU_Nz4oSJU zGELKt8w4YgG zs5=6DUclXTB^a$|SoqCp+z4wNcX5r`&*@Svk&T%0XsI_rrBVS$U4kD`{{!S`psN}S zt4^;6_@+_rxHigKeSf*%nL@6hI$s09V{*%4!P(|)lK>(mKQd^J?Zm6g*==XijO%|l*v#h5Xp zf6R+M!!R>Ig#*+K+y|cvsL^?-XMNuIE=l(Ae_4IEQeyIr|Cwn>3Tx;mq8h21*V+n~ zBZroPX=8vW-ubZ`JCf~I?H7Kmy=lm|au1!mJ7s}&DmHd3>k;%}F?6}1xKmo7*CqPs zQcPqyfziSEI#7f&w0i~-{gW#3xzX!}nfpLyxSaXLsxc_#ker}s6Vm@ieY*>mLa`L^ z7tglZBcwsg?%J}ygWsHs(PN-kQf}Y)GPY7%R-Ny7zxsEd`JK+6{_iiR1wHvTGGggI zi%uP0>91zss3y%zMJ?TTkbkO{73$;h`o_TZSPI2j?(&%I^oJdA6Sg`Gr{G?#!e_VA z;cGoD1))Rpdj`q0P#!v!dXEfx0_k=H5&j;%`7{eo&0#h+uOrg&R0py%qR zq>%F~?+&Wob><%j3;2IWJs&5~z#*@FbRxRP)@%_sdhG^3-^{>Zs*@KnNWs)cbJhMo z`Tf6eP8c7aL02~z9UJCy~!Ae>O;zr&beJH!=aybj{ zyFJSsXJZ`!)yh8mo-Ra74O|Uj7cHE+XoR&Tcg1jzT^(P1tYD{ae`TU<^n)i(gT>d1 zVpEwmGhq7956%$UdEpHdU7Z?|ya_$&GPkEQO>6SW5;?g~R|CKXG+(K=^R zmK_f1slB{y052;JJ7y=e5PHoVLI{-HQ(s>84D7y(+mvZJEn_@ z@el}~l(rVc&9!vG7Tqbk<+<_XkV2_>X-Qzs0|brSp({tJ(iu#+JXw*S)JFH1c>=|c zpVAH&s%zihXKQCH9cDfqNy>waWZfE#fLq#ca%G<{7hkh;oR3J!X6`)}X>7j#e1ybk z)|A>G)NaP;M&H*q_#$}d-TFaYs<~8+>`j^po9EA46N)n$V@7r%rsNhnAJDx~Xwh6i zxsg87ZWxmKU0Z-lWYyGq5#NzB52p$;t(X@a(IY9jLZ96!=*b=&duJbQsf=L!1@G23 z0PJo1^w^mkR5l$2!KBg1f#8JTdU3v;iBryrOh>QF?X<7xp&3|{QIawzo_$_eQTAk` zxV%1)I5`dA%)4^?np_yy+XRyeFkKu6UF0ZC(g?|QwbG3veK0O-a1=?WnV2LWIDe1w zAh;e<{3|HI+*}B@w#0Wl06#f7HSzeR*r|Vby{t_I!%}gh<8$W0rZ=1N$%@HFoP;YN zk}Fv?6~{G~Nu`DeAIbQ2RG7?}q5jsvq3nNRHH>B}432;sj@HU8_<=rC{5UWaCO4>irWFRRUV(mLVuOBJi*yn_yp zujQ7(Fe9t_VYV#c18i(SK*WClVfd~6jb8MRV6$M|bTS=sK8+rC)+wI3tMJgxn6K(l z!&FP1j2kSnH>i843XH-KD9@12Fo0Zj){%>brF{ckJIaH=&4Njt5gx_}KFG8+|Hb^cB!|*4W|FM)dQH4A5;zFiqiSs(ee73k zDSdIb;Ck%hhmE2NeIkV|BSzaEGr!J(y^{t*W11w2Eu$kpmTEEU5?dsHtiMqBXAC{} zZojHO_ii5nLDDIa-D~ID-k!pZ`~j|-7|g{usRmacqA=68N<|(KCzx=Wl4+^)*N4;DRgQdb zaDbVEG&fZ&kl0lU1;gKzu`hh)4q@1^L4DyxTGhPj83E@Nao4AP>Nba|g;Np~X2o>H z^RX2_1EdlwYVCS;zRL-F8tX}r@!6G`cmX3`ipmzH_(jJ@g;k8BNw5_&D@d#+TUcAJolH~|BcMBMRXwdL=VgOSBMw85`u_|RI? zC{VI$vTlF*S!GxuPk0;lQLQ$ZOt@L-4dLM>aOZ^LdessWL-aI`7&1`?aBZETPb3a) zVE9v`)QO1xCp;{mkyZ3$PLfb1^F_2K0*d#V4vBp6Oz`-YpD1~rn71&NObpL!7YUrt zMhyN7Q*w3TH&4fCuJQ8rE-8a@jFK7!4`x54(hH$ik2KeMlL>UidV00WzQ&8cbg zBKQ-Oc|wy@(4D=w$ZEC>3a|}cKcqp2+do_|kx;5&_nMyf-wZR3h;#u?IJZkYTuq_W z4}SD0VBcOguN0GTjJ&`kiTDo?c4KaCcTkMvEqb`hr=kCk{Y8)38KT6vOJMg%iu*%A z%vH2M;LYk>==r>iT~+fQ0C*RAC#&|VlE9rlRWGVblDC+rx0V^(1ufd5Y5zZfauJU< zAT=VDM!IRAKUvE53_WuxwPI2uE(h;LrQntW=<)2;f#r(Y=$B00Dw+c~D3Zf`tGi`p z3(qVd@3rl1nc@WcHub4qEl2NMemNt7&j~M{DQu~QrXWiAY6Ci3Mm!Mjo!)ufG<{{L z`?mAv=+#Fx{M)iM8%`}n;|%h}USq_}Jh!G8Ixt2Hd27%dClYb^(9&k_`2@lu;6de$ z%5uFQI~9d9ztw*FMHkWa%e%`-gUP`28a7@T)v+r48WG zt@R!?TPu1sC10A?MOb_~*N-~k~waB_JDGdg9+ z1Ro^S^zVV1h%@S~%2|^Nqt2I%auGe*N0U*7i!qB0d!*-nb#YTPW7-;aM^e-B->k{4C6IGCDT1lY;c`F9S(M6AsGVG< zeyPIvc%d#jhGrTCC_dgr_ep53r+brLk0(I}&N5zUZY1=HsBfr?n+JB|{lGyfm!*#YAcS|}CFX+^rs@z1|>8(iX+QnN$xU*tuM>J{ki@p94kI0TvHnEm+U>Z6;-1(9c!CNgd}lorH7zSZ;3 zetTj_Ut%?g!T_|}q2a)*4GC}O;$K`D`(j;fvicmc89JH*q-|(!;yl33NF*?_Gxy8^nVebNZ^7T+ z;x0j>_Orx2PC5lTxi20jHt$mF_y;`$i`=8vTw6Y?S4kX+VvGgc+p zQj0~OJMZlVW9Q0nV8|pzCisQxbmfCOGQ-!zed`que!`Nd&WAg1YMtFt^~x*oLcuj} z;|L)aX7~q>FRbxa@p(B@Gi+5h<(37jEI*4(Jp$`qJ$Px=EwM0}Brj+Zpx#X_agw?8 zH>TRID|)4#^62@u&I81)8ix?8wbN?#W3cIW1Ar)Tf0FuPWCd?+b+^ zj%Hr7S@X9qkDK3ClCAbFxcn|eO`(TlSpW5I2MRNCK%g^ehTXrfq+IS2#!2g?UFsUK zDk}(}SWo1Cu02L&yxekEuZB&U)5=Psg{gdF95J_!U$?t82pL+rvd}|WHYHdgN>fJr zWc*7)G@cj@0Y!&rdeV8y1A3OsYO#l9Ij&Xr-uyJ<#odi4+%mK+mQ|VpZ|p5rev7}j z#%on6=&awcm}K?J$dX6~f2e!bP*Z6_2oEQ`4{SG6gdZZP$K9?mPtRNI{_fnEo}Hb! z`MtSna)MGb6=~!sXoXXi=q#RuHhf~T06;IrcvzTAvVMxW?d+B&*OMSz@8#%Xysk|* zT)~J;lmdZ4r*DhE2M-R5(Q5#QocnB=pwr`jaN+8W6DDtk%r zA^^7YR2ns5Ns_UmSFodAzQ;3JyH4~GtPk-BAA-NpV|Ox=;v?WB%_MDsl>7z79c1?m z3)QzDs*R^gvsm~NDp!DiiJ?J4%H6tl5t%r%&k0Aj+VhBr#kniDVrtgE`medQ6QK3v zr|Y>)21OA5ExR@Fd(jFA-D~N^tNO#!Z=i|It6-;Mx5%hy0=i#WEpsV~<`qCt@iO1z zQEm*3+*~6moBEoAd^DgXS*NU9Clt00S}o#gQywgCTT-8CWyxsyjV6qL(*i`MtTWaf zYrMD>`9h<9DOAqmCN;j+`C^O=+QyJ0TI%=yu9^4-SDoU6CtF6O8nEUF3VJCVHto}O zO8vKT`(pmbG)3HB9TEe;#G4VidZdq19)v@^rSI=!02`h$*GZCaBgt`sR^RbTUjH)L zR-9Lw4-@Or#1VK(4OvPmPv*WdRA|Bk;Y=ZTm#ebd9S#a@-1+i3bVd3AZ=I(0RQtK6 z;Y$hwdLPJt2+fj*1}7JlMwx9@MuR|PF}CTVaL|W)Q*of|`&9F;4DgiYMMF@sq5K6C zf>&n1>RgMy%qSDf#pmH6`FgJWzKm@&bJASfx?Asnn2)O`=(sf-v5 zruu!cyQqcHYPUpdYj(?rhWBgZ#H>t1#=e`ks zc*9#D{iguzu9j|KB0kwZ%tzdvdPh2ous?0+h@(rsfKj=psV3RV$2TZ^bZlk3Rn(L# z4^ArtLfL4PbHG#OZlCPY-OEC>gUT{aLbFV{#}H~ezZcbhe3nLAPW{+5x4@hx=b{@b zOZSzbK)iwP*+i+##8Mdld1_!SHd}8!#B$P^+;2kSCRgbt5WvPCz%f$HpttqRXNDXk zq9&6OA_628C*Wr54t@QxU_nC5?#p2nv8XUp5Z_tk0HXOIvnRV1dc;LKJg{;bIZRp* z+!|tWzNl#4M(Kn^6JtaS*5<02TWh#!3+(q_87k))$>h*wRz4Qb4mp9}>YdwtCvVWnSF9!Av1zui%94>ZpSVI3DJJc& z;mdY3hPVbI5|QX0$&C-R?A8T~7(GdY#HdYwkj~_Rr zjQ6{$vLoV4E}eVIjdd+y-gs!#p!VsNMZEM5&9n5vvgIN27&(=S|E`^&@?)gSbLwOR(x12k8G92JLf8x6cB~>i9YT@yj0$b!p{ef_9X$O?GTV~ z5*|6grW^L>NHFK)SVL&T2r)Y5zNA?f;a=y8?#Io!;JsO=9x%blhh(WN>ou+;GS=xv9qtsCe zr7tS!nrM_Ll)y@_dZ|`&&gYg*TWR5Fc~zIiKn|KKBP7+?-Q-d%acTywvt0 z4NZVOh5zH~EaRHs-?zQNU@(ywAt*6m2m?VdkWPhdATf|uK|mxWq`MTA7$YoFHbNR1 z-3lWlq`O9^baVgrd-*(Xx3`1O_qxvWI*$AH_AypfvBlq~?@(B`M&lgmEJ9L0%&~>< zCrKOmw{+>-B)?z1Nd0v+H-CQ-v&!hZMaRkPxpc>qYIi{j+Ob}#Q4Ur zbmf_qAUC7xey`KsF=giHHRWtZDSg-g&hq^l89Ap>spVz}OR=7SF*^{OwDKnD0P*EG z>4i(h2L+A~992W?f`oEKdFNiH z1+LA>aKw3!j}}p@wuJxO=${LvS~NhYvUJ zo>7`#r%~wlgSzqnnI+lx%kfn|ghoSH_ZVyr><*bI*#7!p8|prsyClSICHU9CVW6Z# zq1OWxKWZz;AXe%8Vr+Yzbf3YR-{MUK?rwVU7=^vS`^e#%1U&)+{`{q1MnHOx8OS2| z`nf|-se1|hR_Z&qHHNdeiAvh5de;IKU)QPm&3J@E10M8cr+<{bd&jzFyp}NhOl`tM zDHCO+BmYm#i1+3tYY^Xqam;~4dPq$9@Mgu)*w(oF6;!3neE$kE-rDw9dMKr)xL=%@ z2O_8^_i4;dMa$J7n*m`e3jcK6HqQd9^WafgxoL`lZoLDFXI``a0TdmoQd`8MJd-$5 zbdxl^V5?&&DBVT|zIish{-rbTjv8(&3BJuD9~H&~`-8=(-e6gif$TW`n;Q<|A`-f% zJmmsUTTauT7V9;<5(dA(p3z37U)q%3D=7{ykjY!lfFo zWIeLvF#PV;$74Sk8;I4R>-D>ksI0VkbMxp`ZCcETm&hmofMg4UXC5NAONDqO?vNSD z-A*Tk(j3g?o*4+@B1BKY)2DDeaA8b*e7MHj<#765zP25A$kfzk`Ois#A^|=#bL|Iw z6%FIO5LmXBm{unQkRNJXMeFS~w>UOg__#28(U^aKqTI{{qNf>FF0GR_FAjw8BLJT# zSfdu@&EF=uy-GX|^-FTeR`5hP!Vtfi*Bwm(faH-vDs%=4@|U$bF0}*jSGn}RH*1r%)tU$O=?q~v|uX+XLOyoZ6QF@H5ANN zc2IIo~og(#u%%O@6PuftS79eUZT`vHtK)VGpB61`SjGW0oc$*;pzU1KX?wbc@{-Wy9 z_wsR^D_a<5s=!)+OmuiaD($9o4Nat?(>D%lAc}`y|G9d*t`6O5Cm3KoYHZ!WbjdGI zoENm-R~mQHnL^fVF{YQQm9K4D`MrEpiC#Up*LlNT-C%&$27zIoGhvywkHBSLT8)tS z*QVY&{B$SrcKNhULpdU)qUE~Jk}K@59>1JAI;gpB&tINLD1fi?0}(sy!mjP!pLtp& zO6s1qOz*=oH)I*d1?Cr$bYll>U{&7@)%tHT-{+zqHI3OnzVE|5<}7otXXZj!M`q4e z6voKR6LhQ4C+cn)YDvMCUA_)xROYH{#>Tu!#hXn3UJZZi&b6D8o({?=cm9+}G~Je) z?<{=aRVB6`Nfek6I^owJ#x-koB-btWFlj0>zEXymQPI}ex!38qn42fmNYE}w+0{?> zwZc5;ezogPaRa=wY)0-I&#T>quH+nuN>^x?#)=Pj{!bT5VOB+ z!3M1FDw0m3MpT5cKfNl;MD=@0tMc=s>}yV8Ndn_Twhwf&A3W*GHFeJ07_b+;LA5H+ z$Z$Xz7lZG4{RbF3qy?F~WDI&^g&uivZ&k3k&?l}YAcn0AG$9T*E&v2oEg-+CEDz3y z>EBX1Lj%tn4GL7`x8u6eA^iDZ3+DUc-}b)~iCWDFOtU#)VfJ*^@YBl*;`-bDE#;`k z=C3RqiJQN2-Xz@Z%Q%0kNM+EUspb3$$DoN9m_5_sXfsUQ|74UVcK1e|FutN8tvtS; zj*I)$`lPDus>gJGfX3xB9hPLb8)wMU%8KgWe8g98x7<8l7tJ+_5qMc2*O{wZch6@! z$TPEk>8Pd}0R(YB=MY_Jatjn`7gnd*s*2d>g`d( z3~k;Ql0VbFC*^b7SXWY1(p#4fvoe*&B^&q8%mWl3*UlrZ7LwE`){XL8Lh$_FxizQ{ zbrK_!`s*uhvxc*Rb;9lsfj3p&$31?>I$r5#0;A;s3er$oRI;5YL5gIV#OJ9lceK*- zAr!zg{J(3a<%BeuQz)&2+EaB)8RDV^7T7F18*PUJE{_Hc&@H5w7kBYkA$Zh95o^nn zEE7Dn9uyx|>uE}M-&#=$I=O`P^~hUJ!xBu|}sbyOAiJJHAM*=-8a3;h#%(6m;pjAEQ*Pjs{?v zPxr0ixQZBrbCx%&gQps9x4yaFTnx5Ain%f{gE^NQH-#y$y8HQx>FL!DGppCc3)|8t zI=&QBEqZ6LL8w{4YK(|JCY1Mg>o2eFOb0sEFql)CC*zXuO>NB_;^R+mZb>$ZKXtLo zg1`YluI7Vw<+$o@B)3}!#LZDmM+XTDk?J6a1eUoS9p)Nan32qV7nVVmp1eAN^*=8< zcXdr88FYg=fky1$b)D7F zyT*S^%SS~4H2F!vCV~2R;V5l?`hySqNG`X>#Ujf%T7WLx-)>8mn?sd#eRY9gB!=O1O)z?jI;si` zR6esLzoa*X4GsMUjmZj?fh?5IR zlLzh{@g^8msMu#WuPKTeN%ey-^uV%5?Ml-`Mun}S$eb?9Z|;EaGn9!-wC#?6==MbP zU!C)2i7LMieKnso{WgC)8J?X9uG6nOZZwaGGC6V{DK`I#80o;A3Ld@vCA#FrR`J8F zWGMD+-(X4qbrl*>=VdB3JU)`gJggc@WjXY+4}fOS{k#?l^`*J<91`V^yYu_3c{fyxIbJhmb@$)N_bO(YtJM!`5 zcJ<6Zw-$hdcQg%ro7;RkHZ+54+3mgOEuXc&Mq}yBDze<5<@*b6_}j8@FECDn)<-$e zy2RpFsQHqG!c_^GOZj&IeAic%T4x$d_Dp_~QKl%C2wcRV9~m@0ZYepdN)_mwM=`UX zva4WqI>P)zDDf8wjcGLT+$~G|f4hK5rw%@95sL|S6|veJ!(`EkR7P?XWlX{f!|CC^+K%C;_&BX|g%{d$8_OjpD2Mq0Dz&ee5Jr1{ zw!3(&PRzr#8`*)#f`z^xE}OtB{ED`SHC5_jcralBr%c{9M$?qG@zP<~)ZS`#?2=d9 zeNXwG$y?tmiXG@tGtl9OIID_u`XNE5=UJ3?-110&8SNCX`YK!|Bq;*Fz=&$|;b!Tf z;I#TH815&@{ZgG(&jUc=G)@Up<$U_>^O;{u-xf-w8wfwGjcCDS7>-v=_+lCRm8Sj- zeqg(1jyeIObTm{B_W9zt?IsQH9cy7EBdAl36ypY>UFj7sq>NeeUqS)xfYY7$v>;0L z$j=beIo~BqlY7kcGpMVaG*6|s3p2B z0XXAKuwUziHALU}H$fo;Pk<$KZ0K^G_)|LU2hnD@@l&G}ioqWIN52EXx9YCS2|Hym z-S+#*-$P8?O(7#de2sn9ThvESlIvG<9Sn-)djHVxQ$$hmO$y|y&EpT`u`Yn$nuT*8 z1%+>f!^EHdvR(>jsr1>>b?-Py{bpDZHc7E=LSp+E$(Gs)Yx0M!=7J+#tgUC-tFEeg zdNd>ILKe_~?+sycWxl+P7>Bdf&4c=hfb7>&W>u{k)cvZvsc6kBB5F4j9l5cPHn-Ie zIWC9CrI(f=1$K1wK$s16`(}@Pn8#P*$a|Iv(bwJ1bh0O^K+6=?hy_o^1sn1JSCQ|# zd7m_@{{W$m0VSSlrh`||#_;2p`q|o)#v8d(t-iFyL(8xD-_lbdn9n%Gg_XpZA=IBc ziTX=HmU9-Y4@`W^x_|eTw)r@) z&jDHUH1k!C?wRF0`Rk0-{w$&JZp;-hOGCihVWA15SUOqi!KGrO_~~y`k>bSrM|_Sj zw#GfiXpcP+D4lTV#k8MLgBQyBnP|I{J@Ub=I1#LnmMR>dAg}H9zQ{THP3y!9i)I&# zH&A4Gjo@!)ReJR(BuHc0CVYK#*jcVamQzNynZ$E(W*OhSeQ_rzOAHHk8x4;i-I=5S z+%z{TIUb)-NM0bF0CgWLF$6sZsJHdq2umB3{%pu~cI^iHD!@Jt!|q*u0J^zH3J|8(9x&BI%iUD_{8+q^p+u( zz^r(LZe%Bn0`B48*=sCJ;qh9Xn8%xSO&%@d3% zHMR)Ye9E|dmPWbL>&OM0zfKr*rA2Mdr0kIRc3IP8hlN}yEJXW$3dCK`u0(VY5(f^k zxkxH4eS-mlerP|`=82|0Q;9!RDbi39y}U_$JF`tZa^5nMo*@cl;{`1I6LZ}yK3Hpr zs_t?^bON@p0RyMstH-7kJ>7?nIv$|z-fVcsM$Lf6ooDfsV5y+wF0&{e8DRj+%z9m8 z%*C(A&jZv1=4@~i1M?G8@BEM$%sN!LkaE6$#aPckJks_l+=hXr`xUjGP`Q|pLN4^l zpr&;@$;H)o|Ddo7Zerr#a>3Bu>@mk(=^&)v-XlN2!(;b+R*8Rz+DZJA| z=&QuJ@>QLX@s{M#0a5xZB6qw@F`mBf?z?R1Qe5?pqICNTvy48MX)|sIIYW~d%8Qp@ z({z3KZN>3`tQ0ItkW+#C6eoW%xaT zNKlCUP8BuIXB<^5KTj`Os|OkWZix%1Ynn_AVW!7|iMHMm>8xj&bKVxpd54#C3vH?F zPBY@Y3fV7o!U!Y@g3cTHzTI#X+5)ZcI|nAhC`O38qRhp29)Ha@J@<@|GzEJ|W0ZG54Gr+=R+A-76Ha%g2Cf+;q$IdE5{ zBnk9zlBW~H3`$BHU`OAX&d_V4}i!i+<) z(6{}G(pVXKG&7Y7bp=(R)RwYx90RYy(M-!>MyPB5*AKR9Ex!sceri9 zwONF@85kuZ%xmKnmXHv;R5SU{cjv)qFI&Yo+lD9oH{&L0?sXa58|q&AtUiFGM^q^D zFDOfi3BiFpjh_=kBNom-AAdZ4vo5SP+0e%7>zPs}|K9l)IhkXt-urE42C@3YuY+d9 z8Izds{Q&>%5f!sx|9MV-^|d^%*HS)SYwA8mw_u_ji$dZ`S(GElD73|-%inhZR*23) z>A;e3i3xal-9+}V1+}oOU~@@T_a$(CzVlOOq9kQMCKXHGR`5vE^@}m@NNjfli(_$P z!gvJ8hBo&0nbd!I@G9_dxqQNuJ!fX>%5PGx101 zHCbjYB}uvaVNyMmTH5+S`|`S;%nC^8;Sq)UuIL{4^Ni%S&iqSrv9nCI=kC$bcC4cB zK2_etlK)`MI}^7cE_K^i2$cF;==XOZ9F`ECyNmvSr>@Trf8psJDi8>WdmIt;tMV>$ z1iz~4Bh!nA5tQ^+c6sY!Z2A5L;{w*B-`|@wlXA3AUSE%HQ3@yz;lQW6r?AAic!Vj+ zT%?SAcm{xSJc+ah$GS55fR$4CiZk-_i!vp~XQi{XR47R_2OTB_t%^!w#;{7JivR$2 z?yrvB@9X4|txG`O+#T}%NBiH>67d0JG9688f+xM4M@o|~Gd{FgthOK*VTZ=A8`rnj zw@J7ua;_NOx-WLLS7A_KD9HxuuCx*KYyaTXJD)*4*S59X8HGSfzBlQ|s$t+6A_Y)% zd8*P|kfGk+h17ZPIBQzv;K`d=oeDi6$PU3=!b7*-N=WYudF!oWVT+Zub>W-?gU~@8 zMPzSI@hq6Ao+%+j0n+>Mi6~9-1^>Gs(g=|P%FOj-Ie9ceDHk3SX@SbNmc<_RXRA&^ z{Yk8+YT;!|0Gvu_DO`lhkfq~CyA7gUOdAPk`~6wit$Bm#RUAcN^ic>c+N1y%qnA0C z+HhCq4GbVif5Ih+L?jOlLj0+Kd<9bHm-V&AO>3O+@U-(~Zn9!8-}5@&gZ(uO24LFh z5!%7$20A1)JaR0y_SO|b8GJ;*Sw-PV0L#!4W}S)Mn~Xz#%+T^&G*S}g4oAwPgclo& z4i>(MVbHMvHCN4bPmk5jnX2B`FC5rTwri3ig|6Dg@AGMUcWfv&)Sbw%abS|HWY_kM zbW4T*kx$)U<$Of6%kH_erYsaNMr+BIph#Q>5TJ1$w0bZ(G;U7M zmqnj+H8ugpDTuX<-J^7~>XX;C55UR5-YZQgL9Slz@RZZjK*!zB#Y5hm4r;1y4xPTM z2ySj*m`55ZBsk?|qp0Ya;sx_d&EFDBcKCr3>R1AhkFvP%F7y?iPlPi>0p%KpWDKLY zZAU3NQo_*@;Z4Fh_k^{_(T(4=AH#&1S|Rz5K92#RU8SBAZt=y~m& zbN$}Ow#DOo^;?;3pZm`=at5=)LTZZ|S5_(Bkw+k24L}c5&dA0reb>k=)pz3;0D7@I zZZ}jI06L^V3xWHoILFvGm=#mc2e+o!BWPpqpg)8GAYI2l=-^m|W;5&40lbUjPw|3k zg@o%2Z8dVx5ccRy(-u`46^5M@h6|tjNq-AP&7gbgFc1!tvRp|yF&7&N@s-VgfPeEb zS41qo)Mc|Dt+_nrL>?9JMj0j-;n<6DIgRZ*d~ux8pfID=~FckQ@Z=N}wnKnFF$ zMCJ_et_G5u_vGy2=aU=pZ2FOKyBt}Hk)zRFp4azB;YTy5+5~usC(MVm&2!R?+x%F(ZF^lbjm%zTV#X;sgFHDj7wKL;ZKieDQMy=Ow_o@UIxEAqh0{PPR~|V+ z7Ud|7RgXd)D2|z`&zJhV5*AxP=4?93Oa44OGNtfON(lMTYb3FPjhF#XSJ7c)-I5wL zq9|1D%=Nos8dcUp1Z%gvs)KAwf@iZZ?g@h`n z9~K(tG>G6kjE%?;Txpn^>$2MVO*Jt|+8(6hGTx9&^-<3`0 z$`9ieyRqq6b!2Dxn{*mUxelpq1CqMV*jP0`6h)vYvFGs$voOuq`Tz^5+&|6^!cttZFvhAWm1 z7Ig#vPc^=d*=szHocgVb{YlVRkv6(6U zZYj$%>B}3MISz>kd~C*dcP8o`nBPu%5ID%qjP_<{Sf?QdoruT5MO1FkFtavIGVaOm zm5fh@K-HoF3=ezAK75LefsR@8h(gbs!^f_JrvtibFa0)Ot3w6rcE6w}0{hwg`UfZ} zw`dbR?+Ldtw02-#nQ*Vv9khlBv8d_3KZHmfzdRLLJ;q~tA>|#u&+JsuA8H02Rk}~4O!m5_ZPJQv z0NCLE2`M76i$XG-ZN7Q_#WklNCe4y{opQ0@4C)eq7d3zp%pTr7b_3XZsGQ(-_N4yh zn0HH+)rj-!FK+hqqQvS~gjay~hLII>KvA`jyEj?lp4(PF?%_TB_)gvSu?YXVC;!c0 z?S8ySJU@&r4;s*v+NfF|z2i2g+e-5T9zz_g@EJ&V7WL{}3D5DofmZ44+YrI93nAKJ zp0^=9ZI5k-D(+|Z4}^_feS3;c%cHaBdlgK8!|Mfedc4DQjELUC3L{H-S;VEHzH2@s z-Gae2qec1_>V?WKsiHU0Y#h|@t%m#8P}B$$JwaF)ebM^>x^stYvzVXG=UVr^FzY5T z&J=woh$i-N+P>Xlflg{rzoNlcBgS+@2%?@U`MJj3!BMv(in15i?L3&YcWTckJ{A1h zp0Vu;;r=5XP5VKuR5ot%==kD7Uv^Tq63K65*~#f)_7-mgzYmAnVWPOYw*)3locX-)e0i)lT@oeMD{kM%zqrwxYoYtg3wcfS?x^8>L9BPncl4c$ zyI0nYE#3M(_WXG@i|!oA!Ymay8I6GC9xOZ|8_gAZ@AO_!55T#*0b6=!8y=3&vlV*w=!dFt;+o#LK;aul?j((W4?Cn-sI7VH-;X=EMA8KOjt}uj4|Zn) zP?jTAyhHP+I4WItx@JL6u2XOD%zenGcZlBC9%mPfI>C&Ntt1NC$xs&roY`7Vn6w_4 zSv9H*e8e~&(*MCiUkCp~yULOsK`An!QP3oIc9g*TSMB@bfLWCr+u#QB)_9g&nOkC> zqAXXFVwoBjgQ-1&0kZ68;)&O_dW_b^RSE zp0!M8(to?M@m2Br+nypqWvUzB4casZPgxTZj>y9eavdlX0yb;Io$KeS80dbN#23x+ z)F9SV#L^lhHpu9%^dG?D;$nRWWSPXxve_)VtW2r)h8DVQ{PNrVFu2y4%vdZvk%M9e zq0R(HhL2j%dhr4P5KDCe*hQ(w`ZGI({sU2ZT5|sV0;SSi;L7B(fK|2)=JOq*8ixe@ ziu(qz4&Pvc@wo)m8UK!$MIiO{!H8br>h@+CN~JVGkdB4Rn{*8n{*1&+xp{U3m*Y8kxU>RI%B?^-{B z4evG-yFljBwc5XENb0!#@AH^*s@vR+E`4L8#AzCl`0(`3vmgx(-Ri^3W#WH;tc-*m z?}5jff1Et#BH|FjuqA`WvBT4UV%UQfO|73?hU*fuc$Em!;vaeY*k8f=l3pJcw-O0K zU&WS_g0ju^dAgM*J?O7^NkAnD2%QXeFG^zCgQ8IfEGKk3iD?q@2}B|Rv5wHefd8bs zgEP5Z8Norm>@jD-SvMa*Wc>u7Yb8(7z~`u5ei+_P5L_bL!SmD9Icq~gRlJZMBg4nf z335w+rX1|T6tG*6@-ft@C!-kX413%4s4NDh3&68A3Fz}a{T(YdU9Z#TeEV78piCS= zdOj|S8zRxnXA!0<94vV{84r4*>QYhF6-nTK7Gg_zqn_xMEt~Ln@ zR<(1rNh&q|BpNb1Zz_$hsi2#Bso{q_$ATl3=bmydV-cYfIk6ft`?KWq#udlxyUqZ% zExOUuFuQ8`?KOs&R^KdEQdho62peP48Z1l&6~D%mdr|wSqFN2}HzI>qQ8$;YSj((J zc~&_`59<*S5<+yHi4aU;8i{fqAp>(ua~BhRX+8+q8;zy{kF0@}2iiQ0h7WAfA3q{G zLloE+xKH;IQurEYR>MfHdmc_?dwo@e9$Y)-yJlk#YIR za15lPv#(9gkDPr)-Gs7mx?hXGiNbf|+cY_iz^Vxal#=UWgvD?z+t7be% zPhvv0MkGEbOhrG|>#*S7;rZlj@|T8|hwaw%Ue>r%b)*wbz5=t6eA+QrBL~m# zzvHPDzprGWuqD74p~pYrti9sb*oscTGvL(}n%a)oBsSl6x&=HuPD-ijYP6@9V8Uvv ze&N)D25z#}u~>S~#j+};ypFMn01iVPkVn?dm9C~r*$9a@;HOLJ0kyyusK)nCAl)Bo zEggzY@g@w`IWiK0d1nu0BPUsYn51X1vn(>+tB*+2dIWIGs&_VZip&rPAw;=A4AFcq zWSV&?*Z4^_9kS$T>v+=CdT%Lr+T<{oz4&(R81n$@l*QXNXvTj4m-O$0^yHEF@bv`a zrLKmrax0_Sc>*^`^fIgBk^bxm`vdZUzjwh|bt9E;5PR#MiT^*~%T3k7Chf#R8~C0) zsk6F%TKkX5i$uI>bx@Ne4-sX8hzH^6k)v&-euqZCJJm?OQ0_~v9>u-&Rm`#=bLLPp zbwYT;`LES85@%kb*ihP5+?^TM7`PaxmJ2_+>6C_b`vSD5A1i4X9^dgFAWxX{Kv~=9 z{@{Hdt!SC47WO5rm_jkN3J|j%QrLINI)2?@qId#3u~|q6p~99Ui1^Qm*bMp`D)F@P zL*o1t_!+s99!#C)aiJXKpuJ$oyZA6Cl(cWNJad+0+{g(E^FQOI zK4%p_&^DTe6RoA66e_8MLJA0~N?#P>XO!xA#VA~a_r@Qbt+EwV9lI;*OfgobE(6T( z<0WwO4SQCk8}?c(qpuSSfsgoG?f_0lY;j$;?yYy}uh?I4(_9&+^tec_jIx&wb6I2- z`NuN~1`f5uX}F2SBaYTVJYHs~`3}Bt$!OdtbC>WqEh`e56;G1uj)}~o&}3_L%AX3M z5KQ7%XyLgCB0QX;l$Xb9L&ZFrC1&_gzF2TblW|mbUwN1z7I+1M1p9(qz&-bls!kgg z^R@>h25zF0^K=VeooQ_~WaCgR0VE^BPS4#n9Yu#Oi>H375VH?q6viO;;n`b?_vp97 z>Tzrq%Bnm2I~8<~OU1#EO2s z)QAWIKde1zC z(kxMBG+gQN`R_0`A*s69E(P&`=XF9`wXXXO^_4-EYbTEo5^J1=lTOXcR2b$|ly6|= zz#c(W{H+QEfoS%Ksa!3stod-``a4~8r2{7`<~NvUw9ygsO|$Fsyqp$p}w2AuhnkTe*pU{;@A)Uh+rn2j88d&}P)8 zWF2{G4JsbsQym-zSmb!DR%oQ>|JAjNzT-h&e`nhn_`*5Il-SL|P}PCZT)ZKz#^V^m zwplg!2`a1ah^~MOcqwk`Bop(lmd~IKVxd$RtVatp1-Rd)oY4%O2R50|pPVMr=?hQf zE`-LODDX{$J%e5f__NFx^}d$=Hu+ewkmJ`&mxOAC0jN0+Pes;K@BPAh1JaJ}5>Emp zgocSJ;&0WB87YMe3rHVa7+qx?Dq z2cX0qVg|>wm7{w5-aA3R{Uz4-R>uTFeGlhF{ZLk@+;dwi%1&JENn?@Iwl5 zDE2CNpY2Rr4TNAQdsKHBK^gA7XujG3#cub+mlLFBXA*lJ{RJ}$E{y4a+D~Z4fpURM zFd*ndM?*v;AOQ+A`W#*{>AZf}`|7@=4b6TeC2K$~iz;{>UF0Ypw>gLiT`kTP$;Bdd zz<}80iRfjXs)KH=F`Jzvo6xk&%ZNbv3*BJ9{eXQ-Fx_TKPI_(i0XM_x;nANgxu@e} zdERt8UA7Rdd^v6iOo$!#1>FeN^n zjgLe)JJQw#$c3XzIUu@hFn zBFQr|%Gx9Oc_<*l^u&WiVKaA1kp_>t(Yhx3$mi{hZXv2Y?st4SNltF0xfmm1_f^;9 zE|f)5OB4jYO}Z|B)`P(VhHyz28 zpKT4?cJkUqxIoLDjpovD@v+6-S4f}*D9!UlqR5g$gAo$dR+8E@h z4q6+ZUZ;Q5gfi%PN_zlh$dG)Jtvq*A^Jv6m_2UDV`(Lg(5#aY=W@{Fjk_~tDtAH_dhf{XXxky2~a*R?(> zn|zLy;S&UJr6&ceqv!#%v&%APWlCGhw!Vs760yHZ`D)`@vSF4C)Y0A~@TGD~BQYuD zp~g(y+?kjmV|6Y<6!s^MB1fOQ|IorYdY@ffFxD?9U21!#>JqEkYPzZK2$bO56nYpQ zfm2yv*$C#-c3U+*oW-OSgsz8@ic4-5w(g9^&>(&%%!I|0inn<$s}+W`;_h^gc|tJy ztfHe2S7X|xDlVtiGi*PuC(pdPjPHAM;vL=$W76^YoFG+irSYR|YC(Jnmn0-RE6oV@ zSOF?3*2fL>4=883i07d#G(V*k)D;oaPI}RyfI#4e67jS5d#@ZGR~m}Na*vyOOVW2? zhZH2MFeI^{D+8`8@t;^6e9{_VKS)G(hGQqQ_y-LQf76 zVEkKBjI{Foy87*5dH(aOLL$QLB|1%1hM?YFm%WP7@}6(%I$`UN;Q#7!s=arS?V@Yq#uJX3b#{r|4yPOga|OgATcBIH`Ed-7zQ9OvL{<}Jo?q0r|Y8y zxsE;nF#dK|`ejm@ThK#sd766uF0{+z#8fNnozCcA0EV*C3-60Z@(p|YMvHJq5)ljg z6esGNT+3MS*6h&Uh|#xsPS<<52HbZ|eL;m;DySQli8?ygk(FrizA&k5|3Q>feqyGn zK6iWymAeONtC-qz;NJaD>24;TL)}UJjhZfIfzANKV(IUYH}UXn*n8^fdK%nDLR3k_ z)D^AYI7mOf;%AFx%lp)<<+>gApmN>lR8M^R!y8?Hp2J{_Zx`NFtRR~dbu$SySt*yL zXZ2#Y_IslXD;vsNZtKe1`TA%@@Qc}s3a3<&3j^wSXRWyk@60SdI!Q#2zJcdo>9S#o zWkmJ3N^Ndvh>lO)E|!c`RMU$$t@Fq{5tO>WkDnt!8U-;$l5_jc=nzO^A5A1q z8+QXG?5^t)MbwO%O6))NoYM3jF*2I1!0L6rY&ASeL11vdeF*#4o|&NNoo+!jWD1ZC z+tP*d zs-Cv^#kP0)&9yxyuz*J5*@w4bVR9-F78b^AI61$>G0O4JQkiwDT}RA`2Os{Fd%2~v)NSZ)}K$h8#q&b9Q99^ z^IjQs@2`CzB@8uq?X*%g;F=8SL=Q9M(Y;wgGZT!t+vF5&6USd}%oiw4d;+&U6Mz`M zwwXj6ZBD%6NC~VUs-^7`9GpN&BGf+`qlTAXZQ2#P#FqRr$R@OwEqqTb$>XzIwcBJ= z_$zAgaRpt2f_XBt_sjM-5U}GjdQKv7-{`nQQg-4n;1|ecK7%Jgv+L{GKk6qX5^h$l zSJL0EA6P2viKrop0pZcWCIB#2O!S=gSZm-?@WD;$v+8{3Q$Z*3UP8z2)=z!lS+!OS zE0oK(z16uY?4Nld5~4P-N+%5WdoFG={$>D?qI-9~>#x&TRjub@VXLhB&aiVKm#C1V z!3Pohih+iHOkz8ktC2JK!Te0Zoh7!MIbK%r@t=jL%1R*Z{2UUDhDI60-nNni?FBbn ze-1e##jv=Jm>rxYCy%d~#z2>TKM`NA!)18Ti@6Itx237RDo1t?GS>e)kP;uPpmPrJ zzN;?aIbwYu<2fmfAs3jttT>D*s?(@uk45L>M&gDjZ>P93$kibjODO`XIPgje93r(j?hE)i1o zbJc~1@~wr&5V{s~C3c#Cw%?b__(IQ&VGb|T!?Vh7po;$-d3niudZ`^BrcYjQ`SvID z%}>1EP2^(vlGNv?75^05^=PT1hXZZcEk?KY3z5w(L3^cAvuD%r|%h+K`v~`7^7cClPu@ljy3hdnyLZxo**a_n* zcb!5s%4M@0E|Rwp%AP|GyxPV&cu4Kn1{+R&u33$7E;68SqC0NY1a>&3qdt?*X3nLW ze$+m${!tKlJxiy%+AK8`QLdTpl5kUS-VWtfF1_Jd@%=j#3G|g|=YCmntv2{dBg;2I zgvV_&>BS$L(MbNJ)zAGd1?^6rZ11LC-hHO4hl1x6^Sm=I5^=d1J5?**Iq+)OPkJ3c zCp0`kZ2D&ZG_x|62|c)Cm4tfc7wX$b3_+_l6}wob8nv6Ajt}|D-(7U-c=ud-ZPK&A zsApE*Wt=L_D*r0l_lPZ=F_KU*rYdTxPeYwLZFBAjFpIr!9cXj<#^-8zSXN%E`fFL& zG0b%*76i4~Bn$v${4wnvs{B!Uc2#8D-}zi)V#vL}>r6qUX2X+fIJ>h(Z)yk_wHew+ zs9r{WChik>KajJpisx>emNmN!Ce!b1dHI(c5mdkN49azN%cYek7D6y6FhUO^gAlAL z?$vG{{7^ob7F+StCE}-0D1!7Xx^GqAx4lb;+a8{Cy83Hy3}dd#Srl_gb9Rz{;1xYG zp`P!uJ;jQ*`*~ZJUMFvPZoJ`7OPZzJl7+!b!=XGOEo0=WN(%;Xg4*f$SDf^3BQy4( zGUxC(h}tb|#++g7lWSkORi#l?Qs;FULQF*ti?#c}$gQHq>_PD~dk0Uv^z6m$j2g$( z*Hgcpb}Mey=9fPmuiA`U9EAjmR%j6JkTbdqVx@K?-3E~| zMiw@YxasAZzJXE_Jm9jtdeuXEdsU8Pme#Gh$B ze&H~K-ahikj?(A-T)kbYV>J3bAOuLgem-__@Q;t0p2XEeRwh1NV48?4zQ1qOPp3x$ zC4|Tx@h)#|8Wqd!tz%L}O^rX}Y*IsJEPX~_mwV+gkMTZ%r**Ja3Vx={LJR&QzhZ9- z0R`jJd||G#oXSCpjI#U1VEny)jD;ThCl2eLHG_@sozzj3&E@)}uvkUr;xza{EhxyT zvdl?QOs{x^rhmo%#-ZRif1JDS^y5Imd&Z>jt$_^EXJ z#oB0nTQRTyo?rhQ0aVpmCeH0ZzUD?0a;{pxW|Zt)057{39Zc-$9;4SosDI$idJ9Mbhg(8CbTnf`^T)PnjyySGgbs6IpzoVdk0R*!&^q5xH zwmiJVR=y?61o0KS!c1uA1XzR~J$q7h{Bu2FL~Sv8z}j-u@T0tT<)=?htd$8*wr==( z8I(>=v7f}Hul=67Gv)zA8>j#n4N@*zWl7}d5&S!<77lPHlX0he5`b=tk#2U@u2Jy} zCAKPt@d1aY;kNpiMhoSn{96g8oxCZRiOSB|bL( zVzaCV5s_!3=5qT#fCRr5zu2j~t?q-7Q{SDRvE$FoO+5ucKU*b-#IIJz%!-SW*of#v zsx1l0#hS%fSmOKSLLH9s?}q7ImeP&h3vEu3;^>p!&X7Rg&EA0J5{lM%j%2zU9$t_K zhkM3-;@Nc%rf5LM^tyJ<(Q0~r!^^uQ6F$T&J5&{J7oxns_RgS)Z682~IpJDx+fY66 zAq|W-=)SB={p>Dn^?NtlvE{bwtzep(C)tAUrXvV#$*@f4!mqj)spQsJ*xcAh@p*<9Rnj**U2bg`j)-_x_u~;nA&HVbG02Ohsr~Qs zQ-_mQhvS|Ql^6@GB%YbMd^r!sjdcrOJpZt&1KZW%U(|-WAqKkR&i*G8t7RPz?e4oFnEZ=k6?J6&-X$sRyYf$(g=G>|?nc$0g0 z;_HxK*OpN057qv{?>bWh)Dv|_3Hm#ubbwPvz^V16a`BO37pj?)dwKPMG^J0+)yt(| zX=yi7dF*FQaIyyg6dMyMn3OS#G0NH%r5`_Y7&pfkXwbj8;>sNwo_->8w6732mLD8I zEM2Uk6o|glgX7a@qSI!&jbEzp@TN6P?!>L04qba=peQEB$*Bhi_$~`3jP7;x28l7f zub57vvA>SoXZA<8_@wlSZVxk3$52I!3>X11j9r;rtW64t6 zaHji%?ygDU3B8s|ITbSG6){fYF|t1-H6-#Lo)sUuIQm5?r&@uY{y2kPwlY4`pwrAZbj9;tqdb37K>cK z7KC8$hObV7mbRq!5F~&~V$haWd6n8zz~Mi>LbDr;gNBz$V1%D2kbT=OIM{P*zL3^kO3X zva%=cNZwzgUfV{=b8aqorlc0wvU2i;Oxrx-JGT581M^9;j>U?GT}P`Pw_kp1HOupx z{8en|cI*C(ED&O~+el9d`13vQ5t|_T0N_xmM8d+Kzb*#vE&m8CBHnI0Gm94)@Jlh> z1-8e8=b3;~BSuvHynpNTuNit0e`Kn@5zcGd(ji4L`^P6C^^dp|YcGa1UC+(Np4OoL z5B)#_ztP(vXMc3{k_wr5c3`47SKc^tMa=+ds zhMf+r49Sqhc@pHMOLggMWyabLI#j0=dPyfE*ST-v4<5y^{5`B+EVXfabn;uexWP)0 zrdco^x-H(RP0rfOX>cVirE620X&ieP=q4S4rt=WA#N2hOX7#aYi5)DdYSIwfk6Qrh zl1hS7l%y$ZDMaH+1#Ah>u(J{jtnRw8mk?H5pQ0 zZI?@LI_yTC3uGv%N>)cAdzo{!%DLQMw#P(P+;#P33sTC^r6{FEv%`1?BqxEKb`_kz z5Wg9huMj(xc~e(4F53oNmKk{tmb~WLbx1(@2n8TEI#JY80l*l*tzla&m+fzUfn>6! z$rG+pTa1Ewyq1#Vh;33NECqRNHWpU%g(so3l@M{ysCsdiZ<4%2H)AiohcSr7#l3~o z#SK_tmgtLjE@6Hbj^iW8ePj?8wHVj|$FrV0YiVU?mGH(HyuD+|a`l@bYPiHMN_fw>~q!}mLT*7)kQyFKWwE!h76Y;xjD zZLs)pu(1#o0u-bIFfyOvUExQ=pN8!>16xYg+Cx`wMfL+m;^DYLnDvZHV3(l6XuR*1 zro@G?w-Cz?D71pFm<(>&{Z7P~#IoyerC9+@s4W0zjHqXlIQRC=aDK9UQ#XNPYqiRl z%eZZ(5Zc~N*tbg4i@u3MwYKsU4TtqnAuC)d!=_Of3>T*Bd8Ij+u+{x0=;``1;cc$b z1cCxc(nH(Ih>#;r@MrVu@hR}Y@lD~u+4FAN9&V-5@f70!0556grCk|qhVrF7D76W; zSU_**Qj3HHrCB%`t#mrujJV@Yp-sBd)Z2}%Jt{+pQA*IH@IsQHK^Vv&*XiwfYAyB) zu`agAHz-b?ha*mk%(&}G%1Rc)?Ks-gfqW?a6VOHpx3 zNn7|k`g}wH0@UTT=BSXC7I?76x)|e$Q`iO|ggGkz01>`2g92d}ND`Af0OMqTz5{yT zfb;9^`@KJ+qG836HcJs=Hm4*$;u~cM$x(4i2~IJSo}xJ%ir-lX&WfN5LfXYT*1dyC}<6Ae64!$5Zsx9smU8mKJNQAdpx!&e! zm)`OkNpTJ-qWotN!)>HEQzfM#X$cMtD;W1aOZbuL2U2jhud+6G%ebY(>1S1i_9V^~ zcVxH7N>hJrd7|N^rAh#_mX@`u5{aEdnP2qhGvKQZV;FVA7J*6v;?*g;@%%BVNw*Cz za#Oyg=t>6aAo)#S;S0t0h<^(@$ue|A`M1jqcRahsm%B=k^|D9{A?U0r=B9|U59ymi zSY<91r~pEe4Rz<>@5au9@nfe_`~sanuAsDme$q155iT>SLj2cbw&H|`W0kb((&CtG z7RpWlRtFARX5D;gr`>n@*~t?hVng!fJjXc-#0t^7k{j9ap`H+0+UhFZDMi>qZV61Nb1 zHE%f&I^r2y=xsQ=>zp3y?VD$DV!L|r%%mo1i1mA0nDETdAJau=isDjoKf5>%X| zVLh@ll>7caf2XB?TUSztQm_&a1Muh5jCRgOJx*~{)|$K~`(d!(K~F{I&1<@#0td@+ zg2cho+anJSa56FXRQR}qg(oFh!Q+AGbIJZ;?_Zr~oV&HTb)uy<>rSn=k_jKBX=Muv z0O^3FouKYM+V#GGLPT-|s6+rjf@Bc}XVk|ABBupO!3kJWNEzoO0i5tXJC98M+JRco zfI@%?&U%j|=jwl;@rv+6xa0s%PH=KNkHfd@?bz3-akmCkF@kUi#&PUFyWhX9Tio5; zTom|++bY31>l#$~KY=5@d1j>9*edpXqlf%2EeUBhV4XGH`Ma zrxoHHJgI3)K~H?(4_uOQ_xk$t>t1ipW;y^+Dpx%zNCT^%eDDXa7#)Uco_dYQ$|^x_=gRheLNZ*>egH zTPqP$D+_Ak{pam)+jo%b3AwntVe2&c_pF~0SxcmaSUPRaW6jPqms5dST!tIN+I1S( zdl|m)-c6WwB`!5;+fUy-+M9Juvu^8mCT_cf4U&|pYdqymo->{~BmxNa8Rz!blCH^8 zdQ5VojCB_ZNcxVH^ZP*_{M43}^%pIFN6*7{31rp2FM#G8<8*|N7XJVp%~;KguB?S? zDPt>KhU*|t4aR=7RRj0MC`4A*)pfbms9YYroTEZFkIb-m89GyR*m48nLsI@qh4W+rp2{s z20Uyk;1e8gGX<@%6osg#1xo4}$VnvUlb@GAch}HDwI(yF=}B2mN=Z&|PH=k@gOA7Q z?eKiHl&R#YMI@f2k_pMl1t5TP&(QWGxu{P!l$O!}>Q)W{K*ET^h~vLJ^TGAzzD;$D zp?UYAnUH17@rHy8q&ofyk74c`j0YdIWx$6naNET;+nU-^kS?q*8MVZ0{8m+QYKE?D zU3=)?OZuaedb5C>Zz^86Yz;ank0u@R5^}ohiG(@I7wZf5#r!_s`v3 zX0zrxvb7`*ai7N=3}k{i0OTLPw%*VLqRw=ux;c(@#yn2c`?26rQJ2i;u2Qk1XL z9enJs*?&uExF`WEypj@o0yF4wkaP9=XBB-dHl!s8N>Gw;IrSfQ0sjD&v7bR|K>2M& zAOc5GIrSrqi{{Y?p0E-{iuuy~U22cE+)fbF^cYY1lOfaQvXuf(I&syXZ^s?TubmeiVb0hVJo}kLG-8-?Shx(dhVGX0YeHZsTv@r|TfBg& z1unL@`sstX^a^qwTFf?Sx5n^$8^pSq@dF6)P_w7ZhnmpqYCfq@k+%F+>vY$QEwR)I zVHatRS>A9YC@l#rh4(pe$D1Gmah{RJeT7vz-^8ZNXuANtT{W$x?>ngwEwH7=k#(OS zq;y7$2tI{8@`*_ZLW;4HGl5+2UN6!ZZN#-HX>qg_WheDB)CU;@sBnJn{8gT{-rS3+ zV%=rTlX$wB%>98A9&zRyb#5gs7GXHIW2E&gfTT1Pz#V`C)0rFMS3F-~uN=>VVc6Bv zZ6z%|X4|%RS1W{s1))WDsr_{?N?x_zp5&$77 zaX~Ji5dwU~B+t)ynxA)f*Ks0E)y!GdwOcD>jQs1x%J(12)c3fmZD@@d7Q2L0eUqw6 z<0)=Nxb<|G5^|7p$*+&i$I=?xac`FxP?>gAnUB2dpaua_ma-i48_9WPUHZ8roa2hy zcaHx69D18+wnE*u7c1*8>Wjoh8;!M~%Wqqf>p>r6mXrXry3*NEEx3%Nl&d+eQ8k9J zxZiFvrOBA%@5^y+D3_Mu3Q_9m88`zzz<0+O=hGPi#}v<)rW=`Dx5RL4FAujr;`d5R zZ(ZV78dY?an`P-1_9#jiY)F@UWso%#jzx~%Vc91Nzr#v!>#Rck(uReoF1K%B!6BDa zx4~NOF4wKqEbXJiNHmlhS;dr)O+C);!^M@k-cD$G;V(!$$yVZfE8ND0n+dfR#XbEUMMlS=Ko zwl;LNk`q?@Oj|PJeU%XzKn;X>0Vr9)Nb4gc0hJyFP1er4;h754^LGWr)ZQ(vEZ^d` zpg1to3I6~Vw^<5pcZw(Uf(ltmN=&Up4rhYo+(R6={H=@OkF|$71GbD@vbK|Wg#o4Q zz&-&Rqj_?52uRQKZuiMXjwDLBFd zho6)lIMI&fZ9d}d75YY-+-)Uy(syOXSbf)aJ6nO-)Xzsa@$*OOM2G{yP`seC99ev zrgLO^d2giUM?b7<%Gfvbjo#%vO}slaU<%FnDsJ4je-yDJ4aDu5Z2>{hJSQyf-O~k4 z6kBUifZ_xyvY4+#>@fr>>@Mk8RmzYQmK>XlRVc@n3X8%7gT|lvl^Pgv0~s(qLd=#!EW+Fl=!T^<9kk^E0$uNC(c+=acIrG6w`3L~M`6&Qj*pAT=?^q(M-1A*H&(Pv~;t32tUXt~feL1p^iO$l0K0PWwVWd`;kPZjiD2rwP-@wDZY;upeIaA?%{be(mo6lrH>f3OxyrbZ-srN8ZkO#;m1qWG1umu5&~+7VMSh32i=-RHt|ivjEs56Y?KdJE zwp>fBKFY^Z9Bsssm8D4_f|8&??sJ-$btoM5513${IQzXw_CB5X$2vy%CvVQE+^vJV zXdPX2dh2uB+nek{ktx#rmh8krhY5#WaF-SvQ*PnM6s0JWlj~cwLfZ)mQODi$4mtM+ z{5^TE&CgkSeT#I9skyhO*oD};xytyf)-7MM7TCP8P)qhVXh|}Uq*}Vur6j9Llt3g2 zuVLUgqj>%chhr@?q$d%!YUjcu%-WQcg|>G80GX^13<-cta3*XNB^WCJ;2fM_^ON=T z_wUC!G3DFGFh6f^brX^F z9=)?%&FkBz8VxOKDM%5j=0V*do5t`Z|aS z1e5a&XQcXZ_0I?B54Cu>;>jop2OQ-0=g^P4jQ0BX2kf}hd058)=KvGxeR<$#Ipf#w zlS{dk5{ToZ9CbQz)_uZz^nu$sKDfsQy4OVd5!5ccW@X?RCk<^(Ss{n9?8AvVyKD~u zwSCVGx9{$f+m1T>#V#96I?bDl`>@l?YTVn~T)xLT>Q_}Vhe_~sV;P?1&l2dm))j=> zBt44a_YEK|#9A<=p(tgoNb&CO9A?tr#Ayya<2=i5$G;_(x=V{lPy;AQ%JO>>k&*^S zB;)DLLL0IIvXnTWM2AB@itHvO2E3uZwbyu}(_lZQwN{RS6p60Ov96wKYYCB-C$Jid}R4iyY!XM&J9AHNwq z3ieF499C4K*cif8pVlCQ%7GwfwkB0ni(Z(=*4>08Gl7)tEa6HkA z->J7sKy7X@nl3W~F1D4YQrk#rOG=iMy4fgGcw7tP2dy|$?Jp667wp$}k8;SRQ?CuFRb0tiZq7w1anG8`RExzc(^Q5fYU5$bXQ$FFREJh|yH+F0sZ zikJGR9-mQ#4&?h_;=F>?2T3HSoTQVEcsM=MPpKHsu6tLLg_imf+D}pQ74s*rjDDyp z$r!=F7{DO*#eBc`hWJI#T>x5fhh9!?$ai-}+RrrMmd;zN4{^nm+gavI1s6^ebpFEK z_O355+ohDW-Q#yl7HRj_M1CRie^?N3w6yMD&GxK#&*l5a+Hn_+AputxSS{$$^!s)Q z)bmQWx4mH+oL#xMLAfz5IM6~J9YtB`C3yn_-#*}zp6Az|)|Y-?#>sCWNdYR_ox&i1 z2v&5BTzyDklWwj(EsFfae~(j%wR)s#2Yd-cK+LjjP-s1_2rT z(Yue<;E%f)9)NoJzb&CD%z1uyCmX`V5(xw8*6{#$`F1^wSAN7$k%!)DGDW4-NthlA zj?wSGc#8bMsY+Mun!ZX>l%*+3Qk11BN>Y@iDN0hmR^J9Uj;)c|a;2!DJ6*TvSC~3f z3wlz}o(CYLl=shXPH|t9m42E&61P$Xt7mqa0^Un%PTS~IeCz!}oiA%iIZ@6sN%kGF zgI-|xhtK$SM8q>fX;7~ajPB{Z45!iRlWCf#Jp=_aEYDrN8 zr>m2nz#I(m$JAF(%GvJ=#VoDfX6dCD8A8&XX(kkuz&cg2K9f8BqZ^v?MeA2i%Szd5 zNR*f+V8AkBc|SeLipaq!KdKkUa0h?c-;UX@HOaS#d$cB)8OgRx%Njf%W6-k?HAJ+p0GP-z}#KWF+Jit*}x_ z{z{KdatOi4?cmo0>Yq}ud?L!>mvKfdE<7bE8lFpT7#yT1fUP9%HZVaGwDru5lrGfc zFB@e%(ON=*Nl*mA_)?R*lj~|Vj%}*`+t0^flpLxZs^(ZB;(kS4Z5}q=ya0g?a{*{B; zJ8&C|b5G?)%Zwmnp+tgmQ{Mn~$7Ie zE)yDd8l-Y|fB+j3O=N=`g=Bg=u_jGR(_VT&buXwPY?V5`gN~&Tl%+#GvB1S!eMO~h zxB0AyQzu1ChMe;xwFweoH2RdEEUERn0#Z^+kITom6^EBSMC6(3Q*WsYTGfu1R1|uM z&PX{RcJ=o4s)o<2?lux+Dd*;}TPSifvmZlatDd~2ODQQxAYcsVJa(=dj&)koGu(X> zGhjBCRz+z|yVCBoHid<2Qj!bgyTgUGl0t||cmr&<$QKx3`c4yk>o-m$sN1Qw$_mz) z2~Mxk0GR=>r0JNGB$NEzUAfnmD9_Barrmpj(LP~#H6C#oQ30kIj_6n*5D%C+#&|qo zhPRQdtW7EFu8q7z>TIY#_DLRE(m*|6sR>Cy7&!wNKU&H?TfZ8nG-ZS?G^nh)9z;hF z(t47Pu9i!|r8q_v)5s+EJoc&fJ9J^|7}w2vy4DzW-M`VQDSdMP07}KuUqOACQJj+7 zUT2!yPo#t_5Tx{kkT~12Mhlj)bt2;iz?8!+xRqTg_uOT>Hj0z2=ZJA`mKy?oqlbi$ zRzhoGzq`$OV*@w%g_VuJk zQ!e{u(?Ud;%doukmD$zvl&Mi#dRxsf7J7*cGJvABDD%x}f?DLxmRZV_>&~8p>Gwu@d52nI?Bj0PJ7`uIU8-(S;}4eDLeef=LePb!F8Ho3xzVYY0$pt? z1vTLwvGp$%zPwf*9z(8$yvmqhw&PZz^|dyjje6ZrgxXzKnE1`hYSR^Q^c?u(Q$x93 z+Cr_7-3xonNO3M6w5TB{SP5TJLV!u{kEUwvOy7&VX`K?+rXE{LDe~hC{OyuC?c7$+`Pb6i&5B=(+`)yZ>?K1C zRh}SSwqb|Ej|SF}bcUO7x}>Zn=G3Otlt58~I%TsRM9Ua+3}KiyDuv=J`gP6iqX~B5 z=1N%c7)qJ(l$M$bNbuBKcEfTLm!H#D zr?OU&<-~cwuO&sLs1lLDT6%NbU}qc(qvYFe@MEYsWh{IZ4;RL@u)A4N1uzOi+zQyGJA<(RjpqS>p0l$T&fGpROC&A1 zX%95>X$cUZqP3+U&Z$u!teKGpx6`G*Jp12dAsVAa*fl~K3F<4jI{{1SNY7WmGJ z^<4X7PN^YHYTG5c3u#h^JxGyseE=sa1zrCDeOWISdV^s6V%l`(m)Wgdvlpv`S46Ab z$^A=hw%_emq^ab*yHpr)rG8M9l{~>iEoZy@Mp=pTX);K;Xb!&BpzNFc>jW;gaH-}s)2ujr90n{bJjV=<|BDdTpt2a5q z{{RfjSe_SV(>T5DrG>@KzM%BF@)A5WJeSZ4iO>p=r3sXPpbb&pDg*G7r`%ZVrdT%T8;(Q(Ou25ZrChRv`BUUJ(=AMPb8Mg~KPqHA1B)3M z1w$h^^c75P)cd8fUBYFi?QpbQ5pHqzC0pNmGv6ZHNc}S|wzQ0akPk?}JXfgmhfJ-y zhtLdHBi~-PVE%B+EZg4VsJHq?i`%cGTqgBR2MKtzv>=dQSCt_tSxP|~f-@Py`k?LW zEG^qyD-pT3x3p696k4#;tQHWhDF{lpAtfpED3YB@Rep*L>wO~cLccHCiWYcR+ZsRr z0GA&B0JGkrMyk`mg^T5}&Xd%liHK1EcEU#)?UDU8`Qn}X-iFZNM&jy}Hp0pPH_MiO zQb6e+Y0h!mC({_?h?w3XcgaOwUgc_*@Qj|j-NXe9j0BIga!!0Hrn6 z+%rfpb>-WR2?PmE`659F_t#8cU-0tiOIdRDT86=QEe%nz{;G%`^SlWa`WmNF*&RVp zy2DZjt8x;qJLKaW_vD<8+2G^tHzsAZ*Aeu*(uq%)j1k9lg#tl6vO(g%JqUg!_x*1* z3Krw*2c$@K46zxDt;y$~dBuj>WtPzTmg|l zM(w9eI)j(JfM_t4$a_TT=S!(MRVJ)0K;{4p)I@6YxFDzbo1-BCY%NFgp zlF4*e59`;&oBsfqw>f_<*j?b*wq|j7dzg^CPm61XAqiF}Z3|6@6L9)mX3NA2LYrpk z*ATGc5jU2UrF|Z+o7AOYM~Lqp8!h2Wb~dwZx2fBnnzQc3n)5!>xGgZNhMurbvs~@g zLH5QXMx&U_oumf-?riezEF{HM3D^tHrNSST8WyxLPlE zN%3x$o1_US4o91BxWaY#lOnu=6w~qDaiyn}sMP9h#3c(-6cFGg3M0y(2?h@Yf;SwF z<8evn+nSqFl&L8~0R$gWkMR1A-^ahYt^W%T?{hv$t+LI`>LXFm85(rGQ~B zIUR|RWVa+;(6=quTXdM0OJu~eI)4n6u4WK_DW0?C^(UZ-3w4xu9i4wf00gM{*Wy-3agt~qNtZuSTbGZjp@hSlX2A;RH5 zYC&3oc%+;qU0Fd&Nl(f-2C!nOc+l#D1_Wt7#I5*`q z;i_}#Pc4>w0g2zhwrQP3^#_4c5)s%stC@3FV7kJa%-43;BE(-V#Wt+8(=7clHU{M* z!$?}BTid2wQiL`X*(p|~E2&T5!p<=_H^pKC){_30+7JRpQm-!UxCaY49V6zz_a?b( zu9E)%gPW-s(_4q)YFq2*4r$%03086bS9R9}7mWsxmhlK4XAP1Fg<6Xo}~`1l#`x7I2bwg1n@q!lD3PRYC;H5&lo%aNg(s>o(4al8rAOG?u=`q zn(`(#kdz~>0l*G1!NEO=81(h;-m>RT>&D&k(jGwx;HWQ*fzPkk*BSM#Z>C*P!1E3b zXK`tWGW$*?32o5hf24q&z)6_fa$rb4qPpi$G9CL@kNAu&3k0o6(x6d0=^%k3PSM+$ zthH`iWg%T)5sZ*VJwL#E*P)OI$XD&)clQ-#VQTa}$C#WaA2(O)08cpje>Oguso1|Z z^I)aWPEQ2)BiFyD-Op|-w6l)ex{;;inE=elFhKR^d4btqAzW7DEL^g`J73n~4+aSB z1J~MnRcR_{N)B*)f-~FGKU{ywR<~8$7Sk&%xy=K@H zsVQ5orLYwndC7oJ1Kf^5t-mPcJG+O_w%JpG8c9G*%0U46VoZ04JM*xrD_CopHrJFL zN>&G09%l$S@6@akpF%i1_xjjNp5&BD4yAn{xT2&X)esZal9YDp81>_WpFk_qay2A7 zvktP{lqK>U^4HV=S4wi6VDe4|K+k`zV6Lat60P!FX~}D4(Z)Ld|b$Y3pa3TAC>>mm4WSSq^C+!j6)YAa}28WGc?^kBmwK(@mGPp zb!)g;Va08*)RYX!d#L2}gcFRPaG~Flaez%&VLuRzVph*RbBMcVaN2cSw{YT`Q;G{r zBxyPdj^Z{DNrMtM+s>NXST}BjVd#0c7opb@l&`}DGE}7~YAF%`iBXU{R!&|wHY@Ti zuiPcQ9vBi-wJiw^I^vLzEESa}KQ;jawsX&Fq;HokJ*wlpww*chTn(lkAAGtJdQ_Hr z(i9e~cRjfP)*$O0T!#>t?L10b$OTRwkPZh|PDn}PIpFX(;;4IV;&CC?XUJ_SKPhfQ zgXSHF%CYUnPC5G5InF(AcNMyN((eqn#jurle@IiLsZTlJp|X&rg`_1ANZM9r4j^42 zM~o8S0RELn&RP&uaFqa02LN_BL#%9jm2R1GjVnm5uf=~;28&4GZD zPZ${O+}1-}cZU0Fj>}NinDW_dHuKInWf=)VKuKC3N|KCx$0^A@UHBDb*Ww3TKdE48 z*fs{QGcVUBq6cFa3)!0?wmO#?rVC|;i^Q#J%214up5n0?R@WzEm!X^=F1RmNloX+* zyoB5+gqdkhx`p_KN+Cfz5+Sfe6T4y78P5*1y16OCuPxa^-6r8p+q~N?ol99$dK&P< zX_Wbty~dyjDHzFYuxowL)-d!vip6q?bbT*A`H>aY+lz0CfbGIlt`w-HEJjM*9Z3ma zK?O-2MzMdzH;9cru(s_-vU)!IeY-z$LRr^oxmp;uvo>5$F)>@WPG(e>zRMCy<#f8r zSo=dtN}WK@NZD_WZxH(>vb$#wNfzX@x~@ZUb8p&d*J91LO_a-DXoCBYq{Uj0!==Vv zY0v_a6qg&pDLjzw*1fxHjYU55KH(zbx1V$-T%3suXe{*kfT>5Tq~wyL^LIJszJ~NC zrxmsWl4k7dO`xke{?TrKOZ$N#tA9}40kxIx4B*owdS6=!Gmbj zHe38_pqAt%iOFtLzh($irE!^I#Gt4xB_J(XJtTM2og})KhCBTu%Pf!4W2qf|_ zbN5wmCiJf{TfT=avYt0ej02E6R!``U;R0M+kjy=Iw07UK9-ltpZ@gS2Ng2mllN3RY(d~h7T+l9N%c@C z=KY7$`aYHHiEkv8hZYtH!75gGJ;o0hz`@TvR{+JoqX(ic-X`-d=1ljO6o;C54I>k? z#2QN5aSj9m?Tb!^T#I(qjCQZI`^3@t5Q7PDuMt$hU zD%(SyZ3k8p)O5_PQ*pcMIG4$?3g$C(x4O5wytDxwpAOqbh^@kD60~lbY58p2pF{6?R%>HxL$>4~N6*YsUFz#uB}XbXs3^JfQ^~?g zTvjaA?vI`DY*{>p{7tYq=cZMxJo>!P?gu{ zG2w@HbiSk5y|0%3p|zQ4(VB7-$W+GqlH?mK2#6j$JRJAO;f}-R&NclOmze%9iQ`ybRP3zn zFb-gSAbo({kUzXR*RPW zB`ZQ8ps6Y(E9-D0=U<`N)ixqh+HUsZp`7Ac6_BhEk6hr7UJnAj1^U+O$z_S`IO_OI zXj)cOtZ~u-&pi5jlgDq*Io}cc>kUn-_|4S{Q9x2uo87^EBMBfV{inFd$oC$->wCWg z)oyn1ZVv5k-r{PT{-V?Sb78aTZ7&|_cbe=!&dp@(*X}nM@YIJ9CS9XWS)PF7k4$B# ztZJ0>L7wyN%CzDd{)&uF&HGEtQ>yrL_&ax2s>E8fz^+=pddyt3o3^Z$<Fdk;+m)9e zxw(C1Zo1-+btsrhLQsT&ge53SQWUCJUFvk`oe`|HnxPw#bdHtOI-OSJ$PraNHVZ_{ zsf3wDnF2ACA{GRL9q`F>%q+Xp(5z_io#U6o=ZtMQE_+j|kjANn5xF{(ZFL!sYu9>h zN(c_FO{7S9yREGbtu3PcVv%yRElNW!%3@)1BUymj)P*<@qJ$6@pd=)!V^nYqKqqP1 zX0h|z*Z5Xo(v+zwO0gaxcjrTRaL`NK&Bu+X&X0+5g>AUpH0`yktCra}1s1LAof&hX zEvVBZgu-e_>uc?=MvUr^?5N?(?^dIIcCy>O`!~6}Np@6bqizn1zv-rC%Z!F= zZB|FpaQn5g`dL%>}w%7Y)493RJb*EiK<#6p*Cd+cP?vQ>{6toq6%l*h@0rMuXD2w)nP|IzKJiowXs{ zXXR3un=~ORKgs~$9DsWsdcGts`i@ly&D%uF}L=;V{eRz*9lXVa7Lh)r(RQ! zCQNod&Za*jqy)BB7v`v_f{~k9deN%0-t`5F{?g;$H2nLumhW$Qr{XyR;cAxb8R2Zc z)Ok(44or7jTzOHWyoH4!=9^knfr^{C3Aj@%=xNlt+M1kFq>y+Ejo$=;o~|%|A8LIN z_($kBNONWzn(}S|ik#Vlv3%8@HH+gn*RQgiM-I5d??PHZwz6}^v$=h#a?wE{%}r@i zQlajyr02}*j^*o%Y)1DFRtbokRhxSz8A)4g-P>GSDaDmM=>0QougLEyg@mMpn?FiY zy_a5guh&~?uOY^xxVILj*MbX!3kN-lazNu5$;Nr?D>(Gc)uPi$>Q7|g4xIXOJ$rT@ zoz5#xn`OIDBDCBiAuX$@B_(SFqmV{Qk8J0^-P;wO8aHqdBQUi|ZEJNYJTEyvLNI$| z;~!62@kc;<&4A_%4$-FZCLMjI8v05@i9Q3Q08nLG$(ShzeaM;Gz2DVrg@`8g^{U;( zA;%ON8c5Wgh*;Z5@2UwNhmte8B<7Tm3X_A|2a(&q_f^ERk3wa^&>qDi}WMIs8XB z_vebGuJeMMQOH3^KY8`YJQ7bGxxhbKl{_^~B=5JcJIAoAoC_=5+})wJ+m&lf z06Xmi-@15-fmGJH)H5N6ooPjA!6Slm`;WFi9`%cQk8#TX07RgM)Y(5UUqh-O9vnym z8062k97Y4);h_d38+w3uL(v*~ypdr+NRHE2X0F;ej>`umg zfzxb`i?0of+g}01f#I!4Qe|3Tr2SE4eo`?Q;PDYbfGBfOO%WlCQ+z zXYs1DMD7%5(%t;3-A%E*(8p{`|nw=7j)#RGqIj|#k!W4 z{!~2045Wgf2^k>wN#G81+Nm}*?Sf^!t9*%#zcIuVzNeIwEvIsF74sYvp?#Y^<%!MIpBmjq03FkdW1Rt+* ze(J|(4!qtimMyHqSgU0ZHtL;PoJh4&n-ZOrol8mxDpFMd>{4pJ71K*c{AV<-#A<24 zrjU?Bs6h}!0#FL%1gHW)B%Px$FWNTWf1aelpFR>DW2rJe!qd=$`B5N9b?g-Mfr3-q zjw=**S8w+Mz?Dk2#<$+!b(Hmdtz1=}G4^JoIi5;+4jxG!gEN~}wz%o$+;MKdIhPyg zExj%DC8dLqv|y{8gpQH|Ppwc|!+!N-MiAjKZY;xE-9%?n+dgN?QjoV$$yvrh_2-=T zHO#QgPa|dv=ALscC5%Gp)fF3Z=xw(Ol8}YUn_wk!R@rGFq?mI^N@sY%@C?~waFkeF zT3_2Vl%*-XLK`kXCL3Buh?fZgGU~!a>KcIC7IoC!UttdZR?+vE3sML!DjbfI3Xje@ zkdcCT7##7Ad8$_b0AY!>8)*}Lw@P-PvcjH(&e3+8b)mV>%~C=gi8A7cq(oFW!Nb%R zkF+66XUiak6I%+}J=3sVbatTKed)2ofuroKN{wxaGxUb{F%2P0W*f2)%AHCYke7m+ zQ8_`t0A{(l@!#T(#ko47>uTG1_t+Xs?GLohpPuxWt?}9lSxR9hM5zImh7uCnC~X6z zkTOksL!%u$w8d}HVsLjET9(U;MyGBQX=FS#i;KrXTyEW0DAX)a9a@}%;+hPUDDsY} zbsHDKZEr1KZLBrR6}xM3aLbLjF0O-QF3!qUO4P~FZk!r|0*VsgbU?>n6h0)bJK?Kt z8lv>!op_e&-IH=~{h2bBwXfzx20)0%DhlXiU~(%Tw&LXyE&KAd>4w(gPo-M+*_Cy;$A;NHbMhN$2_)m@SQ#e-jCUlC+~XWpz~Oi+yW3i)0VMu9CI1?Zx$5cu|NXtN|gp6~V_cmk1@@_%F?690`6K@D( z8dJ86zNB0%rA|VfUZxsxHtAB7e@w93C{l=!V!TC!FEZxbUHo3h40{uA1ye5l6$osh zYD5NFaVk;^sck6-Kr3-UBed0-R{dX-a7DJz@}fivN?A-rn5DF(T|=RkQj{Q+f-s(( zaf6Dif2#X4bP0c@!%LKSzoEtC+h5?EY?G!8uiaf8lk)V<-CcA18Y<-Xj- z(lXjFhpqBnkATadxQ{$6M0sE|Ng={YN6X(Q z7{}76+2EZl#oS6-xXU<;ET_S^Y1b^?QtpWam(cQzICLvzpb}C{5=l3>j%?~vc8?laoBeK^(j4NB&d_ugZmCneX;)l5bs*9JMgLS`J2P8V_i1N z_kKE+t@|dO!<;0aGK2VI)~nP>l_)AwcLqgkYwBimwa|vK8-`X86ns|Dv&VMbLckih zFbqKL71pNS{Cu`j?uHKIXnXdVb!8#pr=m!{HiDsrbgE16N`NPWfH96RYcO=j#aqEX zyJ?k3)=t}6abGGmeWKX}HlhLQTXHUP19-^E#zsAO#|)z>KdMjDu|Lh&^Y~Y{+8zKT zt$6#$IOO~P0EcSx=4SqkzlWS*)m&gs=NzW$lt@M$jwbTepkf1UVpg{=fS$#7N+cMP zcddsE>#kaR#?YaZjFM{od^m27wR_%n3vp!yXjny-CCgkS z6}L8G%A1thR_iMPAmzLj01~27NURs$3xvCl;c6)|)SooZcqI+Va#9*dbz~tgxrMkA zNdamAp7qvx8`5ijl;i2?jjktkj%ECrm#uFw+pJUt;t$`I30D>FH%8- z00#1Y1$&JLOl}_NST8qNkYio#FcjFABs3Q5Q)IlQC#;OC2u?`BS0I~j+=fGKt*TkGMvNd0WS<^GAJ-PBnhnl(H#y!Jg)zcPE63X%>{Mo7V5(TD#@yIpNt- z-lMu~pG0lF;;2X&D`}dCn*)L4nKNB;{}>k%$B}#6G%xQ}_m0fP1DN)1Dt8Y$mT5-+ z0}Ro|$zT<3tXp9;runrm2bGN3lLB1qfPXuN|B?L%5SEvUu2LQf%#zkB?Si$KYDPlV z{(g?JyQzU2HUyXn<=1~;XMF6&#mWsUk;TibWEtKVA-J{Xgurq?j`Nxa8Js3CPS$x& zLz6i;wU;H1gT6x#ojMwUislxbL9Acf+P`$P< zql0k1Ib(R9oxwSap3Fb^O~vXed6-%D<ejm=eD0N*tSN^eyQI)wsv70nI~|i*v+Nd|EXdO=sScH}9&w^He|949S`L9|tr$_r$Xn#U4R2WLuOkt)u# zRe6yeg?nn_by+8dp?5$~%^Xb)5?z{GG{ide*TqwE(Elw&i?+2#&$w|oH0=6p@>j{lOll49K}@<@ z$(sO%G$cF@No)7smr&s5DQ5Qu)_}HJVj({dCVc(`kzd&V1z1FCVIcsI7O z#ehM&M#Y&jZlp$i6LC2cN=>tIt}%E>7tm*u{HTqU^#k&a=d%$$7|XhDf5*+ne+xwsAr3(e z6`iis*MLmo>F+M`56hpeIj0 z#P{M(*?0{T+&~lVT{TRZF|KneNc2MWLr+rV`n;W|4gL~>58v~&Z-Qr9f zk=Se;keGgJOev4~-sGO`4czVF&`J`p!_%N)5D^g1t!4_DQZ5xg4wMzS?_5W)vxc|w zy<>GFbE6+Zw3M6z&_@y@6O^^g&l&^{;zfNtSgEw!dA;}na#jECh6fo@LNCXmkUiOF z6;~&^7SwXVn4HJ*HdI;>?!1kPRNju^*d&}7u{!R^X!ILdIXU{XfuNOS$1of8l{bsQ zVkR;psRWRYA@Ua7^BznkCaGO_1x{D)Al>%Oz^^{EIQ0SDrePRsa(fYuBE~Xx0WCx4 z9&Z*grs}&eId*C#_=XR~8YxY0*P8$9aPbWGt=R{{rrry>PEJs?E-?^wTCV0?i;DO> z{xF>Svgr_ODy>!5{28KSv@1lpM?JJrb8$A<_k6oP1$@*=2hS0!$d$7pXi>so zhI~dsh5&oC2TA%i6s|Wjwq$-s7~DOt=_dm0rOKU#Do2uJFf#7u8J{<$`=F*TMhEH5Wn5afo*%tF6q z`ow1)2iHxlP-~`kZQ6(sehLdRiH<+q^yCapm_LI-^lW-p1fj1lzwbL|_55;q!ew3m zbLNdS@+FS~V3?CyNJ)9+&RCN*7q(0SB+#dT7pr_+TBZZcNP!NG2dcb1SP}lQvNL&0 z{E@eb?~CWYA>Av}OP)&s_T`U@fOp9;(QW&7r6QX}Ri=X}sNB+S-! z=-aJNB6N6*GMCRSy=m`Du|qZuA2XeJC7PnHOfwuB-+%Cv09j=)OuF zYAQ-1S0zdWe3{Bn2q|>T4Y`V-)(aF`AmPNl&0EI1i;~@wNI(QYGRCaG` zYud8ezxaJm2WgFjkqGg?xNU)H!gn}~*cHdUOdnUnd5X(Zv$E#Ale3P8uUf%pc%Q#K zohNT<;7%h|hg@oUrvXSi(zlkCE@l#SwO+=FRU+@Gmf`*!wuC3&F3_f5xLa z5Tf?&ZrA|@C*zHG7nqYEI;IOe6*`-j@+2w<14R2;B=WPM;<$ zTA}acq=jj=-6jpDBT;Sc4}H#He z!-iK{I0qeZWnR%O>p3qkQxsJCSxjQe97bR*X3Oi5vev89_@21>(T2&4w)3Nu%!3~v-RFg4_r9f5# zSrEMX_|D>8OwmUWGAX*n6qBX&*2t<`Ggg{%)KK>IXsFa+W7D&vdoeb3H^o&U*1VH_ z2g6?kcWJWMt#J-x{iASn2vTV7@+Y4LhrnhmwC z&oW9QMl5Zlol;aCK+0lDN7C<(@mx~l{3HmB{`lA>0*dMri}CSc&}RMCnDsVnIHd++1ZkIm0kMs57>NM4u5aRioNwp!llMr($E3{Fhd`JLJ)=n7g*9aNQu z{*=r(&Ak#5XlR>4@cJF`P);`Fvw^&!kut}AVAd|}F?VFqeMGRY0HZ|9V~8lRzn}Jg z|D4%>N8xT@T_vkk6{P~s5KqO1GY>0y!S&EoJ^$a=+ zY!l3d=2a~i&IXA4FTX@yTCz+R-4vu%cQ^z!9gU78RsUV_`8c_m{e)P)_IC#VPImHt zLfEJ{NE6JKSA@h|XcB2h$0n6u(&cKKX5MzR(M$pSzML6@WT; zsv8gU%$!O%`Ny>u7s6X^$^=RN`mhWggDIg|@ev8N9?d>fU|n8*QDJ_9Tg-1tkM*13BQ%NPlWTA4TNpMcb11`Jl0TDv zJxp{)-(6Ld6+Y%~oqSJ!|8G22M-*nFsPUXj>(Mni#;0mS#jH4a{V4F|_4i`WUvq*C z@Lr4D#Cx_d#u%PV92>+tB4WD6Kw9}(IZ`NfapWs2bNxG;X1tvFy2Z8t zs(VjB6$tPK2<1sx8`9VO$)*ywpKE{AtjTWdvikt)Ho#Ix>kfl=Jr-!<_Lck4vCKGS z!2ji9*#y+{DYT4Xn~fhA-%3Tfb)ufhyvb`%jLxGc3z3-Mz2R znsQ8el+UX1`Q_>wAb=$596Iw62vnzmNL@_Xp}+X+VJCQhP=*lR8`C<&Rx^`+5p#~mRBetCtLG0K)SqF zwAb0>4pQ%ie&@#2@OI|~Yg6Z+=Hlmo5T+3DU-f{lu%(Zj(c7~gpQ;GW%Gn=LN9U{S zx2_($a;Y@*W_`>vr(t{{p+2Ptk`Ne~%DDeG?_$m!)qj6t5~czc$qkMSdD2SWnOP+}V1j6Z42H@4;WL-3X^eeZg-#x95`vrk4^+$%2d9W<)&% zV1GXZ-J91F4azmlnTZ|ZUU0CVnBpVBlsGD8o>z2OiB~Me#Fh+m@|1r|nfeU;s0f_V zkm{7FVEB{pQ!VEg?e~83)agd~irSwoQ@VSDtd?InvIUvD*_y0iZ`>B{y@P#gd|D;N zvYuJ-kat>uOBePvx1nqzX|^f7NQLv`f@k~iCtF#<57%U6FKgD2Z1_Dgn+EIYthA!R zWag%;7iGI**i?nS=)Jb)J~{^}Wjz=X1Jqg9_3L5edZW3&nEiZ{ll-F$2`(qs#*Qnc zzp4M2C$kcDPnthy5(FDN-Y87jJRUMwwyLIYcrI-(zG_*}CsRHmSheJHvDBxNTl4Ru zEy_TUhM}?t0>~`(2+e>(1XtoEO6N|Fk&tPe;?EI5xT3t7meaO01`>jjgfSw6%C*`;$&Jl2G-tFa6o|Cx*N zJdEi@7f5suaBUvH@AaNKIGeD?bpW>kgZAa(sJs3>i*L!<*rT*i`~0>m1aAK{^Xkqt zr^HQSL)SeeU+tucNM$9v(SR37!*1Ue@f)5Nf6wpH%IG)n54$%-wdZ#i$$f_k8hJ;k z-OyDuZ`OyQKc0@(eE3Wv!b9gHwg21Pc^CC-_ZiTvrPxUY3+_Gl#$uylgmYqUi*#0oWuVAsK?PFuFN@FHxU zq#`gPBBD#JlUYpsDq~8y@fDA)waAF2ItuoWrYMeejaL$RLS$@)X9L~HQ0D`3XLTdQ zyrY~$YbOTR?hv_AE!Qt zk4EEsl@`OiCDm^|sO%!!pxoV0m`mC4Ogu8ki~yDlqbDS`2aITV?0zc(0F-vuJLL^<4|Z3q?#Dz}GgocyOgB=qSq(;!0rmSJ~|2={67w!_ZKaRKWl- zFdTnmBg9jZ(%&ZdEvFNSY|*sR%bdjyyOxtTltE#65XDQ=szT-4Lg5t8-y+VgcE8#u zE?W~B^B-X<8LrUvXktF+S?&61MHHMP*+1jLC5_fYR~4kS!6l4Zhru-lFJBv9#&2%O$(!x} z=IOY$b#Iw8^=2`}5$ZIpJ@R0ZN<`=cbr<){oTM5y2Q4Q3uf?eIwnE&JA#R!(V2=Bl zrNQIH{!U0M9=c++RQ}vFpH)R#o!pT@`)kKGS^pBL#>0*ssl%moZZuL4I^5l?NLEcr z=4}|ly;5f&;^jO$=uCgg2@u6#bnNG&zGbA}0A`y}OT}by?AFnMXbfo!g#q)B!bEZK z?*X9yM~;hUoOEh0p9wv))Y(YH%4D_EA;4c9$6AIp)7pbzvt=AZlT>Tm z5PF_d1%EUr`!`L_P;6uAD=Q#ykP-t}^(S>!rWvQrD~(XIwh&S({(d<|nd9bIribr6 z2Nr=q|MVhX02=%^q{W8LpevH%ax>p}!PE)!YTOA!A7?prL((dPUTd5o+4 zooCV)s*84VH&*O6#WO(8fiLzEzx*FtdsbRkp%Et)a(xjZo0o8YyaC8gCXNQiw4VTl z-J7O10&u@DD{h$feCiU*g#LW5UEC&Msx<~?$A1sud5QL=7Y6-JG|zl z)k;;dOi0ug?r!!pp(t)@I?bpgs3j2e&*7=AQZTOCXPn$A#>!f5HoD$4iw*JBvp4k~ z3NOHGs6e7ATMA^bj*0HNtesUk3g52i@(q!k`NxIXklT0Ll-wG|0GzTyisapwV=AGr zO!Y~o1tUHh@WsAI)47$W^mn-~D(LI$+MXegwX!hy)YY2fO(V^Or~BIY>R8D1e#j3C zMahmX=G!v3ksw!N6Nsl)hhrX|*YL~TByd06i@9)XJ9~Pn?3hD4M%U*%J7iHE9>K$Y z9JlUoRsQqTvs!RVY5A6S)X1*7R=7YP(d5n9Z4@w1`p!fqgVxWs_)m+I zBM@{6yfs8;!G8?KfCmkHC>i=`U_|k7z`cLPH=k{4*KKy0gf0oMco_k363Xl0Z{9d$ zMxlPO2f?k-eZbAs8A{@LcxXb1^G4;$Zd`MB^;u8W%l;v~_fYjpJ>|N{bLf9|dD}+rWa@iaL(_K%+6PM@5%-KP zqF3i5)^^g($bdheM_isK*g~|E3DsggPZqMT>cX=&|HS7;=-Y_%CkW_ zUC|?nDUisO0!BM2@rNlk0@@~d!QAeSasx|~-(W3YYDk26!UMW0ZnZ4bkByUmlzqFr zLfGP#6CxVd(`CPbYpi<`;6_212BHV$R4R``xV7)C>+Sf5QTUF`^le?=839gSDyVlU zp87sD3_`*~7LlU(feYA!3q|=x?RXF&nwt0hzFGzcOvO`cc`-|G=+|{p@w(Gm{!-D` zZ%Wg&rpaABQr+Mt?`i5V9tsQx>8pvSFr|{i(1ghi14<)P91lRHK(f9xvMBAj%0__8 z@z=5qWx;`FuWq7f;H{=Hi#BT{Jkq);BRY*l^#+HS5954m2uEc2)E!!vOs>zQlu8CE zX{hpu!L0V+uPpX+7!iGF3t~r~nGsmaTw;ne_0M(nt+XP-O4IO;8<7wmKh7Bq_hoRX zL+p9S(npEfjUi$9(vxv7?x*H7)WP7L(w+RI8`3(3O!d7w)U@;*LW;#)oZd((=K)>s z)R`lfh-R*?zJj@tRsuuF&{Qd@nYrBMK(SYBM#@>_Sjl8}n{JX2Lt8zaO1=zs3G{@d zt{xjhP6f>sf-rx&`+)t|oE3I<1$hlvX?}JVjR~jUqv?f@B_l-cnLQ*5@?SHW5 zx{d)cDKROE)vlisbWbw5t+2M(rbv!y}xas*=0C2tu2kWw!vLCRyFw) z@SoQc#2pr1?bleA9>E3AG;eJtJotx`V|j-ul(ONuV$|*Z)GgZW6Q@H%12T!bU#$A3 zyc1(l7|mR#t@@tmrV^$?42eOwfcj5AT`>zhsHQfUyXRE;*ZG1>>r$c+;z~3XOD1>T z`rK52w!p=qjJ+y6)-;Tip8-`>G^m z(<#iQA(ijavWle@w+`_@iqGi}`fCtVDRE{mA}E~!M{Ud}HvoeZ_KsH&$(O`|pQG*D zMd`BPUsqev)7nWyr z>T?{ajgzPJp7~UwX6NIB|NpA5iz~&;l|N_AiFZ|@tYLF+`wt6w=YP`AfR2Yvqg9n& z?18%*5bIg=WsT2^-uMlIOO>UOcmAH&ti|xQ7S@qEwLdW~F~E(NE%IkCDt>%^Kx7cp z{3z+VJ0MjO^B(orSw~mm6%u)Hy;D+|FS*-|(0Uh-uS^1TPZwl0u;Ah^e9R*x^!rf^ zo^eHYWXtLgKh+>)ZPPmzedV&J>G@a0{I8k2!Lt*7M9`Rz%2H@IWA&3PwoT50SsTP% zenql7bbt8%@Vasl{$@1giPMr7~s+(un~68{g7Kq{{^xKQ}&^1kxl^?O%^B942>Hxo}Af2lPv!r&Dm?pxJ; zo#9*!2FFKLZ7-}R>RVsUUJaOk92{l2Pe8t(&QkQ>;gkLkumrDVYb+6?h!S=xxDrHY z5gUKdJXOC8b(HS);#&0tP6_C`kD z#l-zR<0|lwCG@FpR&iBpbJam6!C0#IN!VAH4ujgy*4hYfnPquC4HVj(A_QRRDN#1;?tlN-NmOYx`&1tLyf47 z_SV*lFeF2}6Kdy|Q~u&EoXH_cNYPbmX}InoFSj$egH^B!yF6Wi%U@1-N5BrxN7I)@A%ja75ZYK$%%7!LxM7vbr|fM<+TY3b={p{nhn zbQ(P?!lk$G67q%Rs9>3C6*)=iIXM`0qMDlKtLN(#A=@{Y2SzYXvNMS-$gL*BJ?ze{ zsl1*Bo7&dYY^T{BrMgC?AozDx0Pw*KjVeF?RH_$Z_daFcdn4%&?lpZitreQPyoDw7 zzhE!3MZJkTU~+i+G!AJV=BQ!AcMpX$=EHtL-LUnf(1mubQ3@q|U z!AkS63JMFkn1F~-s)(w!-IU#gT$zh_-t6KtXZchlIZfPmjTDG=22yVdosbu2>$Pe< ztyv(xFT>8lH46gjO@-P_vJapl*eyW1@CNakomZ3LWa>r|YV<)1x7`B`IYFMgh`C;nCKca&eL=e{z!uMz_$xb7p zh@E?cE`D#wC7d@yztoBUr zaqrd4I~nMsYQHdbhWzSso9@zp~D>a>RBp5ACAiADRij(*mzQwd5MJVxz<};vvms^qQN0-{}Jhj4$a| z@H*eZtJ<(DP;!USeqO+vimFRk{GkiZFMD6ey{$oU5I8JXIF{vBU=V&g?M(pY;eadH9Tu8 zAGxS|H?14Fl(V<23l_ychcQK}Y{uZ2Vthd$g!*M1&7scMs?^QrKR>yiA5Frl(ae;r z>yZUMm+F(8aFQ=m_B;Zh@Fh3mCML~siS6mCSub|gru*mgzf5)}>vjzx)uIOd5VO;cACN;hex z*m*8`r_KZNTQoXmD23hrx2FloYnnJ?EiQM|Y7sdPK7+&}X*yWr09zEp$+sUpJK^z`(uR#qF|t-x!#pQn;h8lCoXY*j_^z^E920*QL>9 z>Y(EgVFTXETk|JEPWv2Ux|d(Wt-Y#2w4gzr4N9&i%J~O~S&w77Jm)W0{Upv=)S5?Z z`c3vvPRS+2B6OC9A}3|f;(7$A%C!L?^t zu&ag=%@48b;!^tEHkJuVs)_CX07V+-J6taqBBNhmlrPUx)6!aU%o zq!&5CBC67*a74FOZga_$YVS?GLQ|xoNg2wp?JO|fmKhTGYU#Xf z1ra8c#U_I8?-&8@&_xc%8F4|sTRR4`-{i^*@^Shw%`T=2v@tb;`gt+NoQVr-U9^j8xYEKwcgCJa@# zRMvhl%{db4;)0G}%QoS1^GiWi#(7VRZ(;d+Cil1OQKEftco{daNKBkbqv9K`zT<9# zO8PTAJ|>?Q@^M6Q)f^y~dus6ml2E^07^d($KT0gyGcQPLPwl zR5*q{{a;93Fp#n9sP8#Ow&Od=FG0q&w#nIfumV2)B+Xkgl*t?NyQ?p#r+qKjaiXEG z-KKEZ=}S#<$*3CyO(sglyC#)Bj80;Z8mh=>SDs+`S=rV)t1{+k;*meGl^A^6PT)M$ ztobZ9JO!Kw1> z?-1EP!Vb>9u^&1fme2VUM!zm*H-CMa$`%cltgx_>f=y2@^Xfx?yeL}ZPRD3elJDy* z<34&}IQ=I?^KPC(NN81w9k`RE-y10bS1xf;s$H{6A9`hDzkQUAnK}WF_f;k7Gwntx_?nv^>tvCJ=r?cuu9nVTG1qbUQ6E{Q2T~Y2PS=eQP-R60G~`5? zE0iixS@6r|KFifwT$5S1G1SulImEDl`iZ8{$mNUJH!p*=$|VZIV1NW=Ggtnw)<$+HEtfbGQ*WHMyIe z&PsF?MW?$NJYP7J;ts=LKj#DJYJ5`n_Qfv+F`nl$~8w=#iuBpQ~< z-o-fl3a&-O8THl})VhKD3GSeA!52q_-ek$;@`L8tD?Xuz4%U@lHXmzZv7u$fDt!VS zbyBdAjo+_$L|!e5rwOSlJ=Bx=^t@4~*mN>8e?5pd>YT|YhZ?#)jH z_7|NDqu+Qdtj6CfM+xlTdv&H$O}>%I{?1&u75I;y+?pc9u`UZcthH!>2hOPM22Ub$ z%HCK+Be$^*26k`OCI15y1w7L6JTPaauGQUxUUdjX;{)7;9{*%+_p^b)8y7Eeu+67` z)W7XiE*f6v{rq;A-Yh%!#7E*UkJst!fzZDRKd;{qqMOq0KfwOm(=O<+Sbb>iKpPP!B0bJ z>qRyd7CP|7^f{eqi3ddBjf-XCwOgr~+nn4VXjU1a(WLA(`($r)=DT#M*JZxwhCA_% zt8VpAD-88w7s4LO1DAx_?D#XQ$zD;T{`|5oH*c6Z&|Rp+U@S9p?MuN`T+s4-<`|ID zT|J1Y|5mL)L9DOrf>8yPRvFIA?9xNo%K{O`*2eeqQFJbkOx~z#6T!tvdRUgOvI%Q_ zPN+;9Hx+PKeY_9J0P)U%=Pw3d$2c^C1{j|=bfergn)`6ipj3>$YEClhhMfsvA8}k6 zJGof(GUM`}zi%GAuE@fJEJex?%YWYe@tNx2BX~VQO3XF41njEzCk8y*qTp0ay?^?0 zwV)9iT{MKGD=1@N_40iZPfc!2yvRPxTuz;!s6BT16!b*tLUqEr-P>oZP@I+4T~J}! zA~$EIZhha?1T}6)*lLQV0zWf!{{4_>EA*byogMj7`e5cLqOp%&N3O^?=o_k#=R$VV^K) zKY&w%hs-XeTo@JMCvhcsou)1#8tjSN+`=QZ4aKGG~DEt({9zu+~mP^tf0I4 z1UW$e!_S-!oEEaatt)InX*cg`c$-CcJ^Nfe@HI3;EPr+%NO)6F0q-TH+8VxB`WG#q zXzA3e0Jv@QOq7Q2B~IRv-}|vJ6~Eh3X!#G;&)U+f_ma;!D~&w8m+P^DcBKuzNx}nz zqOWpYw%^W{?|HCJ<3Q8cYDLv+0mUxLtp}gpwwt47k||7#vqRrElv7V+PvxwMaXPvX zydQpJlf*pXGSzFwzgJt1ksmclA@N?5NyJH8GKsrFuN;D0e;1D_&aQXcuWt zlcE7U{X7oYp^UN>9#C?NeKu~*_Z?&Wy%Z19!S&q((I}Iq(-(I|Qm34rEwH6buG8Yl z=>2jB7$Ap==}pUntAv;RKYHSYovO&zz%ujV!B^~0 zOihe1eS-rR3<%H;rQ8@p>%A&ea2jTFUn$)03t}-%2Bb}XrBa^<-jzMMG<{e6<^iZeCIS15D7mlj4zEceF}9NK|;T0-Zk&(8Y5;m zN~zK?Z6$!fWa90jfi}&MN>GI5KBe#5Y|q#N;Mr*S-w*~m)@Zq$&0~enLckZ{?z2y2 zTfQv!FQ>=$l=?Q1=T#NMi_U}yn%IgQ?mrt1FFb0$J}>6>SvadqrCtkXV4{XuALH`b z$RQ>{YaY+Y+~4*Jl@06?Iz)`Wu$=L4Z>B-w%jf`0c@8ildNGc|0G{uY!?{7( zsc>N1GSV*6BTxu;>z9uVhn`C%d3Q}g=f#Jmk9@D=pR;+TGh$BNA@VX&+ za);}&pE-s7B(>M6-6M=;(hpss9!#ql>QJqb6CWg7dpv60p-T&Lj6`M9Ff^iZaCR69O&kw6`-Ju>mIEx zs%fM1(eS-fkQ2{umCd&`bIhIW-$m}H#SWVzw2k-tjWAP3w4SSK=KX&ww3-QSp7tUKyriEyw!RBG_6 z&)i-5*V@=wcFvI5aDPg_-MC2J8Qon^CgS0%q66y*mkZLUpvWzfWo}tPHQB7e8j)3{ z$cvp-6jYD!&Qf!kIHrMk9dAthBj#V0E@;1_&~w$)#=hh2(}W@a2jB|5L|1q4 zzt{zA6Q=Hb%Q;x zvnmh$n*&x{ND>LKrM=ENa3hm_cgyz%=K4=|E0H z%AZxho%SP>+9*ThC}da!mT65ZN|z&3RG@!C)8V+0dnB|uF?InS8Qp7XibZ|NoW`>< z)1r4?LNtwJ!F^4BhlPc*K1oN@Qg6)9D>J&@UDt3`donGd1d^RZ*=xwkK z{WfcG6Be|)vyATh=qpwc#XkAb_fQ|8p3W&%Bi2|)ZHXVkYe<*=J` z+Y%qoMiAdO$Ar_A!5$PLy37C!TYFiuJv4MHB~)ZcZvCwV9G(6n@WE?Y4v&`cPvg!T zUcrNz-95?5Ut-RP@0qMA%Hj6b;69J(93jXj`fT|nE!z}eGS$)={?K^C(zsw6yu8LD zfHdk!YK{X1+rWY2R^C{|v%83S@Y+V1z3^QieRM_rr~aan$>(@RoesICQ9Ec8H?(hJieY5pb_aQ(5J*;V|dA@hqEQ&Dvx8BK86&FD0;Eh z*YsOA2V!lJO%Ra|AuHId%J7{G{E!hp>~;ETU1hQ$zAu~op|I~jLSbD{JT?5pXwMKj zuBmbB{*Gcf)PIVx^5HUS-XIQM!I0O1F(uivume!C4Sat}J2x?yQVc&m9`-I~S;S4_ zBcfx3%LG(1#-X<^OkZ;R959cP)=F9PJy%NWl)N2QhxL*AyzwaD0O5qolaH(X{T`i_ zwu_UlZ^>eU)00NU!_yu^K-6g;%3zZEexgZ+66=Ev1h+kL38=meJEUG=WTVPe=l9;qb))LiB-sP5CCwpif!kcDC}Mb;*W7L*=Tt6Z zZH=g5Sk#Ky&s-38qDF}bBfsa@@uZcd+%Ko7miQ&GY#VS}nycyVvJAGZ!Js~Eyb_L2 zqtdWu1`0x9`r$3s_vB!?ZqfCU>4&CpY@~rs)9;HibYLKqi>Ks15Aode_0P5G<9G;i z*q2}wj`uygHT`gjGwNWMsO8m)(?HXr>nlMjA4pj?s948@&59 zFOM6Jf%*U#afCpP?ECI;*>syXG{La3iLT12k|cb9%{$Wks6DZv(9ezv69@(^fl+(a z$K(J*1Equ)0|azLpMU3?MK7lWV;BqJ-AnSRlf2>=`k$wi6Y#O=pyYwcU;Ky$AH0qu zw(oDb_w&<))d{kNYV(2PTc0h+cH3GKO=i7&9ytct<>BdR%R=w4n8d+CK8HtLMQ8v| zw6(RDJMM)k%pXs*OkMQYiViM>F5$d9PyLbO6fzWq)G3+?VhN|TKec41&sR>>v4IyS zb+1+6-_Tg!6x?pP#&QPp?GrMO6k$`IfPAckA!fC_8XAw?;HgyFWpD;E9BRBJE6Zlo zl8>uSw57MEPy8Sgz98Nf2Y9meNtqe^q)6t9Y|mGZ0)uG)S~#3l%h@{Xf*~pUh6AC+ ztM(T}OIX`T8LiuFt_GabtrsMU>IFd&rc4+Oigr=$YSCX~&sI+Y6z8kyktK~B*vX)Kre&ox zp%$?{j-CSw>UTn(BnIgT|0>kJ=B|zx)GPlmVxK5}9pVyD{Wb3{;Su~W~h z!Mhh9ob|6|DxX)p_YnN%VPD*Wf9Juh@Gh_LZMT%yr-8IJOYBd%_uqA1$Qzm*uC6^- z9hn!&{Wor>UO7H;NU|ikfTqCu{-Lo+3Zs zS4s~*KjGE;hG(HK^;Fq!)l4GkSBH2;pJyi%fCHm*?)}T+q4&#XT69v+H+%m`kzw?D zxz$Q9@E#f8$NX$cj6EG9CcurAi)M5u6zaIp-JRX(S6;G}Eo1K~`RMoTKLD>Yhb*9|V+;e~)40V{XSKAXmx}JH>N#!oq(K=2iGjpMh=dZOyF?mAVw7~a5fbu4*l6hvrD24$ zfOH9{|8FtiI}M(sOqtzH4wa>a(_#}c7lTVFtzPXltNqePw(dQg9+vY zj@Xh56o!m|aSn;#i`+V2o67wP-t$Df^)5*T5rK&OQ%bZfMffI-eAS!IkNJia6#L8h zo}f7yz*MV5WH(W=T`z}F^t=l=AJI0ryf>5Obj(f^1Vud7QdV z`TS=c9lS#aDQ>@KKqX48tv!%tjvmQg2Z;$gjs-86PuQ*F=%cj5w-QO6#z?iZaV`j#dR&JUEDf_sjc8UklgpYDukTfKPP?7eX_O;O_1LU=J2UueksO|g&iXj8K0Hb-nEVxFm`m#z|5d5RHMP@3 z;){V32!ga_z%u^as`t&&BXdAvSx6gXHzTlR*a|hRB?M8iqj%tl z=HJB^Sb*Ih)x9=K2<6jm`KijgS>wcd#do(mGmr6o2tCw$&~UQ zW&0oAveeG&PYlN1J*>U22)`#P;EvP<>(gJhe4{oNNm1%Y)H=8;;E~v4{%endwwP4h z6=y^5mluRcoKAxk6%CRhfib%b6Tm7;=bv|BXSUZL_raZ^1<^%L>aw2)5i>%SkS2Km zLEI4U>HrUYmBx9J@eYTtbf8~q`rg9U%}>fc(+v@^rm9TU3pHt(0OI(x-8M$&tDU#A ziwz0|9t+Wq%w+`*@ug6X$!JbCnf><}_x^E^i>lr-i?9Ya>#M`&h6<-)9|@_Ydj+V? zYE4ecj4r~LK9sHqkgQI-V;A>tD@gp$E=NcjkBCdGk;4^*H~=n6v1uDw=}|1cnr!u- z<1G_Dgw1Za8*9xx)_J;zIx6kx-0DfwQz0XA@wt@@kDh^%IEv9CmSO(X+LZn`3KjZb?RNN8oEBm)?Nx#tCl&NLK zPz9t1XP}!)nuKVysR6%&02Yv=fJ1&>1yaZ%IC^U-ps1;-f2J)aK$6TR_r1?M_K1eD z*osO_^SeL~S0#yBWMzW(|A;J_L_P<+$bEZej&A)@@KB>hUs?GQxCz|7${Sis6y9iZ zJr!wHh_}tUa#WjrrT)x(Lc^T@N?s6dK|mR zMH{O9eX0or#uykpEp;X~2r4NBRPv?~i?F$;%Y@ZQt-AEHOr?i#Ygo-r+P9+Td+3~gesv$)%15Zl%_PH9W9D2Wv!7bZ!hm&+{o zFBf|p);fHrShVTDXaJvz8G><>k)}|<8pT^K4hwiq#e-@AS0-KmBl^}^ky>%6fpx9) z1RFI!{8i{eOF5Kip<-SRSECAW<)TI;5*t8FiLHAwrQBfC4zo-)I?n~5BePSCP#lze32T47D*Ou`>$Cw%l z=YFX2xzA{+qTlS9N(0MPZlf6;hJfeCF@vwQeJtVZJG2okmFpF30%!~fG(kbcb+yU$ z!&{Hd<>0$#m8&*{NKmPZ=={hhG13?a~Wx$<;pSF3ve!9%;_fCN7zz<|l(+Nw#j+it>kt9KVYpQuS*&uFzdE~dmwwCW7 zn3GA7I=|d~MKSYrmJH+$Q5zcm<2JVt(3}0U${9OTmX$>wKu{<1qGZC}3>{8#@qh8( z&z&lUFBF647}&8kB51I+SdYrvNSy-lmVXwqHm5a=_y22yj^8vtu7~N&CkoAfYXaW1 zvk6f7=|f=+MSXkA^RAB4+xTBfQXV~){%AIN_X-^mLpGs@^hQAKOw&p)qDrKlN#prE z$yUIP$+u~vW)AsTnYe7TqS#AvlFe9$#^fwDnm0?H|} zKwaNtO2%S;5)g$@B1)gnwjs0@dn^BaYxOfhcru0OT8`#R7&<(~WJ}6?$R2$kM-=K@ zs@2aNRT@q6sS87m3a_6;yfvlnwgeWj5DiagZe|owgw*7%9R8Sl^j_hICk1U<0>l|x zHlWSp0Eo|jRVHJgRo-Jd6-`OR93{4L81VIRONd$P#9`PMWlrjJ@`hRVt+*Qvw0Jl) zgAe4vlu;ncn&sq!sH2Psi+3JP)?@GeHoF&kz!Eq~{cHfR?t61-3!>IBqN9Zgg6M9uedB7*;?S>asO+J$^MuckY?N z;7(Tx6Hw2kf=$9KHr;7Gz)j?U*fdIgGSSF-)t%C5_<`xGzmBiwm*DH-#``N8VM$dI zmepoE68OhgbbZD#-ieJO7|DUm*yi||IGG>h6+8$rGq=2}Z!3*=6w72P-=}{1sKl)6 zXo;rLP-NEWf8c)3J?rxaToXpx*<;u8&2#Z2UmOWVW|d&>f|7_nakpx-qD!Et4D_-6 z$9{2!aE9M4)2q_hC)3TQrGwK;J5!De*RqdGJ%!+5X60i1cg`(UMvuUUpvpzDmc9I# z6n*W5JLlY+hKCB`qdw={EuX%X&!NTS1s;n|wAJJM{EaHQTbBb2T?f$p} zaa?Otz*E&J!jNg~vj~Ah5bgwk2_&usG7&DAQ*je+oKmUQGfsa1qa{D((oWy@LB@ybM?VZM|?zlPBH5*0A(ot-MSn{#jUO zE7@P`PvtTwvcJZaTYzQB)JstRf1I_|&dn-VunFKX$U&JFN?r*p0cp_yssL$O%uB1sO(iVgf6gHKWQUq2USzI6N?L6?@+WTH+BR7%O&L`ZLH1|PrC7@ zkS}n!y-xgH@`oU0n)p7PRiiEY(;%EyUnG^jfdXr9xjWW@3Ln+<}ht7zBAG< zso870mxZkqERWf@6c(ND_|u((@WT0R0}%_?-65T}WoiUH{iJ|3oy_p)L8btG7Ix7f z)su)m=2G93#3H|p3G7YUt}E3S8%M;&yjJeV702g5uK1SwxD31cep|7IkoypynN%vE zxZMW^#YQmz71IL+{^(j~5Ol%$^>n3d#;Hw!aUQ!tn%SM8do>H&=8&(CKsxWzC@>-W5t94|36>8_O5zsdoPgVk{L#D|wv7 z4H^hhrSv$G#v)3nuNGy?kCd&jm~$QOVT+Xd4fZ8-i!y(TOICLP>mCV&#uVVQ|FZad z1lG&p;sHA7xbFkLcjc4}|)`QNr9r^^RS2fpi0Q@8VT|;P3sOh;lC198D zB!YpO^9M@o_xmYNWIF)zKcX^=8GmP;xCFmp5kY}3L%Zso%cO*U%V^jEPunB2b;e(!9^Y zuNBhH#DXc##6C+;&1Cx#Gbo?LJ?#U&qCQg1D&5M;xP3s~^$VT(BRXR;vRB@~UcRB1 zF~Nzo6qkLcWGx2xI4XLf24G`OKHn>H=aF|QHJrLRpz(Z_EeVMpT$&FF)yKB7sbmxIBMv+SBAE=)ta>j7z|SpQ@5ZQ^f%Geh@t$z~7#( zHoOVzcbK)R*i1z*}<7x0E7)<`A3RDQy z7uHT^F`)yIF*nP@?1M-oFp|2~#K7u{fh(;E zvO#+VjE44TH&z6olzz;IT+2CrYK|y7K+2!NIK2h6TBOxjAWF*zV^xn7F(%5L9Z%Y)=D9Gi4#b0E^*WQ=CUamV0&V943 zM@QS6RzlfBKB?+c-|ooALXym=kPG9jMFdYh%r_n7Ui{Rch0#c|@2#Fe^&SJcZ(5o9 zd6Zt&WN6Qi7VBT*AAETFXam+4Y`^u^{s9tkBam`!I6fP#5O;4N5Xg>4iqfE3dNw2U zUXpfC<=k(!5DZ`X12~9-gJV7+>w5xNGF3KMH|C7@sPzS*rpzjz{{5ZUItK$lnOUIO zAP9uqrYIvhjgq(EuiMyovzTFmIgB*CwDB6syzIXfNbqlJ@SP*0qN$J>1 zkvXbrg~3AJWpOn+?x`wfRX~hpv9wFL#nHwV}SIfmc4@93rrkd31?+_C&=! znXJ#+rLUy-SxFl7EU5OB2OZZ^Zf^f6f7&(N|Iaafr>ZH$xjv3~Z+L(~OYMd?@Yr1T zeXPHOt0kOd68dFIUqh^!Q;;{cmdC$QpNKcvmzXc{m)3=F!+fBjJ))|@bc0~;G>?Wl zai&q_OdVo%hb?|^k$~O2h?Qunipn&{ge7^i7Uh|ltDtg zg!B!bVhnuryMeJOWxg^p%A5C1-9&VY`ewiQ=`A{y>lvJ)S@1(9Ci^tn@lPIwhTgtC z|FU{yL0#DlM@Qr#URNGxCrGDh-Pk};zN~s)@blJ{m#e!&Ez5T2J0sq$ccs*N@CkhZ zQDV^6?wgAmWD4Vr%-+B+B$BIlu7{=&TA(=EqMv95IvLfX&~9>{#eP`cIxUz?bYYb_ zdKeK96o?<8a)mR$!i!HC$FpeZi!qs%3uai`mt0&Wwk;GG%s(l!<91};Bqyvm`rf2r zs=CmBI;?tM0*Sj+#}QM;8XkebSL3xe8fAK7LKp>|w?73$TJ`iVuC8nB>et9?ulTsE zje$Z?zA3Xm8L3~D*nC#!QO>g44|EgVk=Ci(AbW*iHK>@r{vO|eap`a^CF|(I)Wbqs z?1S?$)mwW=D+CVCjA`XDaK@Q0T4Ys%bpJY3>py74T90#bdta38kE2y*zkVM9XR=gW z3O%YdB3H;Efuc}pX&?Em@7XKTE6dC)&+*2e*+56wqU)oy)O3raBJ>9fSi@-T zy>m+c`;1vhTF*_}xZFmeVHQ4Dy4pQ{DD9fdt49JUPT-QMx8^h~yRgXT!~>f*sQ(d3 ztnc@EdU#1w{F-9(FAz%btmD%(91Yx~@$Y6~dH)2WuQt#dyS*$ZtgYzc$SNeokOVZcZAHL0##vSD!;Y zTEK`OM@mI6$CiBYahvQk&0R44)C0jEcr5jznZ{e#3klBs(inNr*Kg|esyZRf3%sP| z%BWi1P&RGcvd)x!DY5^|jqCg&V!OQ7+aTQLMGl>6TXh9EPro-m@RUw*UEbeDXpLH- zpT-)Zr7|?7T#j%a+V>3Q63AZGpD^P1rUjEt)r$5>VO0MR-(sx0=ml}jY)D`=XTBXQ z^3~^>T(c&w-hIWxkld&_g)?Z{H=M0A9J+p|Dth0=;Jy6R0powdI(3+SbP4$dnmFyz zCSM^6wULbnLBzzU$%_6JT-{Hs-6j62)W8wi##8NtnLqrX2#H+WZk#UVF2F_5t3+b} znOOkz`)_50PD`@eO1l~zyeV?DNQJAl#T6UfuJ{%}v>^{f3q65l0SlW%9QYkB<1mhB z(ZDYLg42S3`$g85^vA}whw$>r2aoGcU0StU7d=mwC7`&KLz&!Sl5o9mD)~7y2t$Z- z_qoC*vXVE-O*iM>9Q&p|TvL$n!ZH}Z8^uVr%hycW^pv-weY;FP%TV?Tf?E49)e7S~ zN0(xk_x;dd*IvjTG#kd?O=09$hb@CsvlZAV!ue%~nUi zL~IRGnNV@sa%`CX8yHJtI=6Y85^#8l=J|MJEI$vf*El$J;V*7ALauTsp-ygwHme8E z?Rf7iz>QV$3aM8CRP9Y+Y_j_7F+s@uh2tg{r#5TlFc-9IHaUMd|9CxK?C|qBIcWBU zXu?OFuz@M~sVqShUDNZQyM>?@ZuG;A3tRaq$)cprehVHdA zyPWS!8`=Us#2R6*X#|=)Gb_=R) ze#XkBCip)gkT#4^Fw%yxWTFXw$RIti%w(c8LcxoOl$4OZ$pV9*7G!_4RFOZADG-W& zZI9Hdc-m@_ig=63y}(5`$Eg4w?-=_1ca4^b&fJ#Jv3!z%slcC2lGl+Oqvv9q_4(DHp!2(^GM6c zPPeeL{(-+$lGl$ekG8J0?V4?m@9{U=xU|N@T{GJxof6i(RAv+vMBQO2XwB_*sm(xhDziW9KXVV3*k~vE z-~3a?=-e$sf3KK`PXz>aOg9>;9U9x>M2P&P|Mit(@w_YaSF(_{|OhVi+W5^sD zmRK|}e2_9G`A2c-WO!KE@0FwaOGcXhcfX4~#@-`9u3F!dzS5&gXx;{}ym&dfM>+D; zmu};qOqVHE9QTTkz*nC}m97W(X*2ou6y6D*`}N}xKhGH%Fpk{waaMV_j1&V70iw-? z?5$WQw#N9Hc*5QJ%!u3G;p}vYhy(gvH=ola@~|jKGuM+e?f;Lc_w(}=?X<&ehhf5B z>f+sKnik5W&gEpM+jMink$*g80`)7?$bO}&9%4|j_m6@%T?Gt26Og;4+EnaGz&1#X zXp5X-^G)}z64A?PvIsxBDLZXg8WE7U!0F9|FXhTz3cW+#!I~8^Tm9XP;fiO2F8k@b zG&H)KL^~=sh3Z$JNLUya(GmX{oZCu4gF1K69J*g)wC3S;E?*=3k6oREsqE?7*LKPp znRNh$M`@A-S4yb)r=XtR-g^*9#TOr>!v1#t@Eg9A?I1KZt6jWEHnJPj*zjBvX8VlZ zY7}8H#kB^u+B2-EG8iRDy?StjLirB`Wkrd6j^hSTotA^%r?Bau`}$;f$$vV zT|qrW>0mfIqCJ4nZzs1mDiJ6CCwg6IQOW%pf^TAutB_R4246iDOXwV9l92e~@{Zr| z2`ck%*$bX??Mz;u2se9E;M{Du-LhhvA+mPTj4%)euc1~(zmxwx4=6FgRF;7J_q7#JhTEne-Y!UvI3TZ8;AAx^-R?O$+s?@ zd4K5CibE4N9j&gs{u@)0e}9)^yWWgdsa}3Da`Sl#zbqRKqU=J>7a<1D;k4DWLb}3A z#eIBN)G_PH57rnun))TdCjCv@*?eXF13B~>p8tTXnUpL&ESdN{cr=MW1_Xcqtca~$ zf&f}G!%y(s&ZhpCDaLkX%}T79^`i!QdDBh`FdS=iSz42z8f5{^s<=JfL%MfKyp!1R zba^bd!359yZZ%I#V_c1ZY>y`Ftc-=|inELR(aFJvp6MY)neEZIkFM*(K3~;?uzejS z&qQ^TeMFLWCNNfkDJ}b~zJk?)F(Mi9Pn6jpxo0Mun*ADPy3?F0wfpA0qV^TJ*pP`@ zqVlHvf;!OmxSv6};6qsX@7W`cteRi&$F=2%uNL>iZ2@9?V_ISX40e?@jMPXS0z4of zFz&Q|%X+PqJ1X{4xth^VBOv&hZ+q{n{CEBsCQf#DDl>^MB|WHh1I)Oyp3#TlnQ)o# zhu~Qu1yDc8in46o+r)?xP&`7)b^Oz3+;H``!*94N$aH!dLOp4@EncjS{vXkII) zqcpxNXl@odp6eYlc5)Y7Q6~_E7SrdO03T#Qbn~VG|I@SN#fBj5paRn?Wna~hr z{@9b?F12dml!}0&)p57iiaXV~=pgGpPZ~fJ@aD~oGQSDg*u<`=iq~`|fc?w@t{wHS zj>kj9M^)t2Y9!*=s^GZy*vUqFOB$KS)(!pcU6c~X9Ia*8-KDHvP!!A-#V;HVG+d%) zk!MRd45F9zc~w7EU{?L-!!D;U^fnu|6rP2`@tP9rZx+zsy~8v%@jaEP;0aNC-h|4m z2GSM0nN)S8{JQ3Ojd-lN48G^rV32%}E;8uT)OY|QADGD^#91{jg>*N`X=paN_;c+F z!*wrth{ib`Z*rtw{KnG!odQ2R^a+bw1|n)*p)+$$Y)#(ZHDRoU#BeCPHY?=Y`D=)b z%cB_E#dRwRzL*!9;DbkF>MQK~93br&TX&kOv_;j+^% zhC`RB5N0r{1Ji#HF0$tqr9pnZN57C+Q@YjkwRmii^E^=7CU|%3ZB!HQ@Y#a;D}C{< zfc|I|V{d794?EGIm?t)>14kU9lBFM+clvU^b-4naK}m1;nxbQ)(AX?GQPOj9{>A+# zgyy_ckGHBZy3owR%I^NfySQrVuP5`to+eXF{b!s$3AsLLywqt1Zm=|#pML>oMs^(| zg^GPXhL5VOWsVMmjIbk?VevVKcpY(pemr=RD<{udKpzCxo?YkzBlQKvOZ{Ue|Wej>+qVlkYGf%ZESDN5jIRG-yPD|sU~hD zP2S4Y5FL4KB_eojnf;$?%xO~AO3TyLz)7n%dh%FXYez+#VQZQ$i0Ww%pPL!bjXPs! zEjlcc`t`|T3RwhuuSmmrPdM!#_0`#~XZ&UxXhD<2w93CbC*tcB>(5KY9;1A-OIXVv z_e7fgglP;il*t~cSENu*zo!36_dN}_ZteLLOgQ)^h^>rGhK9sA7-}wwq-4|7?riv! zHxfML$%wc2+x!US5}7R-ls?x_G9{ysA>~O@HkqKHng%X7^7gG*_edU91dUnA6*##d z`Lz2!bVqFLH4FQ(GBTnlG|2_Kd@}RU>p#t<^h!fi8tRcD3)wJ*`|HNNNyt%;P2*^Y z`W7e5yuT@?R9|`^H|errpD5p`aL5)hY75m-q3;j6I0uKKmfk7#niR;{GHoG zFg)?W=`Q<(q9>-3gVriKA7Ob|X8!r39d0uV1bX_nO_Y8}>6`FGMA%NntEE3LSclRbE35j0)IdbRmN zdwngl)2VX9GpVr`7nLHTKPO?WZ|jSS^%etmg=`hR2d?q|7tu!^!M)4qRAcDIFVwL) zmX=FX%*2ACGTeuTJ0!l=`yY{=#PKtv+Yz)Md@1_icACG_2_fwq6y_l?GypBt zi0LY=eY)p^aIaB41#KF;g{vYw1Fv1^U$)QBqt4rR!(0j6X;yFA4bc z04*)xxweu7&JD!#{@xzUo5gMizdfa``!&zWQ5)j@INMC$^I5~x_*bI=pID5(nM^^0 zR-B~y%l7_U2`K9BVV002wtUfn{`up*J)hN^e1n<-%q2Fea`*{k#HMR_h_CDNcIGI+ zckSNHoPC4Uuj&G=0n~<}KK)Vy?wh$5H~Z!Z>+^=v4*!Du6P6=--wAiRu4k z3USIJ(Hm18i;0qr(wi@mC6CWd!%fXdt%I3U-pr)Ws5?v--1+?_sk|AN=`rxc@vHcT zhWC)PPxwTe6;b%g4#=k5bbOioa6^sbxUM|Oj%LY^jW%U5VM%Y$+9JCKYVq9q6lFPu z(Yn;u)Jr^A{T>hNrIUAJ!b7~EX6SojBB+Kk3<{%UUiS1w#I&azro6kd?(9L;*CB=c z!U=su12wYEpVCFCES56UJ3eX8z({nHxT!dE|LDs>zrq1x6M;rzi<{+zcjbQ{%p7-H zA*5;#iyMW%ZN;({RTuoC*qHg5`Icfn z(8twJwQY%es~)Vo!%+Zox%h0F=SsW^pQa6zkpW;zVeVg zi`g&SFmr`PpCm4bz+=f)Bkd>p>On+=R{33EbzJ!$^*zP8Z;v|>!;~eA3ffsyQd#%U zrqg4F`Ql7?j9b}}W8d?F;I2H$Ar|=L!0@e*_n&l?BN5uR4SudPXP!*_@DG_&JrKA~ z;=U`gxqaxHrQQp0#T?e=Rc3;WS*+hV#L5 z97dS~OL{QG2a`Ax7p*|E_KM_b#7r;8n~gd4;3Ce|`$5jH-TGIuR_e76kC4SkoK;8; z6Vw7vaE~DMflRvh^(PWeGH}MFI2Syyxq@x}jTj8V!2C0(OR#`Rk9Tvaq0JzeY4KoM z)NC#{NnewODpy%ZnJ9=QUUpLBhen>FWlx$(HUf&8f<+-91pJLiNr^PhDJ)gT+P+La zL@$f4JQLPfk>#ursX19PsbO{(`ypA!R^ zn+scNW#z@0owNRue=a-ILO!N`kL8B&df|L{hpOdY{r3JPU)drlRQBV(pPHxGrjLoK zsha+dPq*>_h5G(UOEA+C>pHgf9xSs~v&1k@Yro5MI1{p2z*u!9U*vP_7D&T{ifU)k zem41B>#)G5dvktXULbI1e;LMSr3_3a9#B>mq&%L`DB1sTZ1Y8JKUZOKQeNoPb1J*h zUir%#p~p1vOpfgt6E+IDqrz_9@=5M7dUvE{Dtk{BIQYm_(3KD)XdyaY3#lQ>I=Nn$ zpWlIec}k;NE&FnF=ZDW#_oOoC1f<{W>>u|kf1QwB0P3Z&GqHD(j9#Sr@}qxj^?Sak z8epY=O$FJ#g55jxebWNo>(4IBP@a(#Z$^!G1{C;AX>}jRo(^bk;=ih%T0e;tXx-k@ST}nFC@0a| zZmT3=5tct}y!|&N&cABgwM=QpoB;i3@~j$4vmBBI5?L!ux)X<+cJkg)f3!<~&`;3E z@6kcx&%Ud_-V;=x$_@FLJHm-D0*j=Y?YrOn(c05sK)h`J+RuV^MO@W2wx}G2<>6Gg zlgdBj`Z#>Z06r2LC@RP)eu?=EGJvHOeO&N(a6^B?ysGP;1(99_*U+Kcc}yW{U3|eq zgV*A@UlSQ_g9pCNujrv4TI?y*c7E8bK}dcV43PSzn7QfdF>J%0$&NKA>|t9=irPuE zxGocV%Dwjo&2M9+?&?ZE{ayF$`RpNU>BA*Us@dThU=?%5l43f41VZMS)8Nq-*RS?NWcJrO?QC)`C#Tf!Z%aUTOY z+~C~KnbmMRXBFF^A(;RE(~#F?9DgP49Q#lCF%SEEc4j2*B}re@$k?0P5cEDm?q>Ze z2JYVM-%$0=-k&C_UekHe8a{t8Dy;=22HJQ6uel&&e=;U0FOq4Jl1{o$S{p^Hjg7@1 zA{a|r)U!C!nEqZ3(VLD48G0*Q#Z$mHjth#qx1mcoc2xcD2vb?jiTQ8U$ooDuDN~UO z*laUCv6<5}a}|W7>SP7ya5%~+;?$0K&_iANL>TIMu3<)7X+m=%O?w@Ul7mJs!l&zJ z1*6Gg?527Nm7Cu3haNfJqjtr%ELy3gjM_sgAv$riWQ=o=j#6sT*f z0qB3G%^3$nG|ii2L6strTy~t?+6h~rQNZqfuVx0~Lqf=1diXd+>{@n%2ePVT-1QZ) ztW#(QpBCu!ize<#8lvPoAHME>7v%e0-WB<-+DKtnU(6MvOi6A9pi5JeUF&drHBQ*D z&{YbbSLR+#kkxW1VSy+VN}jlJ%r}?kuq&oPdg@=AI?Q?`L~BcwoMrZX{ueS&IZ_$I z@420muuCTQ)M{d7#2E?L zf+m3zWR5WlSA!kS4=%|9&F{;YH$6mSUXrCLn-HNM@fLVfhUO}M`o0ai?@pwY9UK7Z~g?@Yf5B|xS*a_W{ zk;JTO8mvgV#_ZKvCvcUI`;v#lHyEl)l?Ywx{|hA`Q&E zqL1@l*z7?dcQ!Mb6`aN+Lg$0NVL#FjtUp}Ux@O;+oVBXlYEcR}>!GW}MCM!gi+)cC z{g0?nq5hWrCp2p&6UF+hNb@Gil#}-*QIwieO<^jmb)nFpW#XpcH&d-_;<6Cz0ZFN* z77_g$Rb!s5+`WpYOo?7V=gc?UP^mWat|dCM)3wHk%Xi-!W)TmEf`yCr@w_w8z@S7SWiwbGE!i@U5Dac>= z7=5_7y3Ega%=ET++Dq%W$DWP+&-)wvIl-GpjBGA^Yp#&q{J=eP#TxCrho8CQTcK?n zj{jWW!VP^Z1Rlbe&YdBw$RTNsmyI<9bgb)_!o446z422=pN@YWx&Ml2mzh>*eqxqv z!|RMx&jNcWoC6g$U?XWIm;gXrn9ITM_LKh5nd@AYLEXlM8DdP;k8&O`)Pb5z0dclW zkcqLFux9tnHL*M^a<`biR5(NOCb73*@mV=zLPj95bhdf(EZ*j2v;b}@E`Uvl+Dem# z=sB^ymi3roq5od-)~}pGu922}=Eg6l#Z7GnRz-ZiQ%i{d5$Wl?MecHX1JI%>teb8i z_y-f2-44Xu!dv+wAzKwLYTlE?LGB&Cskdi@iP>T_MM zpMRwOh&>!b%!P>mbq3jI))6b40$*|vPp3o-;@ryXis>0?B}&TKq8(G5fvh&3;$)5m zF6%|I%VBqpD%tqS`}x%v=N{XoT`>=BM_N=+^9mMo%#ACxm<2)6U#72I zGcvu#(g6;pAN=|_o!nV}-v`6X5b0@trlfqeNyO08KAYm7x_M7xIZl3NdHut$D4K^3 z%`58=9(pk>V%=@yNnDy%43Dv4*B6am3$;@B?uJz!LWuDq+>GYM+89LG5{AtN?g#@KPGOrvk=q}lSDglwk%4ZFxP zHE7c3pORyg-t_6`oy60srkvTvdzD-%Y*VR{2DL&jqtaMe&xUxtwQjzkWG$1%&%Rxg zV=q6O3B5(4(!ROivZlCMywwQZ^?EB^7b;9#y z72x`%&~od#VR$R74ug?uq64x$jIKAEq9Uh80%P`T1UK%1)({FuLUE1@I{UG;v^ZP` zj#f&bI$o0HAaTY&uH{wtBR(Si`x@sQ7hwT8bj|i#bT5Tn+{r=qdLc@2SIm*+1m6SU zipcvCU%yT!TABx{$Xu$o%mjAGI>s*g?9D3^*SCYH=pUy< zZMB?b<9Lvj-d5?LU0xleOj9jL<;~2h+#hW_D|5u-rNQ!4gW9^M6-L@J*?ecWp*dlS zjSY=Lv<60xkvS|m%hs?$0|wcsd`y-6TxCnkBkO(f2~10AF*ui_t^IDcpAKcYHK0hz zZ^lf(gzJ0q9onPfr_$eK2h}X_jl^(*3uniBG6aJ9tDl&vJpZYa+Idu{2b23EO+2I$ zZ!Gaz?j6Gj|$gt#j&0TB{iL zhuX6zZ>51`2fzEXyD)qDwFitB*;jA!ac1+KJ`_d7DKMF@JPBlwo~(N@p2ojn6PYtT zgJNCMqMl%~yaX_6MTGH*-+b(x&5UAUS)kHE#$t;Wr^Rc6csu-9I@&sH<6>x^ zClvTRUAAp!VTz)rvUV=R*@#kVXZFP&klXmx&RHYC4i?Q%xEvD{-@U-m+c17?Si0ZX zTbpE-Qf*Yh!=WgEwVs3VK_wg6f(=M=F(BL4_Fh|FBzw{neAHmC!(@avRQ_t%qeX7R zmdm=BK3Cc5oxsp~-(~g+vVwhy4~mLolC_*-=-U%PE1(BaO|2?EJM9#*8EO2Nf_U6I zyzh79?{`tl>{!cZ%Zo|ADF-oHL)mq)Hl@P>dnk`52Gt2T>bt>{$K|K3XS_As6szScXxsT6;7 zvdjL_)q6M*rmc+#2Bb~@49H?ZnA{Ht)halfhxzi8=1R?Z*fjf&zYBk{BG9Cf;OHSY z;CnhM zWV7J*)Y|W3IM?ChKJ4gKE+CD0Mrdpk$V+-vv@q#qmfA|Hv%heX=bQ_A)mfK4l|Fr; zyhTDuwiSB%#`+~Xzzhtk;C2n6*5ZWhSqiGgO!)mWUKZa?Ful#1xvDE<|9z$SV#K^; z{`)CyY^gr2z8JKL-b{wA>k{DA(xc9A?_db9XO* zdAS)bNs9=EF%gDk(&|o4-EXYQYTb9Kwa-MlgvT^p!GmAMshvWQ#W)*Q1m}Q^3)5>W z0>IVbT!)&a&7L-@1tHZ0`Jmmqf+B*P7n2?{XWId3jBpDQ2K@;}F1|N>6Tw%!w|-;C zDhGL%aZPUjyvM2AU1t^0!Z(AgiJIgNP?*_{xC{%-s3?HNn88l`!{Kou$>o;*n!yCU zZaTHpM*uO=@lT4`&x`8BfxaeYz8>e#c+0ikfy`2oAwLcyI_LxH|+G zT!OpX;I1JA2<{R*z#thsBe=Tp00fddRdKoEFnH}qWnw#vOFzC$%059J7o50+xo21zE}$3aJNM+ zRQ3^ab48_th?_7p#qWEGLJkeRLdilY8BxW@5}kC@W-trgPq64CZk_`u=zK9qclSQ$ zCwT7Ies_nN+B2&D#q0c&#&bR3it;mqlGdmk34(=8ADJqu>xpmOU_S}^VUb#gfkiVs zgu0&S5g+<2;cOl%cv?Vz_3^Wp){$Dpat+Va=7}nE@lkp|7Gq_mydgs$8iA6&G}dr~ zQrCVP--`yuz$?mU#Gm<8_dNM}_3cULTUN8KqJ4=Jxw+91)|x7M2{Da6M7m3U46-^#1$RH4aP2Du_=Bi& zlBs-FMOqiHe!Y+S9=nJSu?UGN1>v@j@T6a)ru#69XCTKr-*!!Wl~CE_)BPFTKZ~zm zU@lv$qeqey~YDJ?*<$<{4qrSj|Yb zAuWKmu`EGBx;-P4{_>GN(t#P_@7$Kxxn5s{W^f)QxNC_V?KUU&vs4tPPPklCGgEhZ z;@JW4R-d{_Z?sBg4rPr<_hm)KO;b&%7hm3|))M=G4Z)rxGBdf?GPb#3?Q$C6S5a(Q zOKk4+eN;?MvC5#0n2mrL3_7Tq&spJjK$D4xCW;7M`iEq7uki5w#9f1l9>(s@Cg z610?yWo~Sxd(u!%ih=Slb>4vfIKSTKg~!ZdCn{>NaY8NL`a6=;4_ERA`lTP}7NSspX!WeLBUzF(&>W@5+T&XlpLt>=`-65TcA;^zE($?~x zg5ETri5nhQKcXK8X09Ga((O2fRHe$STlUe4ZO!-|_ip5Gz%;Bsy#FwN5iu2=T#_1s z(RlCs04{v?dekkR^BwpegbBCnhr8C zWddDjl$X_yCn~nS-&XlFJ_-hZkUv1t7)N1BWnISKh!DTO892Hvr?_*!^UVB5!s;wehB zN`i!f#$??np-6qZbzT)=s&F{_i&OUgd)`W;60(9n^e=!_eVr7|iI=esr2~Ua)O3m^ znSfVN-)F4!833D-tfkdC?7!?7t~Y!Unsr2z<;xnt$J7ZjB5w9U>eL0Is+*#py9SC* zV^fzy=@5j->H&HIAY{f$rwl#{>@Cz$3Gw?|DOEii!0fv$&GVGGJtcs4s%Xj-J~QL_ zM3}p$hqmIii+*5lWVFtV&KIRYilbzmJda!J8S&g!=>BQ?>#VGA%IW>ook#)JU%z1S5b`{5 z7Pt`=ke!IV_sMeq!ZbE56z%1fckW)iO3<(`pM|9_!-gfT{UWERc%68ixP+r*3?-|E z$ma`0xMWEZlPlwk{l*seQI_ELnZy5?HWUFz3M`SMhH4NF#rv>gTJVuXQ7_`$HEqGux4@yQtPEaMH)Dv;MP! zPSy*iVn``b;jkZVgIB~wTolF~zKcVNx_`|oE`!9pakI&043uF+%}bS$7!Vlp5|V9y zclNd=uVapSENOvU4|ZHt56-nmI8KN=;#X$9Qsx@Q#*WGWVKIS3T!En2^{Rm>}V7C2K1ag1~-c3TQ@!*G>TRRUc-x1xEG8%@6GMsiXf z5XYdg|BYs3?VyeU7vf4`-{H89NW6et6?d@yB3+1?@O@o>pcMgsDs2q|CT#R_D0KaK z-~MAJYc3tKFi20zEz^(p5fJJ&E7Da}*l?GF5PNFFcGfdaZbCrjGjy#mZ9SxEvC*Z@CoDFmp3W_25{z|tE$D30*nJk~hFB@I7h9suyf(QjN2{ zB<-!?%CG4;3CJv5CE-$a|4tcZC_wsqf8JEi8K$5pPa&kt&24kIEbQ`w$7;~PbB|W^ zjcwl;5H>ZAE79Wn{at|XlMJQ3REeA$gR=n>k|y7vc#w@f?d+x7yrW7Y<#en{Nh!Mw zeQN7VB=>uD(hNPNTF^)zj$;I_vijtEj2h~lfXrXAk`*hmc{OGw9J_<5EoB{Nw?R19 zvfX}Nu|bpLpaE=L3O|HwA{%WECC>I81I%15YMP{rXq+x?1CApj=a=nsiDlrrQCwOj zPW&7f2@ORX z!RHxP-knw*oHiiVxmov!a3bwyk&}_!$>Iva`F_v>?pFj*8y>{gpP)W|qWOL>e7@Hl zlIMA1)vXfCWxPuVS{EL{)XH4ag2MVf9BwXI;+bNcof4Ya7z$zm1g`-zu0=1}jc*5b zXkfW+eU%v1Z#SGRrCCS-vQR|DuE5-HG_}ubGE9Ffe1w(yxW?B`2?CcExxhD@=2Fb< zv~o9xCYg{iXg?D(t_YF0sRY^5Y&UD>=j6VgV}}<&1=_r+&)Ar$>o7Nil(aK(i_Zcs zb3Y}-q0-2LYrveF^-wV*8k7EuaK0kibJ9tJX)}2?k7`Ml3z6+@UX}-^rfeEEtET!`tD67W(n=&u1Gc}n>H2YtL0!RKb&(|?bL8EZ>AS3-?^AL^ zNj?@U$pHs{DTE*n!5+BTli50HTNhoMWO|@;$|j+eh2@8*9<76s&B@6_?s}JcBdf-P z>Lo9AMCjz9U>sWYd*N^HIUGhSzMuNNrd@y45bu~j^jsK*IQkTA`yu013F1(0myeiX z=zT)eZ3o_z<=9rTrAPh1(8v_Qq)}2j*o)s{oXK0K>rXS4>(4x$ej_F4(rxVYd!2~N zP!%+1iTUr9YSa@e|?{)5#q-~qPDEw>`wFffAY_S4wu z@8UwYBpZmYz%5BwP)7XK#WwC`yG#PrNDvbw%S3-vnu)*!mDU|yEU9&twE7A!efl0p z-+Yz?cKmb9%Q!?s7$1?@UDD<9LeoMU8Xs^=udofPXeaW*^KyZF9{H9){razrJ;>63 zI8kZnNMxwr{}pt86YO2m4gzMx8WR~i-c1YWE@(tk@-jAWr9d9S+5UJe)WY9viASc6 zmKA^0QjKEaO^ictNU=8LuNNfp272BSm_OgV_zq|9^ng=Tz&rA zf6s#aXDY;8tr3AnBJR9F6G}=G%5h^?)KK0vY-U z`h;(cU+}FsEyW1Wv6oG&?$vN3F} ze1P2w^j$@zE#Qj;ae65G{Y2W?%?GeafB$<+rzJ|L{fCX4yWF{B1_n%^&|X)ssVosu zI?Lwq`jU1!j3}k_ny_4w=b8LC#{M`3?w?LL7v&w9;kb{%h$S&)!<&w)JNxp2DQtaQEaaAk}3u9>5zfUy5ftE#F@(kU*-%M1KnE6ZSEy2k_Nj zLv!OUGuDnCYq=B5`L|sARIMw>+^+#eMMQ9PkH8d zTTq%nar0i2dZK+=_T4O+KYuu5(=CpZe7s!9@0k8Rv?u)d$k;)*QuEGHOXJG^k)Ji{ z@|LjO*$onC$v9P97&BhZ8o5sAO47a@20Q!}5Jr27;`zq-<5;^ZvyAkQ736Yk68z%u zdBETmeY_scNk?zQMo>%A(Dw@8H9xd&SzU5b$+i8vlL~Nr1y79;wtkH+)&x^t&Mt2T zWT*DC`Zt@ynN1OE(gRnF(CoM>AyMw%o2WD#I>Jr@MJddj*vb+=-#@Bd(;jCSn;~pf z5I&STr16}RY}s}4@-jy9O1Ji96ntGq$3#k-4S6VW+9 zYI2AyzNg3Ox3pXRkBaFKqtXMMuO!#ZAX6^)rDRQwk=frOo0!yLnbf`*6r}OGwc0zQ z3C>yXe%p8)jV19f$r_tk^#aeSz9KO?M;!spSZAu8Sk$jaJx2eABuZRGS~Pj#_^0Z% zQsIa8iijUN@7FZ=tqm{vtu}Cp)s_<-nBbazz*DDGv3Y?Hx@taqER|Kcnbr`IG!jE! z>tBd!Bb3-s@(Q482N-Z<$-5Wn)4nlIHdn-FG#IE1iCz24R1u>sv!k{K)$!?=W$x(0 zFGU^K9~kwrx{DaV?qtYM-xH;((R%;xA5~*D5ehb=_hFm@yKMQml zNs9~t;wP1l2b3_VFNZ2Vo%6x5>P-mVC-yr()~n0e;)TLA0L1(tv&V`c4axHsR9{jNaTNthXm|#veP>G4yM}~=Z@wDH0r6Z<)8bf=t*f|}qF#Bzv ztCXN(Wm`nNz3?NS5i7{z)7kt~gPvC88b6t`wsHH{Q(|P*pTZb>#$M)RrWTG2QW{-( z;ixT=sO`{-7Zb@QM5zj256Ux}<e~2l{zs@{TyV$b2~`-xZIk+^@ki=~d+_ zOX+wrbX7iV!j#AH#6$O++*Rjp>sz`UI&ss8aMA%X5U=5spyg*pTe@+rA+2$DXLbIn zOp`l@{W%?@tk0%)qr%me-ET%;ko(dR4f?J{$lt^~7~8DQHDaDixAYEReO#Uyhbm}H zQdv`*@ECUOni%B|LWF&G+_fvf{6cP(B_B}`EEB~3b)=yyQPCo~X!iFyIr&d@DypHFaxj0)rnKrREudM7SP%?&b!0UJ)Mc3`*^!`9|@Vm07#@n zl}*g*rp^`ez4r9vlV_O=FcX@QZdg}lg!-LPtmY-~9tlaJ*R)-;|bm^%|0~qx$#*mZv8qM|C8H|fwXical2F4vy#(j>^ zgTKQ<4nB>}G zNJa!7gR&C+_&=mYsEeM2=>a$Oy5}_iCNNoM3LI_XW{KorG?SgE7>JOk`?mQ;R)>Rm z(SG#6C&1x8Epz%cW~l7fJz_Hy-^YM<38 z@7bM0;a3EeO#ZHwNW{diFo{uO5e9e32eZxIGo@`BD)XUZ;iDjZ zmfm!Bi1%Ng^xYQEtTuuvy9*l=9>Yo>J6V6cZ{oNZ&IODs>aYiwGG)*gB!h9A3V5Jq z;JCN*rCc#n^`>|YuBYuYh|R5vlwAeP#ARdo=r`KAMhMfc8|>CW9mYX9w{l!|!#pX_ z-}iXUYKQ)sjUi|pGOYmv2y6{(9aK;j{;xp~sZ;yFZmWFf0~U6hUD_(QKZ4^RC9eyg z7QNa^Yy$1v3eyq3aj>6}!8)fh{ZsQ{ZS&TI@YaO*=8Dg}UOA1=Jc-^`qHu#AoUB0? zT9LsnUn!re7Ob5yDUBpefZmCRbXB9R%#`|{53jOVs`7Gb0RPM6Z=~r;@fuhEBIl-? zDD735b>q6}h`NdD_du)b&*@3SnVH-DI3ZZF+GE{}lP4*NLnr=Sqkx-b1m5bS-W9*U z$I^U@V-*)~PNLk&pGBCd!W((WUrcN#fdjh?-*UQ_RcGltv}tb8JJ4&?+?P{(G!7OtqD8xF z`5AQ|!(k0rA}hvo7ya~8lb7%G?!o+t>%93a!;X!2<5V_QX~Q@@JKFUBSx+rlrV4MM%V{1iXFktNPxV>>G6`c5+&{Xtri?vGsn>L-ui?M731$I}G zmD`xFAI{eC`u8YlU$Q51X-cd~Bkyv0l=ysa;R(w9<8hAvRrx4#fC^*O76&-lS#yl>gnWV2D zD5Ff0tm3wNnU+hP-=DD^AgPsyrq-cjO}_DK+@ssQoS$!#?|UW?3h&1N98;at@kIO$ zFIN^hP4-#+AR!U@=+&pdk@6h>Qjhdul+Bs=N&MDJ$5VLyS^0(*s%B1{ucpnU zpx%SR3U(YBDU|`y!h7K+-?SV2{jNM8#VyHI*DPzU@y0J!Ec>~EmtkKXIF)BIV21`) zWRWP1nwnA#H5h&5I#-p@*RU;TWm$w5y7nZbuqvZ;RW`M{>*aNZm@#m5eD^N<&gjra z$#fxw+UCkvpZ;mG??KI| zsHilp`}d1#QG^xgOo@p$Kp;}LR^4Vb)v}2hf>^XWdWbh-LXA@$$)@Z#{7jFlu`w9t zW*=S4G#8OAS{}PA58iJIVnvv6#*Ol!dEqxJU*o{rRBW=>j8&=4@O}ql{KAM|Z{3xtUmS7%6saK!{h;yImoW?o=5#Ln^(p;{n5@|%^)=MX`%9{n9vxE9v9aLP zs?-bApNibMzc?T>hW%ngAwWLQ4-cS7jTCS^;L34SvT2P*IF|vhun{NHTr+KEbfABQ ztR;W6BS-mHS$W&b$Q|UQ|3tRm{&wAWi)%45@rdEIYO<2#8}#0U^_W!{7_aGC2v0pX zc*yeyAAW`u2BO!fJxtQsr`c~atn3FQ#TT+RQ&+5Hh3YpYBYv^d^()V7!E3nWky02hi`;H_#)L1`0CN*I-EeqCu^HBEh05^v1RRiwfR%zdGOix6v51xM5Cu> z`g&!c>(b_sCXIP*CEmx73C3v*Qd>W&P2vpCS5Z9;~llFK_rL zceoYjYoX4G)_+e)NA)>dn(Mo&Y2Am%sVulElZ3{mSLH6yQo1WQLV^2x zeX`{6qI4pRx+-JJaw;g2u~}m$HM-tbXl_0Z)$xSuZ0-kfGba^y_JqK81||!Ts^t|EdBM|Y7*0@Ihlh_VlGeuR5$%;P@}9Ao z+AedQbyU~!OvK1TNnH^Pp|QF6DPrUDA&kh`OIb6J=8WBbmo0))I=%qi?z6Du%9Dpt z7|5BNhG4&XVs4~LP;fDWY>Ixo^A(yaSs|f>!7qSzd_P>)Iw(qepNBeMS)xV3l`@Qo zlTw*WVFveI1@W_BpQh$5v;HfYhiJ;J{dY~lGTy*3h9R;i`6gxtPD5BfwZ$uBa(ef$ zmaFQI+Leqy@zgyGGnGxn4|W%XjlvKQW!he7BnLtEY)Tl-jmB5A!Z zrK6>hNDAG{xRY~YS?1DmfAKgEJjm-<#-VavlV?~88wAa()f_hgoMSb8=xJd9mABfLw*-|!|D6<%I^#FzqdM$fh=ISvD2I#h}a}{bmt;Fj&bCNBT#u-Ohr+a*#8-oQqlUkX_f$ z5@um_+s0+ZVj#ck8FEzvGv|old1K9jzQ7VnJ(Om!1Rs2PiQ$%EEs*aL3a-M!=InIpJCnFzn z(~rtM5+jpPbv>5 zKyQS)GtLs^UTDeDPYUYC(t~YM zoAZe97ynn-RO@-dBgK^t9hy*PKn#RNGtR|kfGr)&*gx^Z<9#gH1q;TEUnHMHV8gD|^NK0Mq{qRpyB%Wdmv-MEMuA^Et?TFyQx z>c730_U!iZe>@MD>q$v#;V@H9r`ZlP)V-oVxS|*{_LpaQ)=~jTIh%7CPEsX`8|7p7 z;`g#m7y|r?t{B_-jn60!u_&8Cl|7oXS%bQR!hpfG7nP3WsYY{)g;Y1z85`tl%~%IL z%8FQ6ITj=Bo*BN`_2z#{a|(*NAD@upJzny@hv(ZGc1PWTE4yI6@~F$Ro5w>ZscVhE zm(p7zsLUITM7&_B5dYxN5!MFYydzDm*_p z6A%D zo3K?0WGFQ0zflt&>$BYzA4IroSGCbcuwrU$N*#9XU)E&#>2H#r!B*^d)mx9-PwG9g z=E_+VCdn`lwP(C$wELSKB@DL<*Nk>oXeQ67{ERPdF6^$BN((?AdRIuTZIf#PWxSn5 zRFtS~z7p~DIh|yzc?$KE8SbxJRyE}Z7Uhb$rwc@HiZlu5e=$}?nuNFXP-D3wsi(7l z5LDIsmFUyLW5+L?XU)AG2dAF-21ut-v`W<5aDRqM{t?QHh<>e`Z`jKw0nsEiGP(h z$aiM_#_?mehoX9FLtO@Ej20UJNh^k#fmeA+u*TJkjIpYc7RAgFpt%&Q^(Xx<&gjbv z6-O(4wIMR$1lHw3<@hNhg9i4&cWJIMDeqKU^B|J5(n_&Az|a4^iGH} z+Oz6L%hR2KKFH?YYd;bK=FF@9V#c7hyx4H3g_>mu_xbobhMTuI=#tf|wb$Hu&!o?x zsKxo#fL{8{tkDG-ZX=m2oZsOcyFgRk{|ttTM>1+2kmmvtWf$z3nXGAEYDMvZHh%Zm z?Y>gq9(?~)LAmun$-78lf{jf0p^+ht#Dx{q)VJ00Q_y&o(h#XWZJmIucO=TF)h)hn z3>m-ZMnyfZqnsKGL>mSsL{(6kjD#Q|5gm@V=C{cua=@9^dZ!lR;?#ex1$(|hLIXq% z2K|*Te5cMXZY^Ri0{$}JN9RV?O_Am>%TE6|E@b}|>3B@`wyP6 zG8CN$Gbo>hsc*!QoC{+0-kxp*mCiSZrbJsgVJ6U9VIdJ96<))3F<6{GNOIk`3*GA> zl^_A?zNmLg{pkSwS^a(PzVPj9jn6L+5Ia3HmLMj!OL^Wk)4_*i_mFSL$DeAF{l`@Qs_3B7hWO8?;9VdeE_8%bC85bV zQMf;Y^`KLskpSLV>EJSE>aYnolY+)=krPfWL#OiXCUF;z0uI)@6P>9ZGqGO4`T#HEs8mt&Ous5IEsxMp@dSShZb?lI8d4oIpS$Jl9KMJ&4U`#t%crD=EIg>1XJ7wt@FS@`L8jYM ztxYfnQ}+h>i-3G~CP3zi#=mX{YMstdJLW_)=5RK}BCZ|o>|iSB$jWiDq;G7p@|=QM2jGof}eq$eIgsf`gxSq_-2Js7b5E6LM&)ydZ>d@I>H|K*u6&{x~k z&IKsH?UP89Ya*@A_qmAs<@N&iPO2rf>wA9C2q=BEh?W1Cnl-5X(rNyp>75_7b$e=P z8w~KV9Ft@baDkK(5VY{8yNxCh2i#UynuvS;F+AvAcj6aH`<&}yC92^P;YnTID0%%V zi8_!L)fU!QuBT!&+)OamJ~xFlh%p|hwt}Os2_ivX@+YMSnHgw)dO4btH53~}2{QXK z=A%;aB77^D#g#F!Pg@CZfd)H14;4QR{r2aOr!dbYKVyIGKP%UbAfP8nzoZ|tP5k2z zom<)!n`@IIeIq;dfSk3O#CWVfp9=YWLFgsNBhQkz!+I3|R#}nG8OtcQO4BGE%I>V4 z39YDfHhdAQ7X#8kL@yR;|5Hgz%lQj(^Or>XYC+aLjahHimk=AF%silriY9(BobMy; zZesL}3X@7-U{s`pZp^PxW0V3k&g`D7Js+=~X&}R^1$3D*hQnp(NtrW~$v%P(^sYec zhEARTKa%f%e*L(g|zL7x}QNQA!p80OvM*rOBuY?f)czPeAxI?*QBv5H?c_6>@R#iYr zyT0J1yA03=11CHLsJ`I|Sxh#Y{RO+Z(pS)Pk%3XEzV6V!aP64C zV1HBhlm>CcgG`^s&u%+fSl;IEED2 z8!4~SVqH0D`07g+JP?6<8}Y7rUs29kQ@hgX)85J&9xX$w6cy%Le$;A6s%V8tVir{k zbZHBiSW4zr7uQvnU~Aow+XLcG>3Tc z^zRi}p5{~u$NzO>Z4YW2S-pu*o$aQA?|XJ{PZfTt4#p8{oA~BB3G8l3PhXTZTxEOI zrtyb-xul(bYvO-F7dv{(*u;=Y>r+~d%cZPc70G$rzid13#^;4yq@v>r+0>1u^1x;{ zc4d%e$B_*Oq!y86yuw5!)Gv|scRHtRHX_$?jaoRuyFL+Iawz8>bLQ%u%+;@fhDJ`Q zFSwjTaj)Cdz}`<$8y0mf0gah(~2y6jo^cr>O^ z`l=iNGv^?cx?CK!^DRNOFtFaLH~Jrk5Odi5sd@`#zDzOnB&W^c$=E8;g#s=NG<)%= zk92KC&}0E~u{S82>aumtZ2K}NO#*O*oBJ`MtDl;W6-Gk2f@oUFxe|XVnW*$%N+Z-9 z`JA}4bDQLUiM-iqOgXA?#N;LPD@31x*_yra5Mr;VuY9YUX=J2WE3RGj#8EsYDy4UV zURM978Uu7q2AeY~*m*Ni2FXlmFu7(an-L0#!|jC{90@x7a)mpmmt(r}QxGx%LTs4J z(o141{Z#CQ{H&oI{ASlcv#q1&df!E|o4K?VF8oT^KP2rYyS$?^2GieGvzaXlzkP?2 zy0b?O_hYRFeVNJ@eVi?4egNPk|9RB@v&n6N~hEc|Lc1Sb@J9_3p% zHj_JD&s9%0tLfp$kv|;x2-egYxV{PP26IK}ItSHcz!ET;geCRz3vx+O`$L3%WJ0Ol9}NT<|6{%vW#A!@pW{gNAi9?4 z2%rsTG<3K!Ar&~5@&l?ZWr!?9QSzlwNKv#!PnAev>t=gu?I?eH_+2VA6ZzMzZZpS+ z23QHK{n5)Vp`@K$W&1+3V1BEV!kDtGL3|1Fe;HPq`k#2s@Vr6T!jl4A@t1tV9N~OE zH7U)K(?A3e>!;6Hw^X*Q@#V&T(5ERCGfPd(ODYXRdRC@b?P?Im#);tMN&m!(alPy6 zf-CLZ8m&s4-B{xir}m#XaD@Yi`z6OMgBi1MaaAhgvA;|HQW{C{Qa{>2^b}m&)vb*2 z)r%pQbV$MV4St2Dl=Ii_;bly%r5jF*hVK1zbAD{9cFXZnmwOS3(&3{_p#e*%`~Sb8^~O{KZ^W~TXzg)u~cuoq24d1o}YFONs>m1E}SNm<029=bSsY z=yFUCue^S!@_6wO$jdZT`NlHxf3EO)x|Y@uVJkZW~qn!?}g&=U-jsN;I8~AeC37H4F zWr_uI-+~3FfpfLX&ea7o+nF11uV!ONNWmrVnw!Z$PHarWq8)E(GXl%I8B$cKSDf*J z_t))=nOF*b!Av~)r}HtPq5#8!?&ElrU7OW>yY z4kFj`@T)OXg000bKA#li?^>ogqonQIdfRnd`@r{tpRj04PD|-D2G*0>t3ucT3G9F| zDa=BAI*L%SrTNo4ohAG&t?F7YRe_p}zb~P)M0Y`zGb9=w2$T>@s)hvCgr%o}YRd&Z z+*PIOHUna9<%r%*L>J=aj%@z&yK3(~qD_2!TuHEtd&qGHgu3sWzW=NI=EX?`YI^l- zI_EbgR<`jJ)n(nR(fiP=!+O+V8^xp+DsKOSF{FR_bYNooxRg%a{}#*)Xp>}U6+gv~ zR9l;E!Rg46yTte9XRG7<h%Tj-T826u%)#7Qd_LXu0wBoI2Au|?= zHRlJWiC+^*9GrqiB@-n7w=nEe}IOJRZ;y;@|IpI+hV;x{0n4mm1H> zl_}TJh=&B{TKkT#m*=>@mzI8et-u7+0>J^&6}x(IaeBORsB?=;-?>*a2BPg`lw!WrjbY-`4S`sLKQWjT-*fM(e z56Nj+Ogs*jJ-adV0ts#JUj3o-HZf_IM`m(S4IX;RiKiPe9J&h z7DT1nAt92%@JCHDM05xYu|`Q@8$WGPdDFYxMoTocx=J?Y()eL%S#UE^seKt98Lj7d zVQmZG&dGDK<;U;R-DFYFK5nqDa2{CJVDYOKcQcpI?{T z((i9ylKwJxf1v!3%C!U?j33E$e4L54o;3Cy&NZTuoAizk>ml=~g8k)m`9;z|56Qc#@ZZe7REzh)R|O3%`-~N%4`I2C+WMey2FqWqo*k&{ZptAJrQ+1sOKE544R8Nus!2GX z{Vuu1?s1Of-hPbXQkcFUKlkh~%)t+EXI27OGn8dJVr_Di5h-Kua8QL4*D*F-uYQ## z?Ed_W-XVL6Q;0g|2QkMnCtM}C0-6f9&H-R(YSLw$C|oVD-C_exQJPfj+8+OT|tkgA!&ZHIvk1 zIUly`BuTGwNqtBxu7T7J^+EFOa79Ko97H9;9LHn|%-IqFy!1HIbxW2jQq|GXm4}3> zLuOG?I8*Vn6B+&09}Am{ix@tL(2eB7Yj^y*wsZmu?}#6Tf_WX+UIpp@Fz-0UxlvL7 zwdOpR=lNm@2>e@3Or@2I85K7lU zH@zwLG9-c9L6+_kT5@*Tn8R_0*VgVpcAR-f{!KaY;f=GNBZA2|q3qPLZ9p|?5KiS2 zi+j^oQDVIFHfT&V>o{ljY~rjFe%E&wdm6fSN4^qLI$J;&EOkPgwZbJ<3Fp48X6%_2 zlaMknVnsl%L5Fs0PKJKRa`w^8V*;L!8*l@LWm|-eM9!R zpLTaaFGtuaWG`R_(96&RAb{6c%2fFNCl>M%G*5%0Sj+1D0oTNjz4Inv-MDuu2W`LW z6wW_wT+D7vJNf4JNcK3lJnuW|(Jw=|!80d|Cqz)bDeti)`Qx@8HM8+&^Gi{$`Ar30zu0_tH|(1-*n3P3b2tCe zU8Re*ZBPi9X`CUm85Y9%bu2gSM>78cXtB2sFy zyC)I`<7AMY(w%UNNh|t4B=jWTVEv=KuZ~ZrtK`?XH+GHiZrR#Q=YryMG@Ks6!VIF_ z^yWjA-#La^Q1m?A$hXY7IwtlbiJjkhWkFp`Ajlcu8&*nPq1kg;=x_{D#SN13U+Hzd zav|$GfkNVt+R2OSYPi}l?QxSEyv4JyM6l4^e#NKf-G|5~hU`qBK8WAN__zx;sk`z^ zZTVCPYlJNcm>{S<)%CkQXEG;3`vk8L!BqDyr(DySQ}3&)sr(GW3HavAVUV>()cdM$ zZp*;UHIHB+F9+#B7tN7B1j`{E%*NJtqIZkul+Z%LHN4k+%@5%fM^vY+OMH&EI_gfQ zhG8>;3HHt_G;+ksP2TNt%@di+BP&rmfk~BIexRgbE!IxPV~%LGskDh{x0GHE# zgu`hd02Rkr?C2j-wAxXJG+c1mmk`XITjDn<8WohX#K4vtmJ5&8TA{_1Ennfjbj1%y zwS95qL6b4kl}Vuw$FOdEt5NF)a72B@1`z#T{F>d}3DMII*6XJ&Z%Ms|ik~=$D z<}jEuzSA;?YyWe@_SA$9mPWmY&9sH5HTYRCh!Y%f9C zbj)OhpEwb5nvfJ-{bi*_s6}x^c|IItjyT1!` z1h4W3YKcs%w zifSXhsb{RUKb8L6|BzsLg^zXzu|I3@2cHiz#FMh0t`~&PKHu0Y`}dylbWC~knWx`I zZ@fsea=c8s zD)G)Nn!kT(RH!inL(hLgYf9?(x%eDnQ=5d;WGYTPTyC7by_ty0%a-jv#!pEzRnL6J z4gKtUN}wl!{79cf(oO_tJ(|8LAHa{MsjicJ@78sFmW!Q}{;D~9p!ou%H{E&E<>skFUO^n96qT*Frx2QRa_W%brDHy^&W9{+gu z4=Esvd7FQRm0OSM7Znt3AMY0pt|M#giU)+GNf~=G6(5zF%~|;mrh~q2tOx!9KfL*7y~_$Sw9p`O33D)P z&-CE?Z*wcrT!lHND8%a#S*pj9Y8WD6ck9(}hj76|-{!G6Aav=t-aOx<$;4iA*+7$C zHWtxfr}mwBuB0!HrkpPMAptV&?BS^Vgd#~;z_N2Ux+0Z89^(dSV^ntzu`lA^U1eA& zumTpW{gFUS=e!cOMCKX@c;a4Z-tz#~N53bhiU(ECmx^BI-G!mYS@V~jKLRYAxQx+B z$MPc<+uYhjeP7=Yvxq|DC1i?W4;t#P-*Y|v>^)f@oY5QNlpUpND_#>w;x;BMa7m(f z|IQ>M*`ST}R2CaiCY*{fEM39%|FHI5QB6hBwy}aDA|Rk3pi}_?6$nZdq<5)8C<;;n zB#00?L`4BbK&1&tkzNcng&w6NBE2Ofp-Bq?A|;`Pm-}(=xbNNf^Tv3;=Obh6vrqP3 zYtA+2tY)yAcg(*~yyoQhd?ZT?_1P1TYj`l&lv`gXn@Qb$28=DJk)9~pMk7#<*DnH* z>5$AsnrH5bAN3RWHj9As=SJ<`9RtpT_GU;hIgrwvj2|>Bv=TwhY|xLFolIvlWpqnT zOua1)Y+;Na_5r`um;@=tIMjnA3?Lc)e?f~9vcInN_y#Fa&(a@8I7K+oJ0~=*2g2hC z?*}ubr^-(PS&XwcNc}eg@lG7>AcGMhrvhW&-v_!$wr0kx*wxUWEJRPws6w$+Q}ZN3 z$5JY3R&1@cz^*;^r`d`htleN$lRN8CWuU|e)U0-Bz2$CS88mFd3w8OZH`lY?90?}Z zjjL8RGDBhi4GBelo-RfFNzrKMoNzLJQ*US>E=L?LD zj%Y`1J3si(d-ZK~Yjz1us9q{B!Dr&Y(?3_-z`vvWEsOr@-rZa)$c=7W{--6^Zh}$u zd0%gwe{@bw+YU_n8FUO#iS2mEXaM`sfD@^zS$Rp8xkooF^v8w$)|DzL_# z!-BiIeso0#Zg$S?_^vKY2lhc!i?&-=yk-1H{|!MtT`HFjBrZ=80$f(~L9*3ro!*m| zCX=9OkmY)B>uK!r)kEWB0Gamw7;t3GUSFZPX-Z{bqK}tcShJcoy{S$C2M_H@F{Fja>luE7lJy7|+6H$uHET zBBV9sorRS>Mk~x3@$4Pc;F8KRBf(3RHCD5yAnx`s@r-VZx4){r!*69p<<%J9G)(rc z?|>OVI%SbPhUaeDNl7KqdPkWa1*ew10}~dW>qEQ2!oTXB2KNOXuX|LFgu^k7u6qcj zuIgs(k$sR6xWc!hW&aouR^R+(6M=5>l^`U`S;ty)rQvgFb@=|D9RU{ zAOlHKuOH7+Pb79cmN@zNW_^lba;V8(atT)AMG?>C;?b}6v_q!!)+dulm4u^SoL>5rq}8<&w6_Z279-^^gADi zIH}s@R=^Toyeu8>ZJG0Q7vGn}4`+*HY%0~Y3S;muU^)Hd{syYJA9N`XiZ_Oj(@<>* z^2U=p2_;*p{7rs}s<*$kOjf;1_A#y0T~L8w9_$y~h+8*WR-NzZEa{YNGoJlBKVXH+ zW2&UfgVN40miS3rY{_|kz(FF+7KOUuD~@qQ=}xEsc_z#rK5`6?xIdvUQghYrs(T-# z-ikmiJLIFVl?*Q+BHQ!Pp}|l? z$c5eXh@M7nf#(GY9idg6p?L632kavv&3UK7H!OS&Bh$_cf|XxJ2ErNF-U_g~X9JyegH>F_MHz?nid!HA1WlRc}V&&un4G-nE(+cQ;5i-obnkei8Y%*A`zfuVk zYiu-{`mNfb}Pc`u4|<%=-WHS`Da^B>3mKpM~Ta5FnGgBM`ol;B-W<#ou7S9#S<<3AXD$q zWqTtxFG?qj&@e9Fd_w41P~e}6#^wW0bqHEp-^KKac)0$@L&FP3@3%fJbcL32lH9!^ zi^A?;|MX0MF{z7+bCyV1F)3*Si|el}M6_7ylUk!)If?@P4{tikE9=HJ+LhpvXmD9`^VXQP^sUC8-uTkKUok7horomlwN6S?|zdlMEY6q;83m7 zQdV}o>t6`}1nwX3V?eR`wD*yBhoH}kQTcBMr%H6z|Ew;P!5Q}#Xb2$Xg@D^>VT{R|2aIR zPg9;NKw;QYzpYpRJUt+*8NcQ>tEOK6r%C`S_d?z7G*llo2~o&U*_p^ zd}q)v^H11WYO)Vp7&kv9q}-~^xDJ!e?sJZtS}AYzT68$)Ee#xaJOz_~FFlB?;r&>{a=YAbE9{9~=x$ctPxBG$w zJi3?J61O&TJ~b^b^Wt^u34RzEy`(8T;LtYZRYbuxOs3 z*}{=y<^$CrLU#GBxYUKR3+RK77oizaH$az8q$GDo(kAzC6#FBrU`s}j@GN<1TF=Kp}kBe&eVncj)QoI<`atye3kU#Rf z@sMXZyEaOE&0!tvbNazpX5mUU&-HZvHYEwm84JLe7{GAmT-n(L4hKr|55X)?4Z*0Z ztwKbAvNQg?28G@`g3pZ@-Hp!_#qdHKQontj&TdC~_tgD+R(V0SD;KBg>)I|$xQ2NU zcaqe1_Z|oJX!iB|48BViu1PQH^6KUov=^L`qrhL8zT{Mhfb{l`(#IN z^)a(q6DHA)5K4RFl)Uhhj7s##Rzm%{3U}lsF*4##2#itKY5TPX|43@#D5H2euPs@O zYesCN4J``AGZRBq-b4?iyp$XU9331cc%{#7Y?e~_s8UEjC(=_Tv%yL38d2EUtD@I%)5&`GhsP>jX*DOyj9#TM=tsX-UAxnvowY}LV`_gxGiwaKZR!9 zla$Ktd?C>`b>Yi)N!b0_+4y6?BMhSfMireWcPYmUq#*(#BXpsE&?62_7FCZ-@rGeb zmI}z-Daf>7J;-e`s5kzzqEn7w!oHQn_>l+eLApV9ZmMA`F|~GyJJNZ<-WI#qSV4^w+;_SU2K$^$Ki$MDEbY2U(PyIB55c*7KIDByI4Mc zaHm|to6c{fuzohHepNOXqr1JCNZ%6&dxzwkbUQcU$Yj?1I8j)XMMtaEBd5_f42$;P#n4#xn0KL}xm zE&ebQk7lj)J!zfgNSn3Wnq}FXr8dm8^c@8HZ$T%fBQ5gs!6UO zdp8SM_Dp+Pwtna%#lh|Iia<&af1*_;lOkx4Az~ z(^uQGW;$R}FiE&><3RJiLqF+^=A|lb7mmK&=FuE}CAsVoJ4g@G{(D%_kyhcY%myyV z_|;@)JNLeGT`lG*g3GfN+2t_F=cwXOapd=8 z|4E#mrQ`SC_vUKD($L>-7}=Ux8}|P3CXPYkrcZK|;@6eLqu#=QZqQTc2>KiB+!U5O ztioh9kRl4Y)WQ|G`@R}{>NFX(!4JEVzIR^n>2t1_q?%A+pNm&wR9m(5an9tW;Ky)Q zCc~Kl8EecZoC-jXD_-G$Pdk74+1hvv?9QBS!!K8bb3r;Rh4=W~k-%$_OU{p(>Q7mB zBUKp&YwjV9dg2%`3}+KQQs}Vp->8E3k0Xx(wf)R!oOo{O&z9LxcBxnSr&+l!bpI(4 zTQOV0!o$ezbTM-|Jq&gB#xPqFGgOje;NF8DGC>>7KK6j;CoB^IYxfY|5c5qKL#*i- zP_P7@R+mCI*@H*rru}dr;|*S9!wMw06!D_Cb?qEGxJe^zv~Q%ZzP7OE9iPLm;eWqj zm>auS+!uKld$F&T8NO;T_~b-v)uA;}dWbj>vQBj?XGo~JVD0)hr|&zEt?2nfrOo2} z<@7(f;ie>v1i`1lOCs=}XO=bOXbuG? zg=!K7v?WIw$AIoFp4U0o57jqV-eyma;RRZ8as(rUY_U7x2vf$0kZ=0K6B^5*%fFRKMIh5#5g+F84@7B zS{a^R%2k{%?;_GdrQX7@-r(CtujPzJ_@lP?NB?~iT0FcjhaAcBW$^sT!6?n!wWH=P z-}~*f!x1xSd+?zc4@s9~vt1%1K{m3ZFP$6%9f?mlRIHKu>`G&Q@8TX|2ZVDka_~BA z{g{a5IF$pyczUA+{DS7jpw-aPBm3{lr=TT1@fw-cKN39A59%T7(c>da9b*ppW`*M_ zNAbyM6k+2g%J4GEXqh3GOe# zhY-K&^CRj%<&K?+N;(Ey;E0)scj0grXtoG3rjnYQ%v(V+&6$xELPQ6}nC5B0pUL@d zt01BBYN$2e*#*k_2VkE~e?y}s^+~K(#$7eG9W> zLiD})YMFlyNX9Z$jfbh|pC29Vu|;TWjR+*XIvpS!v0-5mr@AruPgB_49rM!sJVsbR z=r&NMT~Vr*Vu6u=T$ErH_8Jd0saZBvh2)m+{GH=+IeVx6=P<8oAG*+pfr%pTUikxK zd2KVs*U3HigVyG03x>xAw@3Q`n|wtm&L2nk5<13|BX296O2fC7*R{9Ue@FMZjo2rI zjMigy!SNcQ z5%{Z&as5|C=$2_;Wh@hgs>GNyjH5OI6F+Ua=P@=5Qyh@D$c1cB$hS z;KPsydA3Gfh>{NKk3mMJKdgA{6*-Y5Z%&D}RAKs#Oa=J&dgTBb@5 zt(C04eLEG6%LUzWfOXWS3xZMi3g4Wm#~Hm&`xtD0D0Hv(X@qk#@L`r9{qwv7Z8n3` z{q902C(-Xo@O>HZqM(r=xZ~z!1cobB>^1Azd`qs>pOb}q;p|Dm8rx@$JU@mW==w&_ z32wSkyXTol>brRve{{6Lis=nh|7t=d`p0w&ag-YwlQzo7+Qf$P$`UZC@u^{+bEQ{b z&)hO;#4E1Fi*j?Votb8kgmgTREc5ZRq`9Qo6~uQHg^s9%SKgkFA7*Rn_?XHD`&fE8 zx$}>1U2sZvYgPPhhtKtXVXg3@mWdl>El5a$iZvlRG9Rwq1-cyGkiRUjPwQl6YwzQm z8yREYc8!38cyyl`INK?-8mIUnp7@*cr*V{x25rDXO;a=>$IBmsz6s|PW};bKA1^=Dzywa^nH?g>@Zg5 zcjpFS{lQXy2A9H{&x4O%TjvR!WwkhY{Wb8ChsjY}%*D!QSC8_9*N#xXHD6c7O2)(E z&MNT9+_`Y~Fm1l!YPyiw!^2yF1DzEe@~;yHHuq-Xwh+Ep`LlXaC_n#0 zZRn){zeTx?5D&o#Q2tOoufc?=+w*#+zrO6QYUen?C+S)3%1{3OAS@{CEi`w=aF*Sf zTRArj0<3yx7vsm9W@?Y_O%9!;ydzA8zX}-1OdG54HE;`%=LMn!JIRhdfrd18c{%c) zgi4X|=q|m^T^#bzy=~k|!U}WpT#kgnq~I>+7CtNv(A6DEddhh%Y(ENk$dsShE6m%O z*dxt90A3|Mfsk=2oX{uwdS0r@Lb7^TT{V>~y_-gK1unZ%)a zh_>n)V0@(KGx-+cmr6Y#cl-K3dfxPrdL4)jLnHi}PB6phR+&9!nzeDdiOIpn$^Mz! z#-=fy5U^p;Qe?{?3{JLRN+v_;BN7&z7wq=)&%cN>YT(tXQvwQQXh~N$NaZoWUUV7a z;#G%}FR|6BYfCS!vi#&ZArLp+wSIQ?T>XfIH<(*vfU_R%5zBQ=PhTeY3<5;GSQqkU zJkxHL8JZ@J4^Zm*5X&1fF4<~9v6|F;AyePX-ns1*4Zp^eFnz?#y{PCh5YC6fgc*?{1 zo(Fqag?0Zvcb-#&5HJj+U=Ot4-8_G|cOYqLXq{3}ETbw=nbI zR9RJIe5wJpC#HVCb0+O|Gz9%dS6bY{z(Q*5I;X%Zf!_&xOXEdSI9HJqEO7Is?)5nY zJB_YF;*3l1)k(POi9<}qVu9!Qb9e^)cAU$h-RK}aUx#oD1^FNpZ_{9y(9dE*L&vD8 zel6%bP2DiC)f`34$2~QqSG&`wX*U}y{I{XbEHvAh3&dzZnD;yJ6 zc~BsWuO+?<2RUxH?Cv~iBHn5g@IXM@h6BWKp6xpQ3IUDYUbG5St(zsM$VxGCYuQR$ zv^1)O!LI)kszz4RNzuwt4^*F!w@ru+6WJ38-M0#L7{uUYk4wti6d<&ymmn?+A zE&d)Uc9enJW3(EkE3Gm<#7g$b8}Qa9L|L6>P5Axses&`Dr{F-5&gsu*4_=X)&F5*h zDT?Wvo(##%0>7&hmfr>B3+4Vn)~4McF$lt$&S_<<8!rzmh`I%)M5HcycDV4{a&RQX z?}g~aQiv^_T=d{#eX& zBYnOC(ZK1c9b_%F>FgTWvS+bydn@ZH-5{ivDiuus9^y5Miw>u$S~_8ZL*`fde+OIm zsO}Io&~?n1f{vE9(c7M8QS4p!tyB76wo_Ba%4aW#8 z>GD=f(8mY}hBejWPTJg^8++GuQmpyG9)Y)s2QBZ=@NXXYbylSOB*MYwtX1hH*0}4O z4H_s`{%2-g_ZdGeTi}NR=I`|43G8Rkygp`w>)typiN}Ca2_M3l?@Az5pS--4mGL-^ zno5`+7JtNAnjW8w`SNGVS0#v4=4W^B)B4@XSG(a6C=|HaCgQY)2;;JEV}PYIaLd$B zJ=W4&U9|w0JE``&fvD>WOLMUvhN{p`3x=cdK#+_ z>8z=(q*><;+cnDKhfOC8j^p`GP{Pnbav=3o@I;!^oyhvf1*^T8?AghlhQ;(IH)?ME z!#bvUcutbv$JLo~>jje_)z9DBg46>xU8yoH>k&+YdT3;~zmoWY6Fpcz-UFR(>Ftw( z(0wsHw5=J-euG&9b8|u0`$7*Qlk8k7t!A`7&VQP}`|s)9m|5fqYY8HZ3oDh($ujyb zV|SZYhAcJ;o($N)%x@q>yBHU+9Sl7pBV4+(_Ksf{Yi4#?aua*W^OU&^xj12OAM+M; z|DPWA%c}JW-NwJpS%3V9lGNVs)?dwT8Ou!9_K80;Vnmh|jBH%Vwkodj7km{`y~-vw zftc_y{Ugw8TvPlbTX1>!>f>M6AP+yi4*t1$f+~)+^NZK$nL?f2gqjtj{BA|AVAg%v z^${?hT_Dm#Fnt;laMCuYj8SG{Jh6zb zwj@pvoJ%Nm?1f8oQLv1j^VPBN@Tdko$V3hj6`nW>4vL?c$_R4fbu#v;A$VxH?^QqT zjpsN1y^EqI(VrhaXb$vNFC1j>22VT-GHy4+d&MDo@Cg0-@LY_7JScY0?C%xF!pU8d zH%d3nBV#1fNFo#EH95TA{ZFw(Nh;R+<-4aV))&`8?ohcg$AA+qmvaQ01E99VjhF@B z#Sats3^YHTl>y)+KKbLUdcqGv7R?aGoO0#6Kn6x)z&1!g@8!}EXnb6$UVAYNG`R1b|gH)$QL&6 zlQR-7Aa9NLQ{g%aoG1u{WzQRXJ$Bd$a%vvQbb+Ew%MXHDf7I*LtxPJj>plybEp^u6 z(=q7N`5O_^OV_10Z3I)>huzH}e=cV+R?ipf{IPjOuhF6#Iv-|Boj!Y1U4(LDVn zmAknK>!Z9?F0w)htq;q!01KEN1J<_YM3zdn1M#1?(O)Cuf|>bWuiL#aN*6DzSj?e+ zdrpIGPCRmm!CC4MW~8}Fa>>lY(xF`5vUXla^f zw1f)UM;k2^cep%6bT_N>18=ijwfyJ^9w(TEW%;w6c$m9d7U;QGJq+J9c0C4k8ldg~ z{+-x+36C@udu~uQs7lw?od;DGx6*iCW+~{bxgUDhdO7Jw{$ICP*&dPTU$Nd0w zD;>NAqqKFzu2CT>4qVS%pvtNVIisD8DsI@D&G{XHW#-!x2G};rG2kXW@))p8;&Ivc zjOG6UpJ`sJOqgb7L?R~k(F9(yg6pp8BOfPDevpaAw4hJaC=R5nixn>!p0OH7s78$< z+Wl`{WO|;xRZq|O#QC}0U^uO9#(*XAmFmaSM{On6sXAZ8I9gS0TJgt#nSls1*q^wh z?@(LQRD9?#Wo_DZS^dZK)SaECgdTUyRDuXFd3e9c3@>+v>Ha0qRfsWs^UpC!o%0zG z9rG>X5py7tWo96th{6#^!8Ak0>pY~Pw!6iZ;%$9pw-PnBLuf}br4&mEdnLhCT0!%V zET63FdMSOIk|N@5ieEk~a&)A32fvG)rP8ojZxD0Sgu4k1L_}~Q@cmfHPU5$bn%#K= z3Zts=v0|aR>ctTtu`_3p*^$v?M(MtK83Chrp?roFFRW9`F-ugVX8Eq}O?a~iGZy>a zb8)|%_(m^2aqH@ESu#>eODm3|3@U;z`|hu&&>=cOS= z{6=|iF9A=3B@+!lhEsWNow}pyN5&8^luCJ@irksZV?a(-C&$r1S}sk0WbBs`EZ~sa zWowN7?HJJTZ-lYDAC|u~YWM5iLPYmQh4P#y{UMwi(nP&6$Kto0I8W)UGF*0ACqM*? zW#e&zSJi_k>cvWb_j}!6mrby~DCxI0_PsJRCKp(yAjaD_>=ODH(dpgYh3z=#9O&Tc z%kveIP=cn9j#H40E`I!`{P2e>6p^+2jfvuoJYz4MYC1oZ^*47`UV=2JgP)p~nFglw zjSVt~jn-+KPbpc{mNdf1CaQHmHUM$Z&~OREe+=*()wuB(Nm5!cbq{s3Bg*=AQLnls z_BSq-3e=mv8Q}$~%63H-D*0g0+??5`4KV$($S-_3lb|7nFQ21wV#vy@at4^5B&lg@YWL<3YV_uI##5KL>=S3@ zK8?K~V!#Q%hJuTgJV?aK8a#~VyV6uWYB(O=`@Vfd6bBP8@Ac0Cd{v+_S$qcNSI*GUxO61fW3zRPx8zXpD`{qb^((;PQ6!p)Hx=Tta7B6I_|WPPU! zi6G!5-)AXRm=~ClPZQ*|BU%IsD*r&<{@h)Rb?w4Te>W4?uquDS|GU`8tO$Jl6mso` z~7F~Ub;q=6&5 zgh|BjW58)U#@mpCV?d?Q73_`xoNPk+xpWNB-D>AxeD+&nRCKLbFyllIIgbH(;3;8+ zgHGn$&Kr5=&%~P_@l@yTe?L%ma6}9-3kjDaz5(9ceVM14JNCO!WP=Uo4)-zOjAp%c z`NHAA22=2o`R#wx%rc++Hwjgf592&6eJi;glSonB&8>-Y89RWxA9yD3=GI2p4C014 za8Uz`HA{O!@T`TswY?TWA)iPm@SJ6;baI)PiGQ>CX(5eYaksU$E0K!WO;-foxD?S? z(N&U2`GALfMQZd9SV{l*homf0!5{wdNE5iN)*V~!xQ+-zz#P@`l|M#Zo<2UWo8#JR z9I`>}pnqFq&O&)*7i?agNdc-!(IXFEdw4U)M1W+Wx|ZdpQ#ftzmc^G^V@SG{}GNlTpvoaE`;pCms&qgCS> zzxnWXrKNWLxZJn!5B+Ywct|}JpyZfauDymH{={KwX7Q2@Nv%rOA2sQ;eClXL%2%`xKc8nNP4ql5i2 zhBt66`)7Iql%2Gl0?}oPZxXZ7_zU#gm3-mOUmJbwgBR!Tmq30>EXr}xmy6C{$d(@F z>3R8V|J|J4*rm$G=}m2Z4n7fzkPn^eWJzw<=H8 zBgd-XNKa{kIJ?nngn8-4gfgY3Sn>C6-~x!v?#d99{nv-_WzDx^%r7kBBz&AHe8cW1 zSMzjbORGGPN_efsNxnl`JMiXj#WY8Law>?pq&aw%UI1UQW7dO5R*;rTIk3@*GkQ@q zEPnwUv)a#2Tw^^V{c2LCbCS^MOvJUVs!OvyUyr;J*+@KU!>+djjid52&Nb93GFB+9 z$+cht$g{R5`(jinW@-nR)2KVT=}UNl_yl@c#+F#o-vE8FH6PFhqx}?)qd;b{RzCd# z4a)uO+$IL~-u?W6X zil@6D=g*zo*~=0SIJ>5qluq}*oE3L`@O0N&+sUWw^viOJa%9lJ><)E*$|-F;iyfFVh<9+u@9++I_eJH_ zGy(MrO>6M$8TH^@NpMqQDbisB*x5gX|5R`ltGuT&_ZR!fUW6cb69Z<5gS9iRzt-YD z9sZA1ZSYFY%s&mAY4exDZ^!!%uVP+r%qw(Z6P-T2RnS{bFn1pcQ#P4)L5n;eyA79g ztW4@^Y3!;TSC;RU{>0T=pjiAdTiT}0Em-VNsO{o6Y#-@^`4#@~j@XWX7iz8!w*n}c z$`*c!Y2#s01L84Pn(yatpUU*^E%{on3;wI`x(6Exvph}y`pghk46PHZ6<15yBTtOo zg4T*I&Nv@Dx^vqk$goX4zBnEs?v=h!1jaCcyF;al(Alk3|zYnvN-GR}PEqHEAw zHe2RYf1IU82sb}nj}XjEsz@w!?p>G2Z~TE=nYiH%(c*K_t>B)H;rChD=bKPIWUY(`Z9KX5PI&hSDz3P>T;oZdOvt;>&|*dSn5UaM+)D z9^=Qa6s!n~E=oS*)%g%3EW^~ln;5s2jwIW8K3TUV1NYZZ#0?Fe@)2)Bc0+@cT)LEr z76t?vguTtQp4wXc%gvakMAj3BH7`1Ym=m)5Asm&EHQ6snIFO;2I~ zzG6aAj>%4xh!+|{;hxkB1Wk@GQB!)x6yY7!Fn`+_k>xeigpnXa-GYFaM=q4#x=x9+ zf=k-#Dw0-He%`u`?e5Lfe-+A$)Wc1#_EaM_qYA4ZVd)_TXV7h-`)V#ymgg$EQkDDzX(XQ_-e@AwU5I6rK zD^_;#nVibtK1fzdTCLAIPD{$9w5!|XV%LLQL!v09G!BJQNB#_H zcO>~3@PY8i0a;`%q9v3s{oqOTvPa_sI%7StVoEsSGW*wl#fNf7R@%~X zjBMDR6pksH$^F+;W$-3OygILVLDzp#P#fgo}x{vWmn#K-eMfu>Z&O`n;i zpI%cG)Fhm@m#U0q$3fdWj2)Z_uIcDT=+f>hk>7u#E$rncBq)5s(%NhPwtXvdN*jxs zo?@LP!LcQ})rMn&H@S2|-<^7OYWQ>xbvJzQW4JyJ{)BP>{vTRdJ6^^7m~Y$y(X>nO zw_fugN43&Iw;~mH;jaqDK^*=r*KMd`8Kfc(ng;Q%MQh z^}uT^%z1kska`ENm2?hxm3G0X!E0%o(92%KYd`3mCdX_VMd54kvqcX0gtpN;^TuBTb=#D1>Lh(~gDqVu$dPQ5Hj^=4 zL4G9Ko59;%3blLu$m17A>7?^8&5y3i?EMZ1YAy6^VEM-+U(MEAg#Tmz3S&;aV!G;ux&J)Wk~{EGySD$#Ro;x zS}s82LyM4qcLhwfM=4-Id1=ND(~bgUBhnevND@)%T`c*vw|ZvhDHns?2)D zVRkh^uqv=MnRrjdVUZj|xGS3zrYJGPB8r|j=W}Q3_z_&LjeXSqA{nyG1=$?!71L9C zFAZoL3JSF0;M^(Pz4s)S1bCcDJyx&@8y_y)@q1W9NKFUKe#YJ89EU1@6wY_M2M@2z z2Nm8hvYa7%4eg9|%H0|5D;v9rNSk&C8__}!B`E!as#-HuUT9$-5gg=1G&isqifHwCCc>8Gg~H5W>PA!*@%R=ry6pQjbePT-v~1;6#{i9QD9mLuP; zMxw`mj_#}4t*H8bp_SHENi2SXy%00yJv53qOIP2N#o*2Jfd^e`@|>-*e~Ig4VzyKs z0&Sc)1%}*RQfGse^cRxNddlYSV=U_{9Ah~mW%>T|h&v@SbPoUI&GkldWy(uOf1h7T z+Qf^&$^IJm;Vgc9AU-UABJT{n2v5$QtuXs$_Jt|^b^3J-5K;)ws1C0483rQm7q6{0 ztO|&4K^A+FIN3kyPmzv-nHja7e}X*ET`s*Wwt3Rv`fD!LtGh=#m-6p_W_^85375X9 z@7D8+fG7JSpcjn1*Mk`GrzrEdis*d&6g$}r?y^4!i81@e|!|hQfLQ0lpmA+ zPm5$BXG(#XOKD$0PxZeUL*&%b#tUc4&cQ~%=@j0pR!QoVvKp1CGoN9qmj>XMgpVXQ zYmNaUYPxnp`?N!XkNLr^Q8>NkZkdYpx7$V1QRwWJ>-u;lu4gA!G|^3b3|7)!HI><6 zxj74Asa7EsUxYc}OFc}Zz=PtTk)zQy)Q8bL6DgxyM2VjP=Wf5`DrLvyke0dHPPZ3c z!ff^(0|LHcX(nT+FB931W6Wu5+jkKS@ALPJUlie^d(FTu$|{X*XL< z_ubF6bSRNlS=)k)lY!Z2hs*|nqX{RxDCkY7+Jt55?b9Y1xn4=>w9yMeoCi@%Q2Au7 zYMbyoS?eAU@jSs>Y0d3~wydzf1Qy2}6I$)o3o?|*oY zDg`CS2J2Oh@Yk7kr4Tc1cE`Y@`}(#Hh;gKSaUec5pq|QR^vdL8-jjrq452r|VW$K* zMMp^EP05>ebx{|v(KUI&6;a9?9gH|qE|0^8UWM+8Q!zevw!NhZ5`Vvf>~OdJDWAZ2 zeiEPMssJR$qkP&Fd%<4Ut<1#3zJ5T`A_6}TIw-qd;|G776vBv<;-7`c)fGpp^2cJNMH9T=j zb-)$djg;1HoUPc7Df@7sVs6XO4x5El_a{i3&@Z+DL+m~j(xoHr@5auy%=x`gM9^8XyGt2UiQmxpSIk0axr zNOHsh-~Y$3UU0AhA(+ilaSFKt0#*|d25U}wWNr_fTdz*MQ`@a3E3S3ho;qhH+Q7&9 zh~!vY1%J=+R%pyuxC85^cFv*M5eyoCM?9-|t%5-zP|#k@95|h(lyar|iHWQ3{?C(J z@@C$wynRKBr`5Ge3XL7vq93iZT@>eBXyGS_(-760FH!(;sb+&ZCnP7J>Z0!N-HD}}MI3OKWZJg9l3^MM3GnI+LSu&Cjn5z6?<)hEl4juyujp4oZ7-er2|n`a# zC@a+h=o}nkQ-iDy#Rj(GB5uRUp3LF*{u3^mKYNY=58LOMxR?WTWM^F%A(Qal`qc9T z?&+&Z+8o>zvc*}}J3o@9cxpD@2=7a3u)^104!P`qrueeajf9sG%o`mg)C*rpH(H8H zRSZR$?-(RyoQ1nf{<6B3k=wLq=b78KehkUZj*B%38evhk7SRH+ihTc#`sbXI^w zb=I)&?`cVwf)xblDf*rgtejHlM>~4ZUto{BrIKXoec;Q4YPjEdTn1lQ@#~_sWwn*O zUOVf?50pugq5H}QnDsnRq&Bg@UK_Mm1*(bSX6adF2fBYn=;dxHZ+94fxi zne1tsjaqTCVp^~CLElPQuu8qW92kB5!IwqJk=-u5wIK_d`kR@-9f_LC{Skk3Cigv@ zksQL@xG@W)-PM#L9ad?DojA>#0QmV=cL9l~qc;8;{U5ZXsbz7gotNhSAOiY|r7a#CeaVwz21kH8gq4X}Gb#{BdQSYdg5_ zLlawYDR4pcuA50i*}nC=2dsl8aUvlL$s6QH5;mIhQ%;{kvgvFT^MtBPD&)?AH&uc{ zLir&fM1qwnY^QcvEp{SSk;A-fMT7mb8({rTp^r)=^IM zM$!Ld-x~CabRSw8d5GzZGJE-NnY}Joj{!^VRHo@eU?sB~YD_FES7>jYo!m|8INJqn zEvYH70wZdOvbEFd4~s|N+{@N-8wwlmEt4D91zsUxV6}i385tRh{29(a z4N%YjY8MK>EDUHAp0|3=EVKBcc$=ZVrcKc8s8I%Z)r)^_@AhNGzeWAJfn!FaRT85O z$${ufyT22+8Zl*!MUKh zZ-~v^uM((nFe%d4BP;CuxaJQ?a@yN1HD?}_8G9Q_jheG;$AJ93xe+Fe?<>iu(C8me zmooao3J3#e%<`Okmb~hbQY5YTpU3`xBYFPI9nYo>*HSW_urU$DUW;?O8aaX6vg)-4 zNo0%+U1<2nbQlSHxXx8P-}dUf%C*$z4Fge1jRejT_dlsZ3T7&)(!^m*i2z-dDHxyF z#x9-uzV=22lf1}q>z(eE89Nhs-fpUOg2T9P$oaYD!$^I*xFa7!j=iToX5WJATrqy_ zX|vA2s&GZw4L=wmsn4-bkdlTq=hA=lE)Y5Gswy!NQD@3(A01lPpY5Cew_fy@zv;ac zqsyt!wf2O&h=HXJw{%>pie|RS2Da6g-6aq#(1>Xb3VlsJq3xUKn zn>`K}XwgoXUS}x?(tS0^HaD7=5H`k_YC}bI;|lxh(1&bno)Nu)^C2l2-3hd5qC@&EQt|F^$Qn?iKi zPg)<&9%7IF908ohU0YmTvk|*-EZ?DdkJJrGbAk#gTi|!w1@se5U^`VF70;rl1nWCR zl)<8R)XE0H5?G8sJgstsF4%(QeVkl`^8iQOcNz zL&GFwwphF)rLMV**^c$S|3WqaNCSeP;e;A57*ycy$6z5hI-1pztIF6SV_8Wz>LvDr zoc#0C#fw5uG|JdbO)t)Osjfq1>P7W;wS}BOqPYf>H_g#wMBkZ7pU8JGzn7$ZW@IDr z+2GGW@5bMy7adef59K^}Zd@K67-i>`dgF?x5?58eZ0w)|vdS7C!>!i`;v=Fnbo`1j zjNg$&Qkh1^TqsT@yC=)XX=8%O9?NJZ*1xj#2*J!nxTJY2dUHpNCQ#-^;Pp@|KWN^> z22JpcH@8`ouRE5j=YUhAt| zY(&GOsMS8)A*&2_j(}{aK|b6WjjuUjtP333Vm=O}OYH9rG|KwhrPOolN=W>k3M2Tp!s^yCQ18NC7KBXtX+%Gc*VtiSLY|# zlk|AtjXE$=9$`8yi(cjQ%9}(yy?#tNY3i+&9dAJX_0H{U$c$0zB9r#rU&qrPkXHNj zR*fclzC={{69QMQFRfKDkphj0?UedxV*hzrDoutvX3#45Exd~NGUPC&yv^vcFJ>jd z#hA4_XQ^X)=E6->Ip$Ez{`177Yx05dyknqxiaR#J8qt*bnl*dz>*yan!OzZ~yd^LF z1e#>u96mUlY?)}|a?ShN>${9k6t)S~oiX@!S=kn%L`=0Z;Kdrsz|(cG(tEm{v`!1+ z)GqFb6to!4+GIw^+zuH^52_ShKG|*OEmRO_*+G`C_rwT$i0xnf)nUcXnpu+=Nmo9* zu8SU#+4&tV0!h)p>uM|zHS=AmM|VHQeom~6KI&P^0A2bVy=gVGYYru~B#|k(6?TV- z59>N(%p0$LcSDz$1dlI-AUW=|0ZEADcT=-@CX8m1Os6Gwx~Xw$Pqv7IpsBWl;OE>b zO0s*2zDCI!`w*2^o{A}FN^`v<8n>PGFu^0>A>~CF@IjIvUUUejx-~M$tg<<^nVx%NPjhko(~0?1EI8Zt}fwkazi1FS;zcvT=x+gM3>I zjS1F4qT2VDhJz4a?RzkgzTKx24;W&I-_Ln5efrTz(RCH*Z4Ry#7u?yd0w3nL4+Tz^ zx)?vkhzfan7_U062?QffJh9K9)FHG9pQFx`7sM^{CqK(yMZxUR&W_hS+Y~N8I~4H` zK)?n3WRL#vdlCPm!Hj!v`sEmYvwXqxlyE{FJf}0`+wIY4qhJ#Mz@u-CrbtaZ1ZWpU z!D&DH(EBT$wQI+=vz%J#--@+`z@8t12GEX$tzRvYD3;1ax>=8o zzkD>pagOj3QNgW!x~Cvh7}1U3kE0JwOE3YE6*R^);!mh~lT?=Ecvol`8$k}@@cT>$ zZ=1>sUu2b8(OJj$P(8PNXos_G{I|xx7`7_0it+6#*@E2ZXBbS+YiA~zYDR)C{0Vrm@oFHHrI1bT8e6d!qs*d4KX?{(|ovt|H^Y4`I!8{xbSUO|77k>Ohd9=~?CW zx$~sxT2G_nSBacF|4eUG{r%SD-$k?~^KNO<)Jt$R&l{HkoSE4wCvGk|6oqtjQm{%S z;~YzrB@C#6=}UlKvW@fwty{UK;hFgV2KxDb ziKmr`he3_L?KejTE4%hJ!vTyz9v49`9Uo9N7`vzJ_DUenDc# ziK`uk+5Ok}3Yi4i>O2RcWo5lczmCeCyzyvlp)+(+9?;C-J#j*Zeymb1`=!o3s9yiW zb4@%}=Gx5VuuxBqe)GP;(PcJj;4r*R-<7ryavB=h-rTWxeqkq(;nL_b-y*E8?FTZc z$+%fJM#Ci)SPFWaRK99$OL(8v=*+E?SeZg4HC|`E+<-IO;HWyku>5RyD}f#QHPazW zR$NN8TeF27T9h860)R=d0Ugy__IUjB$ z@q)~us{(kfude+++Lr^;%V-a=la_IN!Pb*@8EzEt$yu3;KAt?Hy^VtSPY4dSJLbT$ z@wPSEzE!?_BCH6MP4%lki{QRi+AUKr76qQ07w`_&Ht)-@`BCU(Xf~7EcVSaqp3F&T zx-cHiHkh;@MJw!jLj|f=ywl-WeD^s`lA*-JO|*^P%{6}s7+;;f z^jGKyYvv-FT6WNXk~}lGH0{H5wzE4X@I@AxSLt_`xs%GHnK5GS&UHA|5JmgtI5=cg z`bY^K{m|Q}q)B^GD;;I-&E;&DzUC2X+?@pv%#iGnUf- zCdhGan4PIxOZH;rNPb-dUon-%BvS3fT#Z^Jd_9PyqOhQo;QU$AFqa3%_D{7A7s6;~ zJzXvHWo`^YJjTNd;U&9xkX_v_i$NK?3AX$?{rn#w&HAgeT`2fC7wpm>jih0_*GUyh z#-y}7HokVV3JjQ?XyoZ=?g?*{V?!n2SFXkYB~KN)akk zf%>!H3H+Nj>?eDg;VNi`na7XGC|(GwxMcJUsE!9@(KMzdc%JsouF>+#9`DI$*;VOV zSZlp6HekMT7(gMHmL z6v|f8RZh%>C9O)OWubauU40g>hZN=6Drwa)-lUpCKGo|in7$h{m2qM9u9FjKt76?m zp5`CJBNT)I^|^V!$FTkbj2-?tET3ngLOB0EW?Z^8uWL~-N@?h-m43Ws9qB%u;vF9k z>u!;?cGgF2bj8UVSi;QKEnV}KowX#h!0#o0(wQu{=99>|gH?0>n5uar^ilrPjH7u) zGyD-1JX`ZVz7suoIT$r3dV{&lUW`D<6r<)zsc=jZu{M#~6oLFt;w?BGwPETHha{pz zbzmuz?79ry4S2aG~8W5v7Q8(AQeHjPWjYaSSZnaM`(>`X}g=VY|u`F*> zj@19K-VfH?F1v>JIiPd`3dC>YX}9#|mYsMSXOgG!va{W&fJubF&HC!~z85sf)#g6!uz9!EgO8uSYOK5p^eK(DjNeD@Z?1s{j#O&IB{nTM zc2G@EHZ#17FEm{2c2(c852~z+Y2_GLgo60JH|h;22V7(P7e~T((j;QLOgisA9`NbD&d+?pD+R89s%3(lOocx=4#D5p0#in$B|`$d2~ z8d@f;4(Ni?2QI@WC5Ok-5f=N<^o{MCjw~!X#WRlha7}x($q`u$HQGj|B_Rt!){$SD z2T~+V8SHFO!C*FDP#n!P+HIfAKZln~L94yyi7;(Of$Wk_NmOU3mqU z@*4g%R;@hsECTl=w=4$NS823=;{`u?dmN59Nyg&cg1)*D$|!8zVVW*3-MNR$nKa<; zkHzeue4Dp1y?;Qp{a|5yC)?jA&qafge3tWJg~Q%Bm(Mx;A8OU7+}`Bf~kfm_3#Q&%qAF-})o zw`wczIpnS{3KO$3R)!L`7am1ABi-sRzfN8M zGIh&|xmv4%-uSc~s4hDK8*q;5)50I#qSr$PoDo;Grz3uyRUy-x5ZD#9#V{xiywtPO z^OGtZ2+&1U%?b`poTJDp#fv_Oe9W*?M&`A*eHcX0ej{@mgzdD0D}<}(-UZY)-*?RV ztMoc{XZoTXFBjQ`qtKmw@x5x|OstBgatr07Eh|b1%CSc*1gp9}Y#z>{7I~M5e9mbB zMlgIi9-pt$H}DGjPR$%3Lh$k-QTQrAazn)Tf=wosh76AqCz-!JivbcQ;IFOC+5)Sm ze8d1p_j5s7|11#i7Up1&W*5|syf zm+y1G1pHGsm6n+3rz8T)7aE-dSC*N+Q21Om98x?fV}BxomRrNq@X#AScPKt(Qy>CIwlg@Dh?~kNA0-s<+xa2|Az7a%KT#FeG zCTGRA1wpKKxOu`V#a~(HOeu6ey&w3d6&uDfCfBSKejYl$Th2JxUS-=`J0sF5W*%EZ`S-(K;kxlzMn7}S8`f0* z$SE_YQ=!3(5}~-~uvc%+XQjG5^rpJwXZQq0_($}(pF=py0Xd}F#HgY>Iv$RjEN(u5~D4a$omIC zfyr!UXl4UGdA9KweNGF_`L@40g{ro{13a6Y`11lH7BdM1&-bwvd*~K`Wr6$A?U`VN(E`_Q zUjql}YfTF~g{K!HFHOA(KNSF3q9WcTGCRW7$^i+`Vv4kvrk>yDT7TOntK$R;f;1WZ|`6^ zYgkOJOjdUVxod+mh`FLW)9q2-_QjKd`A2sViREPp9ut8R(`6O-j$n(xVo{^orsm9BU89l4*(aCv~8oBwn*0wB92K)&PWvf5* zMTK3m6IXgped$Zwo4TDjSDg-vy=;(v^%MwQ4-E3jBpHkHrc@St~d1hT)XnhX7&e^|IgLoL`n?gukhR*d%K znL98-xISd~($_oedjBDlB!1e^{pd2RXEEx6z~M+Z5MH4Tn$&}LuYC6rGe^K$Jz7kv z-JI+>=3pB+M(@OB$?Ct)6ZjTPUz8c`nh%|@QZz@oy8V(7Emr=KK*54H50rum=qarE{+bavq8~4iL<-R(Ktv?Q4Z<+9ja~t0aI=_iXzHNEsMJvR= z;MhB8P4B-fTrCwKDZ7c{~B>Tf5NzMbCWeVWNSpBjKXDN3DmWT^w)avG^ zcb(Y3J3XB(ZbGnRmkYJ5P8aSNYU=y4)33QISfqvpk$2=jk<&bI_|iU**V1}ay&rh+ zb|ik0^w|E1n1EgK@17d@#{;F$d^c?to=JQd>$n;H34awoSr1K-F~(cTr19+y9%dW` z9+fZeswpGEozvHDrkLU?5EE-SDoz1cS+~8pF0*!Q^OhI6zYuC@TIJTZTf_8B=jB&X z;TCLkEqxM?R*f@xD*Hns($lzd!rw+cayG$wSPOr2W6wlRPbKDYRa>NdMTM**BHf_a z+vRIf=+z8lK7$7(-12Eo?H&SF=7xBiKHE}+TqD8; ziA(|_LiV8e)9~JqyY}PZovdb^%Y^PezUzCNxk!js@Jyh%DeE?k*R$*V6Gxkg3e*qm zdqrC&+gp!|~a>DLN_o8^e?9*jy*Mmg;6g+9ntRup!3bt@=)N?B@A zwHRgJsqo6fEMT7Mi41FEV||omKrft{*{6>Hrm?L~4f3Kn_jnx-xx#N@yjj$LF?oS| ztVpgNtl4&LrhG!+b-`T%JHZ$g%fwU4ows?@GlFu+_@ro8!2Ej!XY@IlW-Mc!)USsaIKG;cI4eQua5is;%7BGKazUc49(f z99!n0t-En*#OG%-qbzgOtE~YO*Kw{mmmPU&>Tw>^YPlNyqNf^5?ALLzc-;k=)FL+9 z9SL#sc8IN#t3h^#1I(yAfk|yw+ybv~IP!TceMVc)%ar5L~fHIU8CDVA^y|82%Q87v>7ZjSyGo`cj5%q!eX zRr}2j9=I@m)ie`#F$evy>@+=vRqy)EO5z0w#FfopOI8!^4)Xl71pUYivJA;ZYj7t^ zW-TjK2p6jWmJ(-(*z;}ulE)OBE0mGfJY$p&Q~jLhb3AY7XM`7@ehf+9#QN{4<^Xlg zT_&nu%*Bp&&8x>ecoE@UlG>li2$2oMT`R5<1*(9(`h@%a`u_pG+>&@N+!~jkx-MeQ zzr~jY7HWfuv$Kk`rysl{r$V|JSBzd5I{ASO2xf$gqWymPwiY@W&^`5BgYwr3TVY?h z@qVTlfVuYfJ6FKNXCId=e#uE|Qca=Wa7l#cX1wn-*}t#VXu0QQ)N?P*v9&vkY#=j% zTkwhD+wlgwZN=1Z06;c+69RE0ctT`!1XDZwh~o%MP?s-ZADgdY^G>4B%b#sAbLHYS zDd%_8p+jPq@v0x1s;T$lWhVcKRj@e(*!|l6@bPq)(^k&FHNV$Cd6JbMBO;i6;VU*% z+_AGUy0{iHB!$wVMKl@ptn494Ylt^3Uf$PEef zc>pw{?15A*H3whV9tI-&N^W-Xlk(O$=LxvEW7C9-XN~VD4eMr_$~hbl#Xi;bXE;ULIf_{3!s_99@gpu2#cJwEw~E^H z1)vW*7{MHb-?FNO|INlt^YsNm{Qfh;pT978e>VL;XZRz^vKvX8ouG#}C~RSWW*e*l zLqD5*`GD7?=6R_Z4>3~YNmB`u54DNJQ!%Le^YfY=`w?`CF1oAROkw5XscLlKx>1~U^9R1rk5#2lOyFL^W~S*K@&(x$i!?FVQ5PZ7Sp!qe7{Pqn6a~&AyZ#i zqH{0<0t{`?$$BiN>PiN)wC-(+_F&F?{OZqJ561ms0@XZTj|D4i$3E(In}9Rvz^oe< zO3FKM%HmuOgG>iW|0r-`ky^@$Dsamd*6;fr_$Enk+YQxt)~fAXB^wZOD?0X&%UoRL zMqegcQ2a5!t>k7vw$tyx3lmI8-DGPdA~64?&kWAQ-R34)^NnJO)p-y3J;PdyxsVh!!27O3X0l{6-CM|fFl|1dltrv*7s zB`kAexf4Ut=|5$RNJK2Yq9~4bx5jfK9|#)dc)ypVo_B!tMLENJ!A-R`YSv^fW{i5= ze9L!^+2Y!p3IOhaD)^=2<(&V{;lMXRSv%Wl8DXzeh1>{Kg|>-bg7)aNup?570`H1b z;d4D0GO>`g`;J)evmpGYYG;EQNlVccHsb%RfuYUyDDsHnNNX+G=bPC;;Vbf`S;gYh?bLpg4@FH)7gK34&sm}Ow)(BW>>pO`_qnDCljWM549#fX-jd`vvOf0$&Yd?}Ibr-_N_a{U&T!B!_d3DzO z(_%gY!~gJ4{*P($e-(89KmAt@R3AZ5G*|nZ2K@LUtuC7O+54HS!b+F8(w-(F1$piz$P|!K!0%_? z)#QHq(h$bYYdv)3@Mv^yx`gDTScXt_s%EZxYMry|BHjMJ5y2OFJ03$t{h9CSNKKUK zgNiq>j>iaFpKqdpaIdA2&#zlFt2}H~ay;|X0NeMK$hr7J~L_8e*axQmY| zkKALUNVPa`@T?D^D)T074_}nXx|uXVdbi)4OU4TjT5l!aaW}2+!+g=3%hGZV-00;I zZ)qRwLLbRY=n@~DV6YZPP({3)8pBN|IG8QUNpbwB*Iy1V!q*OrVOvM%8aW9E!`Ho2 zcb*p}Q(X>2m4I63vd*!kKtf;`wjkd#q6~3TK(b~i zmnGTyR)k>GwLCwskav-XPdHn;gC8E8mpu@0Bft8ilnX1B?^ciRq3m4m;sfA!P}Xh! z8NN}x>Qz))=Ef61$kw;1fDKK__ppdu=})ePu4PQ(-B0Ksc#@nQ12WgQtmq(rZk@1Po!QPZ33g}s_+@B+C7weP8#o2cx#h<% z_Z)<4&i@BkF|8I+>v1E#c+)(y{0GF@Iy>R~B*LdPuzs?AptSkz$5lLw{EhOu+vmd3a}aAGmcu+K^i-1y!BRms z(Br}76Usu*nuV-ULgU|{PSoXWWG5w;1Q|}cOCj>Itv{4)yf9%dq^WY5=?qq`q(1fY zqTeMudQJMG0k2Xu(=m@ib+k7jAKz*bwIQe{Kcp@Wo0xqe`dF~I+N1g9Xd*xNQ&Ul6 zY*THdsI+s5a8r7=7l}__VyeWFEu^nXIgC2B@DV@PAv=w+#I3@mTWdgTka2z0XwMH) z$Iqf?Cu1%3iezB;$&+*#wG|hE#vI{WD$bRi&v*I{@0Jm!;;g)LukYY=;=L{m+ZFCm zzlm&BktT}pW|Q+#Tz#)Y!eD7 z*Z&e4SIJBb{KX(WP=m9%b-8LQ&hI%fsV*mQ*J*`}=)x@bbS)u={m$v;57zAWbcn7>r&NoR=Y)y@kXeN~EuU5`yrSJ*uL2W*}^he{D0fOE2@BJ8;=C9%L# z_let*U>T%FMzlVMGS=sA;WR^6zgi72l$O?eiI8u_9R*S~7Cp^dy0yHm^_P2paTcZWcJHvpcU!;wir<^)-*nfyWa%uqyd5q zJIm`Sv5eL6+3TZx`nCE6P3-qy(JpU`sCR}Db@50d^L|+W7E0>HIgTSX_C|RW=lVq( zy$%*&O*I5@TxFq*>ua+ok7EA;PHk@&A}zzue;ZP6pPw;uCiLR`tU?y%buWuFb_KwWI$| z<+ADJ{p_s^V0KZP>|*n4o96bWDJqREGGGF2ERJ^ccvLVb`~wWt@}ciqx1H`=S~9S$ zimAy8C`b873P1|4gAZr?@~fu#n<<}Y2#vf<(CGrm>DT--lt zleXif4$g=m5Qx0MI)@hEBY`?sgr`@^KClqE|7|;7ne8Mci4$8ItFearMS6aN_~B4+ zE3|fExWj4nqOD(p$qhGr%-eJ@KpPQrXrVE1l+-T*+Vu4}9oPbi|ApQSS0nTscR48} zl|MqB{AlQU`0|mi1~+@^NrF;5h^+xZ(D8dfKmO4j`d@4GSoPbv(+lxClgwXO(~m78URM}&^phq_(y)8FhWV8iO~sE-qck1v62fNlphX(GRO@2&7Lyf(hW zZk1e_duKQI{ItBYD_$8eMuHc98JBzKOVYvdlUhf`oM_vhD7UWU4sdVD<_VDoaKj4_ z_=_+F6IR+~Auvwt zp{U**+~#^P#F7-5Mwr4tS@b|(Rdi2RCK;5mWuLvx{;pk0JxX5g__+ri{Xor%>0}eH z%3B{Dc}V)45AmbP`SzLg$VEO%TRuI7Ucw8baIf?Zx;?b#!pZY>0j+f14Dfzn>e~Pq zX=+uemyFRTPjbUVKMe97C?X}kXx|t=t*>2Vo4=ZI-ex7c{-U&4pZ^VgA}S?!gmW4| zOf;y-AauAw|1hVFDGUGsw_fy-IJSgSCyeAYbch$Qeuvr@(wf<`d(*L#a%}vn8C#lm zxpp$@FgeMXgw94t@pgBt+vn%s!K-E<<)a8sO6(oZeJgp=mdnp4R&DlWK^>`fMe;?n zENL|fX#+V^o-+#`xmC|(l<5C%iQ6Fvdt5g@kh-GsqG(>vGwd2%u*mHm=YIU{q2cIv z&n5b(v~FsSN^Z}{zUvE_)=A27n#g}^lrHeE}ZtVZjk2 zrf2l&n@*HqdSkKUt13Zm$3Nl%5^ef>ALZpf?W?AJv^6W%))d!ik$Dj9&aLA^q0*}2 z@^D=(??rE4`p_LPjb-c_pXPg+haX{GCzOOi=|{$gt;@-4dTYW@f(3FM$PTL+^G<6t zlZ&zhTcQ|WPeoejkC~{^4$|K)-T7LqAbN+&Y*FG7eMfze@t*_1p)hWQSk*m^ijpSvrkd{fEI=gloJ7|zf=%tvlECToZmkI$U^poOUDy=RlREhw_KqHhj*tK^XxN5a*%@K`Mq zKCk}}3*%*|K8QRWCa$g1*>q?+GJmR>?}CDmcTbfOu2h{?l``wwKRWm_X=hh+7QgEBSA9@iOapcEysyGqHml+?jjD_nj+#Y#qtVJMQG4Vi^v z>wH`}H)u)u@gJbZGXp#p()Ny%d_I zRb2zSHk2Ij55Oq=lNq07RQg`*Lw*R#Zo{Lv0~1)gB!X}n@>ke2zJZ63A8=-AiKBXS;)@Ti9p9m0l_hrs3oKb>$c;Ok3h2b=$f8qBHTa}I{`HN# z0C`l855E-CcuFz}POEx3@E;(j)74}A_w1P^#S|~_N=A(hx8E-R4={?xgQt&fq1p`5dVwO;F=JL_Q~IQnu*3ABP!%?0n{EtvyPh7DM@L38k(w_(G22G zhi?UW?)H<-g$@gphJNVtLr4FJ zbKMVK&Qfc>J2p4m}>-1Jnm5D_cxhf`qlpdB!ZJJAs*RhpfEs( zL93sQG!?W>TfmRso$LBlKf8RCE@ln^`ZBwH*3uq?)ZoPOl^4f!gIvyq))3- z=e^sIKa%kxET1*M>dHLW`GdxmD+?rHzilU&Hr1|F53<@=!prNZIrr;L^`YbjanHW> zRdQ|uiUf+^an>rFk9*&6A~sa-KVI)oim@r2#H-?k1hF7eYKbimNhZLB`~)Dk;pS8? zwsQxs&ONl#R0NJKUJ_^0QsHtGwi*7Ecl*SdML841*# zzfEfZ!ZY@c*Fh9#S>*Mw4xgq;YS?`OE5ymq0sQZ^%BC`MoJ zBwVg6pq!LNYA?JWq89vyj@69XXm^6el-{aHcvww;rkG|TF;NL{nh5gVk~%GeO$KK- zrZGQCOe-e}dEKYM%};oZnB=X!C@U>6=Mt3$;SWmae7f?(Iol@=@$a=$OgAmV@rI>1 z2@Uo&woK^a49_?7Ly<{aPM^S52W;eN60~$HU`)WC!lckV2lBV<-J^z&QFekF;J)VR zjpI$~{O?i>TMBsj5DYP-c~R3&iGfdRP~$i~_fWs?V?~)Q{pmWZm$8mF@=28n3CX80 zZW8Lg2m}Cw1CrRvQY*?C(*0IQ4mEz7gaBqVyh!L|_%e&^ATx85mn|?-90Q89_~UyX z;-yy|Fv44vp`EV92x7I-tK1;AcOl*(@;63-Pknq6Jt;jkq{b9FP)%ha5i+`Z8EBpKQI$w{IkevY=W~28sAp-NF3cm^)?+UfP|O?H80mM|)D^M{MsRMt zeJ)2^$}5`BV}Sj}Pig=L>`H$XgNDltJ|5=TX$n!h>&lPM3+=2J+U*|fsD|!z+8RR8e zASFD`7j7wX<9vWQXk;i?Qwp?tk8@_TzH)nk-_m=Crno68m{sT*EhX7v8-Xa!He??- zYFTsYTeEy)l-+H);hMMt zf-uookbPbZd-S$*lujansLV-2@SP2&l0cz+l6+nFtkhPTW@S>yJQw&JZO2T0%cnW* z5WBToDYVDY^;EyArh$oyTY&ZbQ#e5impai5JsZ(FYf7`fv0RA#QTQ%&tj5v}9q+vE z|MWE%%Mg1Q{$%E8CSk*-QR33Ffq8)+DpNeA4yJ5CT+r6NHZLsN2a;!5(k>*W@Ub^n zmkfX5)GR(n2ha)SN zN(WIXWE>GJ_Kqnl8JKdF6%T2+nhAA%&Y#(FDK>p%V$kAjXmYTt;c&eWtnrv+QFBnM zxH#bAZLxMmv69f>dzs%p8hSnywdVoy72e0mOgepU+0WSOb(wSBfmJ65@!j8--`QyC zk7z_EKgciUhUFI(g~yi+-gBW=B!5aT&A|1PY+>m3NgwL*Tb&si56{0Gg!e{v+gp_G zXbQaIeb`F=F(P>6DFs(vIPvQ66Re!Q13r7SYqNc3Lm%0t1V6?>=u4htYNnM}D8<=u z1nQ>=89S|7-&KB8{IapGys`ZsK+S^tt+;r-N(qLhpq*ODF_bfA2fS%%Jy56_zZ&4G zY@BaB@|2_RQ3W+4if0uwn&ItTIZUzgkuH!W@Q>%;ksOr~v7$P`D3bAwGw+gjR>95M zK4NwGp9*ysQb6Y3dDpuo2OhX#pN0`n=Khy0e>OSqQr0g6WLaNnf-m>O2rFffG4y)X zI{6~6<~27m7$AO4Z?@mUT1*Md9eam5zWS67t`V<%@Zo4&8i6m1AVPwW1D_%B{ypjtL0Z(Y>e#6 zh^nwJ(v)<2r>Na($T#GyVrtI~``J8>=vXrl_F}ZuOJPKcrzuA=bgehLCkW2|ZtSxY z0-Jn(zqr{0SNeS0z2cd|O55>>CpS&TW!{Y=x{jmIni9xkn6dck#aitGDqt302jQ!w zz)hX)9{;MGw{VFyjHyE6MsW_2#M;Q7O3!{7pSoRFD;Q3_`dk0t%g0Bb0<}uysT|n4 z=SG@gH#zSlwmXZgFMV1jT`;2sCp_op8@*+9ACB*sQPe-!txSCf;lQg1G*^~{u;`{I zmos%ty5?6Gy(pOGsQdIFKjTfMZIy{G+3?fu81)^%v)g}wn3-gE$>@~Gfun$Ms1kHM z;q^E>WV+p%E`o{{aTx(_b*S;MEvq2yun>%JR((o6Q(@70Y^iIK z*b_gRu}s$w=+gcDiJD4}@x*yDb_;EKA?GP|TD5z^TRJH725kR`o#icRNBbsWh!1eR z2RNl|6DGK{M3`najhIlBF37NeamB=WFDL7h*PKRvJ$(x_;pU5+qo`2Yx%B(S2^@Gh zP=nK2d?0^xbu_+zv%3MLB4VvPAOlcR9R;fM#sfz?XApmG6LPAGJ-8?13sOaM&H^EeF+o8borreu#XMJGL?(3>c;-iQ}y{hl@fL)391nc9J^)HhdGm^XhSuzddjW}@DEH>_nvA$!v&=n$#1-{Y6L zj{N$n$Lrxq_yhjnlP}9ZfP(*lrN*Uu#2t$hZ`L`cfa%Bq2JAKKS;snlL60fmv3_dl z6fOju^y{2{CX0D3`vw$d`M6KH!ts!d7rO*5AE3wgvj#5WA@VyU+@~k7_C);cFz&>1 zzA=ah=FRs4>JE(mcs2PCuuun-k2?CDahWstlQ&Q{AKf=wDz|m=*9!_lZ(F`vX-dv7 zYzGkj!n3A)w*Q8XqHEwlMs&d;&FTm{A&f!3b981Q{zcc?+e0tR>GkZzb!4#VMZtgI&2(y9`YJGzzQQKIm|(ep8kd6i#=QXPs?wb zPD(K`;BmtzHYKdZoumVtYr=`)549fDeFD|PyGh$u;gczQz`^>48+_8dLSteW+(fn$&D()Wc*rck2W;G zy=mS_Q{P2r$6lc2{{aa2&(WpLJS!Y7E8vcph>|*O4adA#P2pd2Zo9{rCaZSNzpC#W zuR|<)4ugFMEI+$FcR|P6q<{NK@a5jQ)D6Ztf-=suAo77d?57+7R~lifvbFpxnR`uR z#A;hmhSLMfUjMr1nx!+H&HvF(J)`csy<{}H|*KQw*pA%>&jEdmDy3nTn9pog# z?I!GWD~vim(eFL&(>21EzGwU5Nw|3DODMqFnN)SJTsiirpk8C_Ikh;H7lEmY?uipx zU1bDIsY@Bevk5kCP3z{<3=(pk{2X{vt_DhGE?rT?zvdi0G$sar$t zP&F{h^+W4qTl`S)oYd@Gh<=}P7RxA>H0gxIO53n=nW_kVCeT^Bu{exDACCmyPXE{l zmS5-cVfm9t6MH7$W(3%(<2CWGKe|W^l)gU&|-E zKjjA|av-vU6=eAA;`aUS!x)fHH6+citK7E1cC1Qp}8ylVx#ks)q^WH`k%|E>uj%~)v~wKq7cc!UH8J{ z7kBwIM^@>J9uzW#CIo4eM=7?bgYX1BulP+X$rt|sx=L8mDzElsZn!#$Zw$%QUm+;! zYSRlxTrj)*x+u-oXKLS{=vwlZR(`qEAZU{9lJwDM?$EPKBjtWuiHq{R&gX7BiEU1bqJSrach3P%) zyvC`?cia2M{jCl^O{A!g;jvMb&%#||8yaWe#>l?P&*l8E z(m=@8h>a4?>eBVAiO1fL6WJn1T(%mr9fq;ze8r|L9FhD8MQPMF+NjC!0-6h=(e&4J>Dybxuy2AqZXfinCGjsb zyn!JAk(WZ^4+%jY53Ywxe{{WGOt%+gn1-flD31rZ7L;8-Z0mr`qlRnzqJz?N-89#Z zzeeBj4%QW1yq#oyDoSu&4z7FUyjHW^8<*nos_5ViUv2U~=q{5Ow6-r9c-kZh3+?GL z{mAHKVoH3UxL2SSgauKNbV+{j!@$=#BWaTGkDNR!ep zlE>(G@3W|Voj$qDgc>?=jy8+&lG8A!jruo4L=jpoTg9u}z3B}|6Jf-gJ73MbV&!a{ zYKB!{zZY=P$?>jT-ibBC@l^&?d?@yht2QXOQdJ>zP z^mO_vD^2~nz`1t3bG(K(IEO|c<|+LT>Or%-XwKgfDWzX+S`Hf87247lX6Qy@e7HIy ze-Yg6C%NjbM)dSRY|y>!^Y)N2m^Q*ngkk&rEyIPrROe0UXGy#rMlPOP%IH>nM~LEVLlg>{3U zwY}xB*h1uDgZO-yX|+cia>=Sxba94+ae+j{6R62g=isc@o=TSdxl~AETTt-3Dz#Ml zPtNnxWg{H~diVqe*5Z-wE*rSeR;QxExeQJEm*ih^ISqROk#6FMa-*(!!oj8DwE{PU zkXJ3wTJFxj?#(@Evl=jkbzNh2lk`Pfe^1p&)Us?5NpHzyWThY&SyjLR0VN%fGQ|>P zE<>k_X7jD4Ng8JtA5soAm=ricby+0e@jx0xjRES82}bP&40`)9C@IY3+(LXZ$MT|P zx<#9CmL*H+Hv$32{3T~@W-JAsRja?F8H$F2kSX_Z^{-t}>O@mbG56>XGHr-JnSo=H z4Yc=s2jFD-c}mO*c9z*R@Mh)#e!O$RwRYhar`g!9W7TutE*=VIB&f%!Iw$O}QO53c zuyV5`-CT!f0A4pxz1e5;)R3I;>~|H-D00KQ%=@3<8E1e$lUZz$ppCQR1Ulepwqa!_ z6HG?mw7OY*DX|;^6Zw*!2QqX;QT@5DS%m+-d|ml$=PO|#rw-&SvQS@e@vBeaO7$71 zd-mTdFkbobLvYZYw7&|6o>?$6k>z8H*5e6RC$XI%syvoX|0huFY{-82Ja@_u4*%sG zwQ+M?n#|*LvcE?U`ZUk3>v*uJk%}qvV*E6!u9?O4LxbLOGQFB6(s0BcFl05bZ2eAw z&8)XEoOTZ`=#tfwdYMrqqrapF@manUaJNkJgOW^*OWo(2J?HLL2Kbzgr1a{Ka^3S8 z+lza>RDmQ*UQ8~j3#JZdZpDE}QinbQyem0Ng}kXhx6%?f6&25JFG9Yh7iQ9cz)?$@ zIG6_pAsE{61~VKVS+hLD$=Z5bV!s4sJ|@5Izt<1^VgH$H$-sb#uyKU#p%PxoHHQwE zeN1X8bM0fhn`|6Gc3{7!6ndsEMdG`s7vGqu?;pSSo54rgi^O&~I7Ij>{51N*%b>1L z@5l0MZi1w4%OLF8I)_x_TNOqeXi1~ETovzyY542@>))Z+iTj`h$({g#>-L>vONqNx z6oc8BK#bR$D^H{x33g*%M#vEr-qPfm)hg%MnB9SUJ#*|!j4Lml2{TmcWt!v+faPeO z&taqR>s7?$qPvdV+)ICT`xO<_nnOr{8QzQP`p?z;(Ai%fU(kGUA!Rzi;mvMUOkf=& z;O8R5O_LSh2@3^c0Nsy|#Wj&1QoOF7-21>fMK+((yG(YH(M3D@JI=FioJZANUnvGt z1%GMxUivpx*3h2!RCK?0KRvhLCnbV<3rdZuNSK&EN-d&W7N2BQy zr6egzsp1==tIo0rZy@9hKJ;x*WvFkXu=`An%%}49_VBWBzdcofyu0w)v!&YdTMZc5 zW)q^ZcOrm9U&G2aSqx=={I-GjQ%^VP2v z3Re_$S(*0s(b8Pax2d^&JGDhwz*2zC+pQc*TTn6ZrU zsvP5&o?sLWj6Ei~C6f%!-YW+^<=O+o0A!D4?5>>pA~;%7>@@4-b+Dj&`iC@nOtgAJOjs_{#nTot1wD^f~Y4tJ8D zX}?Tn3gmoWxFfhou@0t)$bh-d3@YpL>+k-dC_xX80l%7raRDgCuS3)#13&qt=_pqI ztV5kQc7RVEFM{(Fmfsy4WR_>ZaG@fzhq!j#n>>*eBz0j*hrm6?^#Vhkg3HLwgSQQf zpGlRzM$JnP8Q}cI{|f@4y@qXd6qXs?7?!{TQ^X@XW--l{HhTs<9+1`yxf=wF=&A zcYeet+#_({kE7d|fAiI^?o)k}U%ERY_kLXQkqi;NE1T4XHDv%v(0zc9&kDMYF=h7e z_QyYrlv@=qSfSaeCc-|z*fkr-K21{KUQ+SphK`{y>1()oU1;qK$o?s z5BrXZ;?D~Ey6H_x>&`s>Xq3V>BkZq_yq9TD7#T7r=wX6x%N_MUuiagH?`vlff=Q*- zQGaC7MYX_4if<`!k%X%=NrS?*nzt7bqC;{!-2t!<8L@VBO3^eLx*Ie*_P{fVgTv(> z{%TTvyY3Mw8(eK}<}s*Asfym`ZpvkrHhTEevX|hBQ^Q$i4Ax-!S6@)lmJ@pc*R&P8 zYN?E}oa(@5aE$W0X&51--bK4aWfEbBOS&Vg!o(QbNut7d_6D~!>K9mLn;u)Di}!A` zGH-T*ZHSaSqtZ*sGQS6clt#>s4_Dzot=N22Zvs`W%xy|pO^Br$cdEc9zD!M zM~F8CV3j4dm6TQnkL|xNFq?AdY_2{c+1R8 z-xlJ2}f(1;V*pnC!usFQCxvIu0GNi~YB|`hB#o8Xo6PzQURvL4{s>LYtnx54i z6Nt}o9fsRXm^Gc0hooz~h zZF~n*r}IC6d2w*GyJ1JgeB~Hj3a^~?!GhQMBPUiagog!b@l=y2lFS!%j{OSe2S6r0|Vx z9Jtte#D~hYG_iR5!$TFQuK50j)+BXwgI8Nbx6u8fz1K-40{)vv?mvJ%sSc2;?K`zG zWp(1GcSXAXzR0h0p4U~mOO>rtSzMnh1*}Hn)x)~%Ogs2I28JYyxRm1Q;LRK!`x^Xs zYq(#?tq0ASiNzE1?(+W7zZ55P{4~|QQE#Vyb)H%^8h6+RN!=YxFL{!Bi!o}r@wXD2 zgLA?}w}c5~euOAPH~dUIIPI!tQz`04Cm>7)|BjbgU#G;xn5e2}n(>gY(XB3>jcNru zJ8eG_FM7TDI-O`phwWzYwrc*MuBQ}o-ZjJ}IZII!zQE>Mg{a`%IkdwP<}@*C|5>hp zib>0( zQi#2*@=-AH-57J2XL?Dp*&H|ICn_GWq0e=oRTN=l)-<}LA9!6p2ujO>K!{<5+*nN1 zgf(|6t{J4Bo{3RJ6%!9x>f_M&2%kDbrXoo&{N9b4&OVZ<5#owyy83Kqu8J#HYLYu; zMZ~19H4*6y$wG)C$vC0@b=^ zptksLHzox`bW4Gw{Xl8>c3S5=CcQ^ef!i(f$T?$vw-ND%>IcKL>4Xh+5yio*7ik1o1KP{K0g zSrRG>(dp1tUbSOWSGj0P@7f9TySl!rBrZ7;a?f=U%vA;}j8p^U|D`QXZf+F038_{x zsYt}UAduM%q(@kvmVH*wnuHY!%~7-ClVQy22kz{&+%W%MIuhI@wkiUKVbaAPIPBL# zxRK)d*;MF=Q_pV<0)L{qWtT9^v1~C*O(|E8CGTCcJevpI)P|C|_E1_2 zElqH&Di>Cnv5ps++{by9i+L*?yP6CpMr-6qmM!4uVeOm6B&BvC-1d2B$CXETHToX%Z#@5vZ%mW#{9l<6wX z$(wSIG8rOc36E=J+$P#2LU6Q6l|@e5Y%WYq7_~Q)BFrZYkoBo8a80ax02&`>ihTTT z&!y?n3VCOMl0$Q0&62t_8Co_Tj#s9Xq`#OU+i``kYghIdkQLz-^aqDa=&hIQ{*@rC z^o+`%S&Y`V4Xr?JuX~#i*7YzGr&*mP3(c1Az$K($k&4a$mtczc`e0mD*uh6#ZtIOyMC745j+oR^T^KhfoFrA6nML@yupW5B~e7nCF zvp|5#I<6Gc$IXgQ9Ctn$3k3(hYwE)bCS#hYY1Pe3t)D&jBlip!*gFZbvHZs-sT~}~ z{{dL{j*U%H?{%H;O-%AGJ&JP{Dmxlas`mdHHMcngN;KY#T9lf7etVdwyZLhJ!7di0 z^{Iao3GB8lWMRsD734@omvKlaCHRWdFf$D&e}eybRHWPnH2ACc?B*orocljO)Jn_T zgOU&H@42rKl-#vxiflf<2qCej*K8f@C~L)Gd6z;>bm-?a>i@UdkO^|4RYizI>8>M& z+`g>+idr@y%t>`$Oq>dLYCPnW$5-$7Q2jK^wfoAo+Qp!$lf8i8r&zoj^Jrx+lYB#w z$9sG{G{*j?So+$=^P9l^veJ zxCEP~XXzOkuDD+C-^!JqgsoW(@gEM3<@iwsA6ectyI%FS!&NS#gVBgJs0^OeR_z-T_+zm zRJla{j0_uOCInUC(y=CBjCXU3LdlthSc;mqt;y(h6fzeQ&;d*}VI^OJzDjGB_Nbn*J^*u8bMl{0eu~ygXvSR7NcI#Uf;dXnHUZWhhH@<@4VIw>NIW*M;L)_7&7OjO3X@7gtq7llWW zuQ{wFKu|89UFPLBxgia^k!Mi=?X$TwQi;~7$k1v!t@IOdu&>#v-DGZD#{V%Nh$h9w z;ch7=TSGPdE!hzn&G~7dB)`zz?+7L)6=yY(=J_uMc(_R`w|oG=b9&*o%B>*pH>QE% zNZvN0`ib+$mD6q43-M3r#G(Z!JWruCTyI`)El&}YV zdegLZFTP1F91zt$n_X{75^4%&tl@QaHY;bgl++VRy>U_vb#&enrNG#6fcX$(JR_AZ z7CAabjO(8=?6P(FG8n1JT6{rAfh%KySVp++HgcCQp$n?@Dq1cLC>7+#ra38jWIqW&G*02qA^*0AZ4xjN2*hr0Bxlnmq$xDIgt=`*Hb6It3tI^M;% zFQGNeT4^$`Io-TopV;bSy8(DfCG+!28VZhos%QH3`?UaAv4vUFB=c9->gVpjb5cKD zlvrtn|DE1hO!Cf`Ti!cG7TO)qh-mQkyP}HO$+LwN2^jr=0pYH3Cryi`KYK_@A4)V} zDebFlvB$)r$Gy`se0Gu#^28$@$#+lIhNScqSsCa{ ziFEgP%2K?l>Xm%}KNVWseraLhtv`494Y@?~>+5d$B3(yHW=TQKWmebhRH}v81vP_P zCD@rWN7&A-r*A5FAv+6Z!murQ?25C)kxWD^J=d?+B%xX?AQltjC8G?f!LDsDLpR^5 zSs$15g9de|`FvzMJS^lnlm2h3$?C?>=~Wheon2NZT(coe0ieB88Mei|C)xxyz%Hu zI?JGIBeO!%RC4=ej2)ca9;7)MA0K^E;CHaU$IZ?C8ZzE~F}+nSx*50ZIzR==M3lnV zLBKSXw;fVQBr+SeGs_fXWN5r*+volp5w0nRW{vId@+|wzc^PHnV<=LloipW%e z(}bKeH}naY6Y#ntU(yxVSkg5pRJ>>OUE|^V59_LlqLY(w;YHLg(vDpVlN2I)X0<+| zVC?Z}yq`MZRYoPo#w%$MYGmBl(dc7`K{0<)j32&CuWD-X?@&QU8^#sx6hkXFo!?8I zEshj2(=WqSmn!X`qA>9g09g^+Sw<2lb{_2!K2yl(6x?=$u{>MD@fO%jV&bHxr!!TV zBAe!;HOPXmY1LR^W~;@nwH~hL-z&IdN4K_WDe$1YBZTN*oRn}ddLMt7^APL;+GYC$AHh49pG$;q@WjfPEd zgYaS+yc7!<_O?!Og16blTveIyyDFOxGUa(yG`HBaLuetW4 zh#hqvQu{ESlo^JfUP*3QvsLcNrtGUok>hS6n(Nz8mr*8@)=gd`AvpKN9Fd!%O=3!L zAqqP_T)MrfczIr>(saD5;feu9JCUk_Vn0bX9~&=>=@D%PerWvFhO2$@d7UQ2#g5$7 z^-B8UsuPgxxrp1#@8bm_H zi=RS<`qYr62zRxBVS5~Bc^=f?9UX0-vEU7DI+cwLH+c=6>>nlV;YtzREaGPu*(|r) zS@{9?1Xg=ga$M*nd5-~{UWy|Oa;x4K_e26cI`;UZ1L}Wox0N9AQ(uF|(uq#Fm$ya> z2~k53efM7Xlg!c6qji(L`j-aOoYO3}#ya_p%iZ0&sri&$v>3EcKBq?f0sMNqJ~Dqm zH|-7$mshhwp~fA*M{@|!8o9=!<`SK4aG8LRLRiY`GRFEh5(0{_^jiKy&G-b+CD+yG zTz;HDFWtLLv9O(yn(h&MZyfa~X0LK-xNO=;c@NjuKwK@=#(!L`g=#IElm#DtY8yEO zay7OHX`j46wkb?K$SKaB7$kpOEvCe|IaK(`pSG(EhJaRl?d%XN*++s$vG2>8&cAxz zLZAkYS?92p!6|QvjmaSF+ed95%LXH0XHGi^MzugQ5Gx|Mcj9^^m148F9JbtET0P|d zc_5a^TfHTqH2rD*=#m6?vt6@tc8QToOF0{m{+diyhKac)0qqddUSx0#Tjj#t#BlJi zK07WN^@dVbLEjwOq&-SmXLs4lopB3}WN08Ihdk?7-l#XMyoolF_m5yF6ucZQBC4in zXOxusU=p%|R)nCNQcdiieb{_z)y-IHddSOTf9qMdJeXRF0=jh{wImMe#=oIQ_JSAd zG~A44PaPvN>Uz?4dt|atKWfK-#AOS=0V)k+croSE`QE&>u77~p1W25WpP$w~YPYSW zNC^Zd921rxN_N+FOhyV)rQS>1dvC9eFr+*F2gqn2*}XS$e9FQY-#>ih6!}XXSE!AS zgXx5W$zah`#A)*AzV+ksKfpldxexO{&R|rbhvETv$gm}JxrSoWXPpx}?flbt8hM(C zyytvc^9^=*I%*F7liyMIAHd|QXC6z6Ay9@B)Wyz+9;eYOn`WBWxqiF#XA%}O(WZVd z@MO9y{+_dEcc&I!Xlspd4`R%i$T4i@lB6 z?Px?PHQmnbg~}RbOh}SD3qhtJBTb@#207{mUurZ{t<&w)=svn=PdbmnJ7fMP{y2@h zpx1IKi_${ldpRtXC)cZVh_-ijo4R+D$_ub`p96uNVr~e@XSIa@I&Kd zrRC7eYXnbpOCEVpP7w3%K*&rmYKpWpOR#D-KJAB+iM9}~9wik`(v^HUJZ@s3-?el8 zY%MjddMg6R5c~u~)LT&-DhfGGQpj?Xx-69)!N0G}r8CFkLxJ>)o6>IsmCk~b%=K*y z`A%qUJPvh5k_LFp?htjl0k0b0)%{~nSiDPo25(W%yAyrb_DO`dM3ge^fqwz-OHyZ2 zd}+lm`^7FKc!-+^Xv!Ld^;O-hV3?Kq)}m;a##<6bfXEm3&n~=OS2XlgcZG!C4dImR zmVsY|HGd=W{bt3AA` z%t*MZ3gMPfU|t4*lCjp1Sw6%lr!pTYeLEgMxnT|-Kp0t@#ey-&=;ahLd%DRr?=6$Q zr)1^HfHE7l%4}VMA!y9lZ_1MYOlOwlL=2$~WRCD!uun_d5tjTOUbY)&QSREXmmb2% zonbc>9MNHS(qKW&pow1>9PWdFmdqlabv_4wKM!J($q4AmduvERKgLY=m&loE3X!xu zFtc+58+0~faSs7$pV@#O@H0)C_%zuWTo-sZ3mcZ$$#)gLJo#8gwLmLpN>MP*_Bj0C zeL=^)4uX6tpxx-tR?Nk`<$%>{LyFeo5FM&k3J?(nT6EC<1Vd}fnfT%V0gUgaTrnY5$_(oG#@vsB#5myEvvDg_S%L+J)^VF)kM2@) z#d4^?Mczuh`0Hir>$=DSE@Ay~kc=`e;3))1u!SMuF{Sm3{j+M&OVCtFo!3RF_s2Bp z^$Tl4U1c6YY-A}ZL;c>;X<3T^K}6e^<3g@XrN0Yi4H4JEet5cYv>hOpzv)favDR(> zwqahVi(Q5%m#wud4by7YD=mY}by+1vzZ$$&dOm+0`DdZY1G6sZRmrhapY~7ie*k5D zX_ty``DiBq*3_=9&9?-;Q2Dk#D+Pq$E8DVOf|8Ly7Uv=xOfHo{@$D^`d#Ye#YSHY~ z5kM49vN))mowuv}g@3aO5nx>-}et?N{JtyUKhBId|c#b{$JWW-%OH&bXwAkLb}ugh!I%gtlqTTr-kR}`J)7wuQ`%{P7L&-e|~l~{%OY*I^AAzgZZ%SzLt>2 z8<#WEAVO#CZsm|rdr^&DM*C0h2kVG?qn59RFC~Yz&vE(^?S1n<7s($|2JLd8c5{^+ zn1it)*(5@%USeLrm__(IG-mnX=DhLOLGv#-d%t;W@Zb(=5W9vBxQRWlJPWg+9z%&x z5mZ3XEMH{6@yy@N;Z6%zYlrnO4|dJ?rHh2mP_^YXW=4J|mL?muxI%QPG@ zHyQG=)$cfaI}(Y?#gkIy|FRef$CjR+i5};&yLG?$3}TXo@u)l`Sln7rlJBv=EGhQr z;WoLqub=OquU|BjU#Ew;pfPB=1QV@Y%ylb$lu$|_pidoqE;xU@Edy^Wvua$nbxvEJ z4v4GazMg4`%AWup3GIrAyGx=tZpg0>XXk;f5N?kTjo_!6gfIlV1K|e*7C_*Rm9#aypu%(6elK+AZ6UaXOr3r~gDB}UIzNW8YLj}yf~ zCA9MR4}$$&NxXosfIkXYvRcO8va{?&Mq03 zZx`Z8t}Xu8UbUM05%*d9hRpz92gd1ePF~6)1I%NQ#Yc+o#u z|5#5MRb@oT~Q5WYset`Xgw6Rrt}6VWY^)7AMDRr{(cK zUuhfIe8hx#NS5`J^OdRK}% zr4hc_P-_6bSb#RBkv%V10&^tkM>7R4v;NjS?T$dj+z{=XHE=#kXUENDaAt!+ zy_JF{|S`VXmiJzioe{cp#% z8);LVhd1Tt&5Vc|6ULZaW0*@Q@H>?tkTzO1XMJeAb3S&voS!Rce`(0IW<4pkXvV;m z-yxO!rCgzv8{1RWfo6Ax9QiLP>rvwTy4?At>e=={7UfLsTh3#Zq)QuotDD!V6)oXo zR7BIs#$N1#d(e%fIl{vm~bN(a?`s|{?#|$jj_Tn@Y!zM>%drQwI z_)Zr2eD-Zw!oNAQfZ=PFt3YwtoI#jIpX_^MPw2ybsS^ml(TOh(v$dj3lj)la$%uoh zdJ(&vEef5L$OL_xkytGRacua|4ey=xkNoigUc{n+rv_Ch<7)IC)}cs)y(ngj!~(+C zBhmRG+IQ+FQ47cY>F1TNT#)3le??xFTQeCnw7wkIo&V6fn6?}5)DHDJ#Lydwpj>4w za%U>kIvz@A;)Kz%)R@?EeiYUFxZL1XI_1~$yFtija_&&_2SRugl;gBx(WKPeWiW{c zN{Rhw08XPw1LI)QquM01CeSJR0v+8LF;sgsr0QD`^FiabFFNv?N&qaeTST6R^IZ57Dtw_wfDePjtwHU5n zKE5EZR3UpwJJXS=iN!#g2l>{f(y=)%@8i~<$81`AYP{g|(xQL*ceA-U78R*W(k5e9X?}&OagPS%kdh7XFtY({Y0_}N930ye(ujmVxehc>DPC<1Z7`kR;_J7UH4a8;Q5_Eqh{-`yFi-5-K{Hlh;SE>@@rM z&bh8j9u=*%&vTE10fqCUrjfYagpM}dnZ-+$lxH?8bXF}nZmUOsk9k{1T2z+qslRy9 z_{E)6b}rGpw2Frf(?E_F-yDcEHqXUue8J6eOys6by3Hr?9jv^&vzT3dg<$9|fZTbm zjFWujv^`x)Y<2ESV#t9`zbx)P3e+pS*3IuQ@*oh)5&hz)_-RT_;Ki}O6Q|3%bdY@C zu-!ZMUbPyJSpGdtKscE&AYD8Uc~zQX;Vzbh31yx-Epw`!kbD{7?1R5@N-AbmBP!NY zeC^}e6#YIkuf;Cg&p~Vd&Jj|@_q#O3bkP))>^e1hLS`|P?)2$~@TRkP#w`{4^}zBE zL;;aw;daM=m%Q>SY5A{M)UN)mHC^LSQ@ux20j#RxX2*drEv}U6m*I2ssEl(&>6`=H zQz6#HDy(mK9rds9fefpzy&BHu)}uR(&+dGeOh{hx_ZnaOBs3MF$mdS<_D)}?TD%}s zj~<2GhU`fFkDo(JHkb9S#mmFJ7v@%^RT^udA%_)OISMWQ7SR8{kj2BKEnBgmT@xW| zy5S+Dwr5a5d{s5o_90##^ryId z@9+JKak1WU0iq=}do{K5o$KSzU)CYL8ksg5Ih*_+r8W+2fXtSTWi`eX%o>oK8fW>K zmx8@aCP?GeR*NmLF;LxA(}YD8GSbqc%JbEzc{X9^GO!vUALv}=nN$~B-0&|U{^zyC z?Z^JfmmqwvQLdu-UxwSu22&4ii+@yI-g$W_tGCY(Hbf|f(?x*+(v=-gLP$;vXJe`W z10V2;(e|W2Q_#v){d-mWQTI3EgyM}m`r3{uKO{H!796y1h8hPs)1A`2Lb)v=(MC;! zhQ4Tda!%aY2-`~k3XjNS-G%F&0<{CxzNCdpkAH;mpSl_~{D2P6xzXF<(3XZtYq>Xa z3ue;{yq`zaLeMiA!}&xWYTk&s-`tcY#x*aG`>Y4%j<0`$){&CF|F}H4-vc#J{fTj% z6vs{zL7X?E>7dh2Ih7u-p zoVFvEUlHY`^iZo(g&1AeGAV=Tq&pVYxf)=(*!;XKER3b5S6jay@jblyb`8#KBmKEW| zD7yGm_vp2j?hx=X_bUUBw%G=3CL{%nko9j?&v#IZSyYJpKXAXue}J8x)2>JRFl&Cn zv!?Tmkz~@a&0)$&QkWRin5vy@EE<9g29G4;jr;nc&cVz>C@F;MAmLc|WO#-yB zT*c-Y%P|GE7m&Xfi&?wkCE(4K!3&*S6#B1peUBMoHT(C@*WDh$f>dnw;7gsl*VtpH z0pf;2N6DdsOmN zdIlKUzOA6QzRUj`q`ZFuIT*7Y&lJ|wbPvx=usZ6!Ml2wD>DtT>c1H;XM_bmM_o;4> zPC(&F99hN`kC|bTVObP6US+;(%~T1R5b*r$pwAZi=G2{#CnVacJrjbRI;{k9`7VM1 z*h?;U*9*Fut`tIF5w0XQ%xKJr8U}Bv7uFOHx~5S?zh+@)H%Ol*Q^v-e?V-D~J8-ZR za>CG2Qmr-6{>w&)#bSFT8@^t`JZ(Xre;uRo*CBFGf5}5fS8ft(qm@&LI@8z@WH1x` zS~e^PEE`ro{XN%J`)Y18`a7Lt{crsewfa@MuP$xr_Bm~1&-Fr%$f(7ZzP@h5fEAu0 zZF}>f>wgJcT9eyO=v4*;@9uRe;UMI?qcO@raY$NP4;4r2ouuO&D@vu*J5BCahOK!WaDeZhx~vZ|CTMDqfdkrT4N z@TZ|39rz14Jm$bNb2_ER?p}s=U#iOGlh*!!fZkvw(dx`gNykBvE&Ktu8#T9$wxL+Q zoT#PlQAEL%iBXSpV^5i!t}Qo{oora43}y;^W{M>;42+^u1L|J@NHp!jL|D^n%7qBy z6Y3@Xveh=bv~5AgI!ZaXTSZVoRHfTvgH!zKf`^EZV*8D;XD_>&$`Bhju#f||jbP$n zB%mMB|Hz0%{{$sA@sXN=xFRnFpxviD*V7Xe%a!31wbFcZhn&)S4n7PeD8Zp6y5(R zj{Z^|7Fi_xYS*$$E#MVFKV5xFc`(gx%a zoFW_&(&-SZ5`9s3&nf0vYj=S!`{Ua}^$dF=s(HXpUN9S!vhZuniJUJZSW*1FgPNKZGd_V%#T6y)YL>~+SGybHI2F8;9K5g8ZYnY{pBHN|NP4Da+Bl8u1 zd>|A1x7d&qwg&T!%S|J!j3l_Fg?%u+@Exp3ZCS{zTXCX7u&`*cbWk);;uvIms>9I2 zi2U4BT=xqM2A(I(dX2xeeG^AQZJ~G=VU1QG?btyy78TXeG&CRI67T$bg1sr6kk>T*%%zq6U+#g#1P zeg1mc-!NND3E>yMiW` zlIJjS6alv}5mf-FCab`X< z3+l=k(Un22WalP^F4(T7wiYymp)#5vDD5d&#V6djD(6+ zX7XJ~4|Z;kdfscERpCw)QCz=&$L8fv_8(w+KG8<7DLcypc1=6PL&YCLe27e1K*bh_ z?tA@)=}fCCdHpbw4_VR+o(UPoAIquBp@)95?k~u&pWk91KS1X$Di1ehtbR|0k`HzQ z9;Mh&zv#RqV6s0tvMeE1viBD%RV82)uXw=OAeLD;Mdk23HfPbHku)DmuW+#RLY1_t zJdsY-UO0a_f+vga0vvW23CX^@3(l9akvay>{gb>7v~6d{O{W7fv8x`TsL}wm-ZT|) zQ~@j>P&xeMh*Wh%LV~@DFA+=eg+gy9oEBdkXW>_c&MXG&FPR!fHO%lOwp$*Hxis~~ z3oE43tHeB(2Jk$-_YH!Sf}6h3dq5Z5|0N6T8E3KXmV-D7Sc=gz*^3O19qYv8p z`Zl}+<>d?b;4FE3$Juf!nY(Nh+W20}K9RPy?%m|Jy_mSa)!0Q*+i1~ahJIaj;}Yfl zsD$L4KUdYhZP0>yoq~M-+y2Hi6r_!`l_N0;1qQ>+XTG*fY+cH`I%v0(7$P;wahw!g z(c*GhcDw0e=XByv^H@!9kqx|MhTu1TPT_WEM7Y^o&|<-_ZKB>sGejkmK6uODrTqEF za=X^6nCq;hzj>Sv9i94WJ(i;jZ;)n{cWw4c0 zrgV*rq~yokwGJ{J-4!mDtZPgx3rmyaQ%c^%yiA*iE90X%mEE++Si+3o?PE_k@Jp%rNdLmM)Eg1-YtW%&SBm8p2c!? z@DB@~vrBk~1~PdvTEDtpenJ=8npJyVLD}@gZg`lN&TnV;yvP0881&nj9!h$2H}Axj z0-eTo4lA)s5I5KMuX3`EjqcZ)zq*5lk;_L%W5^+IwFSqj1N={4ckne=pK&go3oYw&8o$s z)i3TbYFB&ND$?U4wM2EW- zMmf;0o-^_N6t8CXU9Q_RXUH1z+Mjk#L0zq_+ydQJ?P3lY)d>FR`X}ALbeDGs=sy&) z>oanns}$*Eoro|Hps&s&*MMy9b1~a0;4s0!q=lc^`jbx*v-}8LVW)lYCaKS)Gl~AK!3njZPbo_l)<;KEfNu29BPWRPCYj z3q}o7U!C^9SNzuZ>Q}7DYe4m~qy!Jfoulbjimwq#1}v)eM(c`JgLO{)YdZ_v>{`LE zjF#$Go}@sWf9Te<+AAdR5DLA+zAl_W`f1Nc<5a`SZ4&5B2fLk2nl5vpi zXTJry?F!Zrs|5dZqqeVPOXsRBr+MKYKMS;nOfK!zEndG2S6r!F%pCV^_&X@(@7D54 zVs@OvsC^U8F)m2^2+Tsa<~y=t=Wc8$J`RAG9%<|zHU+WYMEB+|o>;U-d04|z5iL%Z zpG7+~T#NX?l?DIf39>{j`XQDDB*W$D}*IwqIoeAs;3eCrSH(U+m-m` z^{9Y-t??f~SL{*#-{O>~&2=NE;2o)I6&qH~P)czqr>hk_bF%?nNdq<{Qg>&XV$8174`!(HHlZfSfe(x(A8?=5znSDQuc%2YxwOjUEx zB1>6t)$~u=qgV030AX3(%-J*w8I3@DemB7+-N8eV#ZULVC*GR%s2GTYoJPq=Mq~yH zAzM=v#%C=h8G37Jn$7`33Df<@JhkS(hrV$Ofy#{|iPi&kolYA{ zj0>IcqBW9I8s#*ly2)h0bj%du3XIn=x4RXHUz#mNBJA0-^X_zyK#xFJ7JM+E6tM?@ zGyTS@~HGr{sZ9f zX(P{Xjn)6sKtEe(#zN76Z!NRU*6T2OMxNiRuoJG6C`tUBJ9g@nYRQWF5XL)wiR zZGyoVxLBfoN5In#$P-GF^3gD81~7EGQR|Fcl2zMfo=e-7jeDk_jd$$AUVuWYL(Ler zYdECr=~HV)WkkzC#qrc8mt!DPm{AY4kP^>g>6XKXydaFTWDGvDe54y`2 zldyM8+~tKt!cw2NH;fq5$5-H2k6YRs^%b=FBaBx|KZSKIM}2bi^aOoARW>p%og&PK zt4?%bhKqHR0dFDN(cppqBkQccn*8JbKN_ZVDAFGq912KEg9xJsQUe6(7)X~0($e7Q zk{I2wQPSNKqq|#D+I#o+r|&uUAF%7}T-SD;_bZ-{M)#`x@IS}+=B{P~usm$s75+d-aD$Au-QeOL(9e{aF_G&sBi1Jh^*)~8rLxpk$_5w- zvsD6rh5~I-nkyPE^7guvXIWy;%erPi68?SItgVD#UTl-tLcOXV)Z z$Eozk|gKrY4V(VH-SQbg#{j!(?vX+fZt?TFS_YMcKOw zTbJ$2ATbqn$gAJn%0rx6>aTfJ5vNZkRClI^yC#fkGpY{FrMK9Of2FuQlzRTRLrmku z@8qVtpQcvRLXd5XV1M-%s23N#-t+`x(g1=p)DLhDas7-}&xh|a59s9WBBYgS4w(`q z?UW4Xwfeyf%7aZ<{Z9-IiF$e0bhlI7lu>yXb}Sm5+K8zp65yhl-IyF27*3oAqQKSw zpg+t1i7c{mI9;kUW-Xzzfv{I#$-h(QXGj#^Ao*F8P!;Uv9U%)*=6P^HGoi2rzd_0w z?N8A)sr&-5B+vGjiAO>x}C$%{ChK-r1Lx9#jCfZBH+&8Pf@nNJ8k8VzxssP43JN; z6K}E-+0Az_oBmW9lCX;lJf_r6@fc$0S%6M{qDw}8UMm#_yUQH3(#CoKb@(lXQ_olZ z+aua$WUa_)kwuKQsvt(C*e)rnN`C>F&3pku+*0@?a#@d z`?TFI(-P_>CT+a2x97Hl%8Frw>gy2V?+^w}FroNU9u2xa_>ts;=XTkitkNd^>DbDl zr0cB3`O!zSKqZ6z=R`^uoB^(*l5HfiLRnHwUtTPKk+HRXku>KU?V#$d5>+&s6d9?_ z*YaC$k6tcd=zRv`&4!Sv5Y6bvH|RZ)`oHk!e?^>}kBA$7Y6dPgvV_+Jh$^c@!zD<3 zl$b-vM?RWnlS#4=2GNz>eMa5T$=_h5)}mPhXMRItUpVar3_r6i-bIZ#5hw`EMeXaH7H<}o1RR>Db;=^wS{Om=CK6!E2HO_;i=+T-l$ibQL9Ol>*d3xDG9SO z0tjBBW=wv>Mc1t`(1Tv>(~+L9d85yywFr)~W9ZUy*5v$*f{FP@x1*(`xi3ALbj9qi z6cZMz5nb@ESout#ih6H4P$9gA4*Ckj5nPN;_vr4;t@>5Ms-9Y4@XK&%N(&Sr#|J4> z8#VLvLSHsqSp62e^NO2#C&m)YA(zhMogY|tZ4)4itHd1mpM@v+vLEmH4)iRlE$mrz z(UI5e^3ppdv&)Lvx}}*O#}3=}Nj0bFi=V9OE5b`Zq(D=ZMkeEn407p8KV02fH|;Nm zsQ#4ueL+8q`Ujw$pX@=`BSo7njInB={#1JpsdJ?SiFB$q46kUBIo*$Sp^G-!LR7O_ zxw!{!3RdZ0+D~&1Rt&jRqS0F-r$K+TA3GR@F>V#bpnrgw?;8KNRS-|8n>#(YVKq_r zGc@6o+?VsWy}95(#nb?+l-be5TYy`q;sWu*jera;ap-LXLKhmzDX%(nJ8v=zJwaxo zew19|M&sB|ll1vR7V7M04AgCl#J^o9SoQc^(u@{47%fE$Xp^YP58BC(z#B@0D?>LX zXV0&u!2@bMp`+=wMWoq>%uH>?@==}-jVF8hlpEjcQ0@y=_<~MT!5G+(!}aA=d8Eyd zT@1WB8KQ}R&bf=x!zG@SXGzk`Pvqa_@6eX}U7v*)pUauZUF>qCx2&jq@T2%r^9akw%MOx$Td6G-j&wY1)Z;T z!(%V+aLgJRiO2j(C;e$FibvsQwh~WIf|*k9V^i=kL5Giq!wie_##pV*?!_K?ZVoZ~ zYItSOgPv*W&$X<$-5&EF&FG61m8B!o8s|=&p=!Mg#4|sD8}wky`mV$s`S}0dHnm-c z9mh;xYyJ67*AX*ft1;Mg=kvzsSL|GLdUb!J-IFOvu@cBtM&my1N2hp?R%J(CJ)$TB zgb^FKVWl2=$U@o_j`}_>{GlxkbKZtojW`Og*{EUpEJ<1;+`134k{6aFfm_ba`=zV9 zpOze=fn&f8_qe%)bEjd)C7|k9wIEy7N(#mMH(Asuk#3F!CyzC+l2gzsL2tpmS1g(` zJGM}W4VI2ZkGw+On#~r+z|zD=oF^a|C>3pzw>F>TEeY#wx8q@`i$>?x!A6%iF^|DA zS*o$kb=mDVmY=H<$Z$d3d)jHh2)fK`+yrG7?sZIr75xWVJM6{&NBif6`p!Nf&5p-` zvYnc1V{);;c&Tt!s%L}oWpA+=B?TP0RjV2q9O+_-OyI~}EDpyw7)XL8G-rhfBHNyR z<^ReCO|?&bqAvQlbfgyy;`MNPGD&CaumU5tX*)sr#KeiB zxX<}#cOMt|1P2j9?FFRW6K5?g*Z%3sXwo1|a@Gmx^Z2)_>7AphHA=M4kO+q0dFZpJQio_2rP z|FH{6Q-ZxnmFJmY3$dF-u;qW(g%9A^YNf{U+B{N-JX1R&?Xv##S4E%C-bc@=MbOsI zxF`elPOva{#B2g)#P=lZDJYLe&iwUynyF~ue!Gk&n@)OHVGn0#WrXBpox|1C*i3?Z z;{J+vW2fU?US=&+L4~;SF8@+ea^M6X&H7&75(`n>2i7_Z)X8QFnF*!&B6My1+a{6K z?UUWe(B$n$sC_1FV@2-UG=dMxbvT}rKK|;Brd5k|<&DK>meM?9UX;g5SsSB+Nrg!} zx_u=W9@jVq_T>Mf!9i5>H~c3qCj{tD$rv$}690J7<8mYd6Y=$U6#B|@AvJaa8vDet zctQ1E2)Sx|=K#$riOpfP`IYPfD+P1t!Bd_!7v$d0nxB|1$Ih+=!J>w&eyE zP0+%nkTV(-J#@sMzq3dM^>liH6Lj{Qc1CUY;;y^9$zhq#7h8RnyO)c4daO9}I;Uve=Rjg@3pw@=V%HRYRt1Rf+g3c0zs?ozkuQuQR>s z%erw0&Hrw<9{r5S^`%Jk%ipQ?nG|!bAbzK*`I<|YhS9nOGnDv_ zEz%J0H$R7ZEdPerZLVnQ!qMYkoda2Qz9_wj3a2%;#DA4y(&wTv7~rKIwE^_6p4+q2 z#?~XbzpNi$*&EDWIgY?e>O*sM1MK$QsME|EQF&_Jds9YFQO0nqf&T7$d0;<%-Y?v-)=-Y=DnC$UEYLcJjo~C`V~!%!eFE8X=&8B9qk5q zM(k1Vp{m_=CdXP5$rJ}!9-Xke4^4#Eiz}YSzD||#^_333!bvD@Lut)InYZpQfFn;A z=+KIbZ}yL|q3S)ae>e*$BrTevwsVxJNMj`%PYLQVZNF@iEMbo^)61D~2I4%AKelw} zj0LZRW1m3K#?WH=%U(n`5?V>Qswn)+7k|&OvDv|6;Xei1I7{V_iRAF-9L(MxH|=*~ zm&Hk$%37LQL@hsRqC|?t!*RS~HEK}hrpUJz)dnxe7I&7j>I^~1zRG8MiT~|+c;E_u z5_4@-GK8ac#-NHieHa1a97#hxS0bgM$#t)sLZO7H=-LhIKLFkSE`4!N<#pw*5<~xu z4#Hd%vm>FnT~|Mk>FKNPEgTIF391`8>GHmt|C-g1P#O{n>s?{AD1EtZXquXYT6TGw zpl^{pYXB1dDqGnW9vcQzX}wZwN-kh~?`RdFh|(UG$Y+Ppwh^=CZ5F?0>HaFGd4r{= z5*8)}ctBGq+x7WRxCtb^R$J zAxtBrTv<*hXD>T=_8)-s23zzW-~;~{Q>sDiQN`yJ%bSYl@yh2K_Yb_y4y=vQbkbae z(jqByvXbxEOewXIsh=qAqsdk2KO1W)6DdhF-G#B08*o1@%izVOkN=Q)r~0eTg)RLZb$~E(8ka)S4w|<7}t+FRqX+*#JrTmrTK(hbXS(=F@%4q9pB~FC&DEo z1k%U6LiZmIKb$rhyB#V=%^Q8Qk2>^RutdI2X1lTD+0~W>a}9!l4SzTFde;u$pE{Rm z=A3M#2?({9D&9)CE*EjZ!@Hge-c48Uq~fdM+oWFD(WfY^nWE9&nq@ie?e z6obEo>2>e&QOL_z8ctyUX_*Ly9$A0Xk+J$ z?e}E;-q{vWUnE<>SQpQ`KicfFmf~SE+=6;UAF2Y3R?Ir+`pKC+Iq&@3|9;iU6yGYp znhL9+$l&qf=5j7raC3u)!htveKc?2O>~Jma^T=E&ELvci3t*E5CCFAe5s*;s^NxW& zps)xDDYz!0Kc6)|5MEgn8b#IFK7Wc?#!>TFpxHjVpTeG0Fq~HFwUHgDP?t+%4wdC(v3Wi!JSGzG>F^!tAcNI@A{cZMkjhhncIAr zLta)HRxtKn1D-cd#7e(j^WNj0bKkgNe9SIA8cko2lLKn)RyvI{HiUoygyZ*-&J6>`Bfwqfmudznb!nJhCKCsE~ zvx_(0JN=#N%Ki2326m-{kX&ecNR4Oaqepk&Zmu>*z7*jh9x20Z zqK6i#dgJl#ef}CEU*5x&&^FC*;K*RuPjU$wjeXh$dnB738rcqW>4%+qEaPr`w|%zE z8Ft}M(9NGOu8XyVPgqp^lDmDP?mA6~sv~8=wa({sOi-7wn_!0xv(3>_%UCbb)TuBNnQiEfZdp8jHPE^;1Af9Ou=s`^lDdd?JZBTv5P} zMDhLCu6u%0P_YGqIVijE`N_Z)?ZwrOXlWZN-Ivwy;H^|7488Hz+jqbU0)JwPn}`>f zO9qkix~aYp`oYNjh0<;*aw1;Hb%v%=c#a~eyU(2cdFb+srwx@LM_j}V!|nk004}!D zf3@Cu6-zG%7~}>Xct;HSNITBaUmZ$$R`qQBNIv~RK__}Tp~C&neFn}2P|#ppTnwRxafzm3rth3_pJE3Trj3~jqUSM=NQU#YpZ5I>a?0vn zBQk-~L!O&Tu{>Fj1oQFyK*{jR;7lJ|)ywSSeh8Rw)N4IJXFxtVMsyyv7?PuGsNq(` zcv-iCpY;-n+(J>bkb4b(yRq*Qu54_3C2xeQ~^fek@d4rajR$L0QPyp!)U0 zS}dQ&<|R1mc75^X4Bb{P>}pwZdoDY)cX8i|n2}g0gJIy5vhrt&@6kGPevz%Q@Fhj; z{C}|HS>+&s9mW|Dzit;w899{SJ#WQ?d@HV7q=0{+NOEm9g*j|2Fh;lbp2Coi%$He> zp4t+RnGG=N4RvUlR$Xen;er*zKql+M5^-TvAx+d2c8>NjHgRD9Ap{!EvahvQorN#B#ypz=v1O zFFuusf})1blhE!6IF&k)@Bd35>!S^B?1DB9YoKJ5#vw-q{sJzqW)qD7ag8oM*3L-u z$wJh*p1fX75aN<{Z%y3iG!V0*I^q^5S-aO)xT1S!yDr8HQG#eE&DJYvM>1dJS19PP zNa}MxG11XXBm-;KKfNH=OE!QCBHa09nK{*$(XM^=7C*k-z4g(nzJ09|liA2Uryu$< zzdu@ed=OmN0F?W240+F8S;<5|bHISzU>FBs=&Qx>_c*QTRD{c^^ZVgpFPv)>qLnlk z`zyX;k|4m!=l>@O0xd|87Op#S!HXF$`_}(h72}KTZ(VK+M)@QDquz@amBA6_Rdika z2Z%m>ko?*@cN=ly2V^$h7CUhYee8S1J1I>(d9gT$^H3VBA$>ga4=`Hhv4hOtr=jxy zFl6*8C=eg#EJ1dIK_)7WXib^zJOU+@Xo!0MeyS>NMn6)V8?vqfp;}eal|a+WK)r2` z;g&|l2zEXe9xPbnr|g9hqoFMQ`e)xB^qwb@E9nt&Vrk@buV!Uu67I7YGbl5>|F|!# zGE6w!5W{X6y8KD_dHVKIVHqs5Nn6F4`!uz|Hn?GfN4}{JWJ}x>!kl9{MvlD9Dswv! z5=yUDHR;0d=64XGZ4fClGE0hG;ZCNP!@2say1>#0LoZCwId|iKgu#*)`Mk=7-a$&4 z*8c(Y{mWu87PxEw+sThB>v#Iu_rKe&+X($7(#EBQ1vwuy%(HI_{sA6e8TDI~ZF&tF zq}*50>-+;;Bed#D#;#k?@9QIE{{fEwv{~PuFJidIw~u_61MZd^DVdn$Xq$3%9 z{{Y(8E01<<{{WFwE2*^Or*J{cXIh@4?jc1b9~^{t41atxi_h42^a@%zxzpvnyEn3T zx(w9#2Y40o4^aN*ety{hQaTz#&%mhcA9~)O)4gRB@UWGB(D<}+3eM#=phhlJO2{AI zWC*V~9Q))i=q|VhLn|`E&C92_dpko0&j~AwiFk(mOwEDJaXguNVbR-)r7su^X;y`_ z(m?B@oCcA-pOUviB{mnM)N|XCOOF@;e>JI#3!ioBMVY6%KQEwf=}3AB?LOzjO<%Ay zr}ZUvuVg5br|-fk_)ApnoWSUK{1E9u^ZS%yP4SXE-nDUq(<+a?*QDnP^M%E0F3J)Y znFw0hkQFa!oBjdp`I;Ps`q?@?)D~0+DX{+m^bo>#i#>(Xm$n+{Qop7@1na&ob%#sV zcH@5><~dfJlATjag=h+@x>3SY3KEDn)2+A%S@}tKrDhH}ugf4%+s?V9O{)y~Lg1C} zz*EabX_4mivq6`FSKsRc*TmrF4WFuBLTUZZ9ePPa){7cH#IQvC`kV-@4>M@J+`w#v zvn0NxiYUAI2asG8UjSJQPZ+tI5l#oJM;B^6Sku?b!}47a4Rtx(cwkK>Hp`D4xeEPR z>_Ts&y#E0#Q&AHzZSfa|ss9)N_v|qfbf{k*+i`>N~*2iTmy@yul(Mldi2dxzn zR^wB%rwEBjBD@>*dXoRJSODBe86K5}L2wheLx}y_e)kCp`nb%Gp8`^WDyF#$UdZel zP_a@7*lJ7+n9yC%?Kb0rk>@pRGz?X8o0yMMx__~1!+Lwd-&yy|S>E}D6;C=iojfCw z-0|jTB8d?ZGdBTqma~y7$J*G|*q$J=h6p6-|EImV#rP=?xy#881%3VLAd!3o()Ou9 zva%-Qv$#F%)^N92p5@Jq_@Gsbo+*|QlwqX&A?5Kw)|9E#4ej}DR! zEizny#6j{>ZaLMI(2T_8S4!2(ahHQ#&M~arwqSQ-jX@b0GHpkcnJhlR_smtld5zZw zg(L68W+3vw;){ZkWBW+0@KN=y$5|e(!@l3^yrA?)>ZN0<&GAJv$D>@1QILkOxMi9F<`i3}8J~I|!T!N+I8-*wx zu)pg+GcXg~k67vdSzq*$7l5_sYC_f_DBX?!Y%(@BgSD`cfXjU8Rku&hbWmokuu!T| z=jkB=WY=ZSC&Lhf21)qn8wGF=K_HO8N z^{){UOp>byA;5EN^SF5(WgZ}qEw8%I5v#0m&ifhXmzOz95|g8g(^k#TpLRbHpk`Br zlf59e9D7~#nLI(=Szmsm^&NVpDQYmLUFTzM&H(+*cj$L{EEXGXZm^FFK#U*|VlONq zuwuQp@SjVNn`p&d_a?Sg-CsN`g zmDpcIL2vKrRKXZo>wRWy$@1vwx(<9{Q937ikr|Gx2diHin?5k)Z85`xqj#cvA9hk# z($IXr64wpqx3)K2oSb1EVgfXQjdpGCV*tuZV4C&uU_5p_8BRa|^%%9{5!;bc^w4kR zMCu;IL5oM0GkV}l`Ao#n0ptg|@JBxhUQh2*Je- z%sx-_2@8N3CJZWpbGR4dC;$SiEkD>!uXIoOM3Rnc^-u{EIDexVw|b=@7^30h5VfVk z>OVk|*y6)R*8J~3g?US!-ReIhL5bfGA2a!g(|;E9 zgs!mdjhxK?of=FeSt}f=f#%eJ$?h8lnM-N8y51LKLaQ;}MUfRz$Rm2mt;OAr(51FF zsaN$sb9gM1j;hn#bKi6G`sJ?CQ!YMTZ>7BG6Z&|ek>8ldzGfom$wZUMFobTDL=H}c z3Dzztbh~i8rl4R3iLwt?8Y_f&36JtwD&u1kxv++BHCZrjY}s>aTM3^+zrJ#%7Fj!L zdkxYTS6@WvD7H@bQ#H#yH>gW+n+>S+)Q7Z z)|4FprI5vHVSTwUTWGlLUfDiXyJW}1p@s5mM~!?aL;H9~ISa z%p&)T2MxIIwi@JKRz0hpseU=&QJ2G`X&J@Qb0Mx>I(+_#r0BJKf0Y@&L^sKzYUUW7 zYeURZkLF#oi{hh~*_2)#DaV$zTCe<}aE2JeZi1zvB_05~Qb0Os2OIzd?suFR`p=`a zDBa&U&}A@htUu7cl$iIC(SL$vMyLUXyq1f5o#3LbZtvsEUE>KO&8d%xpPGE$&^_g1 z;wPn2?1hgW0RKMw(c#L8`^R2I++Iy%a+$eY_xm>+5Bxe%+}Tt;JJ5N@5G*kYk*F^O zkVz`OjsCkTBEI?70d<=9do;@&ow5xcA~A1V_;1;ud}`rs(z(fpX4svtLQB#LVW+bS zyFv3hGx>UL(coq|w%Z~yknlxPqGw_jc0I)zJANTr?qp&bWqkzAA9dYeM#a~>ugY%K+XpM>N5{z) z(wYF7(t|`qaW~;ZN}gP->>z>A)2Pqh>#YX0->vu}?}eAPhTknL+=rcN1gz0!zmg(D zKv!>0{=1~x)h|yE@YcR4d(p$or(}qs(yLq|(Jzg;jlK zG0FZfh5Xo5y~uCO`MgXsd>#x{Uo(6jt}3ChP1pwq1rK9KhF)6jDnvtIojZti#B3_Z zR!TJ{X<~hX1{n!!N?PJN%jhRIr&XRd3}7R}oA;p7j`BBpMJtszBPKP?`H35-krHf5 zss-PSI?EL$xH|t>mi~nIsX515iV54GW0})udj9kzDjgxtU5`OLUS;q&auG}q>*f-j z43`gw=)}Ey`a7}8VJq4BWo;_vboWQqYIuH~MnyaP6gss~WXNEvjB81JF2Mi|dU)NmaqBzeL z@?v8U@#@FCPW9V5uB17ss|aT0B5?Jvw6r8euh>jYulh&*9urr$s!cwQoHZ!zOWwwi zh;n9$-koRrY^%G`fw(PD(Ho!~usB(@#d-p#sosUvHrDrXl1aLm$O^;SuP++M`1x*x+CVeUf&6B@ zK>0vA5>a^Z7N~dPFnKC80l`ubH?G3Ib4J1nmJg}VshST})WM$e6G#x-;AD&Oy33^x z50lTN-C+jA#E~*~o0_vq#-foDrIYl{@KdT!(aoOUKh~D+R+q)+qo~rthMb`?Ki{9o*PWvlir>HlpSgQ zz8O_5`1TxCh~O2vfAMs&{`Bfw;p4G|vA;jPFPci%>H4M9NRuO=SLRz`?f=$K&3Aq) zXK2(UHoi|g#(3q{`VTPeIlU7Tqj9Y~#Q%7J(XQ-&QLN?FsYAV4Izo2vzBc)&0fy(o z&3eoM85jAh<_xvp`g`_A-pK*M3F|oFhG3lp>^O)bRU#fXRRSrCOdwHzM=4Gbvh{#Q zdrW`gdzIeQffjt8`XP(yv2Nz? zbsL(qKIZ?GrvCt!1r2)Jy_Wv~#fPr{0BYfr|@%bOeLycc&}Tu|H1m+ zoi{ z(f|LYj(Mb0=4MZSoMRq6^!k6x;)oCJA@ymGYXa=8jOpEiizB58eF7c^&FdHtvq!G+&C$s$ER{4Rnh{rY0<@`&jF)%GIXzTQB z2=`5Eu?Pp>t;t(84j%9dd-2R{4fPg+Ei3dxhMAbe2UbMO8>giDsgmEfADxb$!ITmQ zV7kZ(T^fcb-={1%N0Z%SpMfrTMg6yX$)}t%?7fru#J6p1%Q^x@kKfB>j7Fm(WH0KLoH4+D;eiSJe^8J95R5S1o?Y}>thnJcW}Ji2bu-hD>VV0U;a;S z7mv5Sq?s(QQ*nGIk zmvK1+VW88{gWRmeo)+F6E~M&D1m(ZE7h@ne@P0kB+B#cP%SeeX7tQ8x$By!R5%a7Q z{#tjw3R72@zV|t^?s+R{Y#N?#v!~c%b4}4qL`#@9=BO#Z$ zi+1aML@V6$*-1F!h4?My(;4Dgoi#K>*m` zBx}XZ?|O@PA^21ndY|m0${iu6`8Q#h(@Rtta{D_#Jjutq$71{HI(LT7wR~sKS13?u zLh$tumco{?)cVk*^`H)ZV`g52M6pRQR$AQwzS)74$z#GIlx){gb;w{vxN>a=3)S3l zZYAE*7RE}bE8=$%S#Y8WOuBO{SY!{P<8~qWCI#n}dWm2SkCib!Q;kO6obPF;AEg<$ zb!K?!>DR!LpC>_G)fE%w=e<#r1Ra8lkPI5eegh8*eKa`+B%9$%Ajl*_vrz>+1} z-*d)j)MWGk0lY@7K=viJlgqR>=mgN8XaI8b8WgV0pqxa%k7g$u> z3xWnL$kqp&YTyKTPw6pefWoX;-mxPBJUktJ6m$L8N^E@t}i&?QZ z{SE#Qxqwe`6#sRqTwOjtkrSN%Ttv76H=kYEhWI}!)1p+mINYlZQ**mH&cKIvQZqH4 z3!Af#;PM2^*4?uJSv%ln`3wmp9=)zLRc~7So?T%_2qxhM9T|vjozZ#$t#*oA3&HO= zz_-_w_z5qpN!)&nHX>h=IM<`>+!W*bvr*n2b7$uQ`a5L1iUQ7TsSQ@Ga^_g$k|@6B z57HA+-xK!iv#twia#eENC={a3oPk^uc0YN!{p5(X=qWqe{3192pFiTym1$r3tqkYh0}9*XT6O)o54=9^Coo+p;ip}iiHaxK*M@}5 z^bn#+Kv_q1bQN7tI#TFYPLPKjdM`|;`taW7nOlx5%UOFgzV?G{hanuUgk`VCyd%m{ zFStRoe2Y3h7jn^8&=Ilw{CZ73lH|=SLBq&#@uUJY=Ki! zGS7wjoJ=in{!SsfpZBwCH!(k_-`}!|i9m@l!V(!-P$SUUkPgq-qV6OmE_j1mSBd-3 zm&_h~tD@TGyL13c=h&J`uO@1nNKIs@XLF!H3K$8-6npPwQZQYK0Q|^vOtr zAcXvE3f`5aDHl$pWjm1(M*tCA+`#q3uNLVPUar(~dh@mnE4WH4JG0t6$xNC%%{4~5MEc`fuGewsyrT2(HC1S?sL|EqSamc)Z~LyvN%>lw5X*}T2+jK3=>SS;mCbev#=S^0;}lF%OiJjWW=?=!Ebks13M~53KTM zw}aq(F->wz=}@NM+`++3Yx+ua32OF5R-V7tDHh=K0bhmDYBxZ4$=C*j`?RM+%tBKY zz58%Dzv)-xE^V76-0OVN{Gx6eYrJfE|6}?fE6tsh(3Jb#aQ2i$S+mwx=Epe0PoOsl z7IUzPaD=LNc;t6&r$q^71nP?r$*pdja7yN*xpPCA^L3oO{OMIHXG4uzA43%=WvpnQ zp^tVo`wUC^ZIas38%r-Ktog z-C)C(bMb3FH*9efj2PJM$03lMydb)g4s&wQL%-UcZd7T*H)eI_nF>?<>?|O{<@t7- znmGKJy5llDvt)e<&trg&%!14_`;Ef;WkLbculB^39-}Ki)xN4{nu$Q|$EfS6!zeiA zRU!xmwbd_JuAq=0SVLEg*nmz+~_WJZR`krEjG2f&dO z=yUcRh5~y9`Sd?s?(jVeo_j^w)CJ3Q?&0oUTR$H0dQy@jw3hxK;3BPqa~0=5a!!fuRo~b= z(?3!xx&G2?`rP*)?E_`*r&>2Rm^R$9TxUstG+vbk<$K6O77K(Q5#gmm#kq-1gUc3+ z98dA^^=%WI^yO+N&d1z9)l*3e+NoS4!ulVToa;j3l<@3Wr1=yza-E}gW;bIl#n0k4 zY=A^0ytG_c9dWO7ruW7kFT}1&uKnK${EA)>5z%W%;#lBS3H*Htt#>$8&U}VdAfZRc zhFvhRAuD5R0HK1oin*?u7wB{FTYg`k?X?_6qdkeGZ~nU(7G5A}hJvTpJ`vT`s7+o) z=q4Zy8&D+dJg{jHq8NWRojGF4N)w)w6r}cMMT>_>PL3?2wAii_g0G|VVM*af+BAl* zqR2V-ica^n_6%$~m@VeLYV$K`P1n8)kMh?3nqPb>EC~>JTs@;tRn${la{@8ZUddip z!-DJ74kNnlafBVuIY_)i27XbrZjv>ssNt~cH{EzibWzIvu`Vtyu9OPFt%@7GV%RAY z000o;pb#Eo;lXca-n&x5*okv^hjvwzlw{<{W7llu$btjlIm`fxH~;{^44hu#2A}(V z87bc^W@a*C+hLVzaUob)xaBP#3xV$@+%wwjp5_amU;WaqST&+k^vNA^g{U?nyM6}X zM0|4&!ek@QHj%CgGFyuZJoH#&qL*Wfz7H7NilD*DYFMxPWarnsCd^KGb7=m2x#69_ zrDVk1mAlDNvQpXe(|VFL)j7?djgqSPYm2aR_U>4>@L|)&_^cr6-^cVbVZS2Qcs@

Y|WfBcA1(nexslc+)wTX)*hVe953L`MeY>BMILh{gll0NJnTnve4AH zuAm`Eigm~mWfXO1KWS$L8Y$ zl^QoF^b^MHvFpin*K`xmntTbWFRs7%D}k2}FP2VAIc1LVs$z~=4V0w|RJ=^3D#Hx_ zsR%kwt{kiX0c`4%|DrzrJ_Y>wqK<1ewrbm*}A#m@VUSu1I-UuX<+HUdAXhgFS9GouikqHiq%4b*6mfdkE z@978YfFmF1%}d#Ew2qm=kKprRIqw%i-Z*NK&K<#v>3(Ng@o_ccYE3h*Ia!`OxMWWY zSshyssctl=>-oN3g;_cHbm&*$B^&pY(e*i(@e@I2zMF~4Rp7IP&$1IiroF8lFEQ+T zt_A={-XpLnr|(*1tak4vJH`K6yb`7#-P6YYYiQii5_LN*qIw{P;s*9T5qu$HkLXN79L(zADGECs!yt%NC6Y-MkCW zdY^GSo=kHKDS-00Vc`A9UryJy%jxL9*D*8VeGu_PBl@bIAUmWY%dEJkYH!nk&zwcf zzUFUle&b~JJqmglibFk-$8LN+n^&{7)T&wM7`Pc5NGi3{snDo^2 zK1n<}m0Q_5lsW)5_c&D>ar`B4Tpk>~OmfUHOLBfzk<~){#G*L#XktCn4e|W_`sfhJ zberKo!|!wj&sX~?&!Ykm;`^KI?0rpuU2_2Dn62g>gKux;6ZC#$MvCm>R}!uPrKQ@0 zCRO+!Lud@BecuR7Hp$8j_{mH3oc@aX+gFaw_V?j1$UfC~!GD04O+(d5^E&fpIhMBI z^QpLsnu z+_&Qr%Q%6($^h#Kloao$0^w6TS|WFjH+sMb-5oG`Jsa~PqX1`iD(NY6GW3^?%#u>P zz?k~rr3@z;f_8yR#ov!!6(v83Y1w$kSt6)_ivR>oxhOv`!#~a8IB{Ao?J9piEYz~5 z=ShZ2+UVEPG4KkyGm#jMC%huoN%(e4BPB!QcfjYG_u~(Z9J(3bH z65mjre^*w2ixkLUn%+i^)EQ=N<=*q`9x2n%rV6hhK$RlT1A&8)up%7qPo!QFBh+VAXNRDX#_-oXaobvK;Y zMi)DMJlb!kSl=`Ea-a&%=8=!BLu=3{_OUXY|3#7rrvhXhPYYzPGZ0CjF zCb!Gp?xiVF$aCoDx?-m{w1ECd0aEZwr1Qv`D0nB>8`zpGnL!noh>VH7hRX?-e*6t!AUJbrQ7o|6NsjQKKc=tszOce z!v4h@RbpA?|3u~$Zeg5`*U;5;wc_$_^o+q_G0r;~@0p=J_C#)kR7h|#K*r6QSD(RH zbfBF&L5k-j#@>m9(ur)z7FiJ)&BNL;9@-MpA6jQ-3dk?A@a+87$Gkc%-lyC?d(^zl zphhS#^-II0aVQu$C`-o4_tv40g_W7P*t#$qeNk%9zN_6ryZg7Y|NOZ}k3ApT)2BgX z7(pp3n+w5G+M0fn3+npY_4hre?n!R%VSdT4yM^Ghnq(d&`=^5{W$5%wub7>z6oI2tJ@$k!@p2FmjGe`_*x2q1>nQ!N z4Elg~dM5uZK1zJyGsKYHhO&u@Ci83H*!ro=T?(P}Hl}2=ns_`)M58{El^!AUcX?8D zuz8+IZVaEbr#O{j^X9?GDV$*)pR%>0&3Q-L#DOV&%Dg>0Cm){B_1GgTy z^{@1ZA|aOGoP%ull4dK$cz^@|;mB1q)s= z)?V9dh;A*lY1!|DFQ7XO37FG^9Z6ejj;9GhP6;YXRDyiBQrn=QYz3I0WV(b5B|w4! z00Ga;M{qeGQO~_gU0^Lp5gh931mhUsjAyyWZ(ncQ^qV#GyDjGIL0F^jUg6k-sckne z*(reC*M^MCwp1GyIY2fy5km_XX?eQ|TkV@zCH?bPhlw{K-5I3&F_#t^QY5>kSt_5lQx zgP!oCk~({-N+}7+8A>ohkKPVP9OE3|)j_S#(O2~X%*2uUc16t5*rw6(0Q?v~dj zeJffLlro0qTEf(SGc{_6$wr*iB)F5*oTk+)jP~S@rarZM9fX&gbE7{tdPh${;PxbT z9l8FV)Q+sv@)^)N&2A{1>1{^oPBM%t(JcP921-Xfj(vSAt=QS&_!bC^Ck$aW)>f9z zw#z`NN;-npfX-ODQi@% zx1WCD_m?g|b#-#<*EdhNN}jlN#|f21a|=pRyro|wsY+6mr722Ml%*+3Qk11BTi?>r z5;Vq_I93jxvXR_!ZLxXAPaXKrIpkJ|E251dro);yNhlR$>AMoM$xsWm#V5XUMtJ?A zyMIMqOE-CdsFAAQ4*-em<=>~yy+QEKkgNRLCO^e6eg1D>)hqT+qM!l5_7uK?c9zk?V`aM1AwKd@q?VJ(OJRcp56N&$JAIGNCho`3KU2n z5<7a33H`O-ew>lOKHF`)ZOqQw?Y(;O3<5|b#D5X{d5_;#eYnnffwHWefD?h-pL~=5 z0K0t+Mk)f;`k_bAfsxOzuOsMt=l0wOwBnMYP=y6&sVe~b@^X7}aqECS?9^-F$) z7{{>n`;II7_E!n&0a;h61ayzbgP!?6vzobX*?JvFNTPfs}a=RBSVuYXL{HD3S$h~jzTM8?K;nBuPzzyp&60zLSX`v~M!t`S{HPe|(H zByv4{z|Kc*-Me?F-?F*@=?9U>SJ%HhpYZw$x#-g=D(XlX#~=WBIOo6N_x}F?bEmmT zOJzsYft+XX>_4B=2B@m}GZVZIb0i4bC-)y(x@`cRGI#lGW<58o^Z0ab7K+=Z_0qL% z;uW0Dn{RR|r8>)H#J3IPFQB;M0UbvN2R(<^RKHP0eDYRy0_FGtJw9kso4VSsn~WGg69kqCtcfN_Bgf+hT*o@ za&Wt+bojRxw+-8=#~XKA0^M>G1kK7=kupFrF~{0?@9rxl{9b zg|i*!QP8=s6*RE#mO%|6r3V{8O4JsTm2t*07r|Ef=__83x$RcGaSQz2qX|d9+f{(`WNuDdKZTL zC|~DGOq+&e)nNFp5q0@~*55T?Sntzayu-6C<;}oK6L*PUyKUFpk|U(0SombbbtB5SH;<)PPFj~#MW zQdFd)p(q0w;Br9Kw(4s|?%wU+^>buKM5nx%Vb7B-GTTfvKp`Y4Nnt9$$;l@?RL@80 ztwE_TOjywbOuIPu*&A+5x0q31m>W|Cvf8}I0>(N@bC5@&uf0{ZcMUr+_K>DgDTe}P zFx%ZGG#{gA-hyMVF43E)nspHwVFm-=3^=GVj{!O*Qn#D7|EXD3E zhva)}q^-1BTOn>g`8~^q89}*mWv|c%+6g+f&eLt7MpR3N05UZH09I#n$l#sH+nUe4 zE4kV()#0I?F#Xyi_q1Cs4Jz8F8ZJ8WV=$|eX1fAR()jO$|;wXkuWOc92;tqS@{*3yuq zyats452K+(fUR1mRK$Wv`n|c>{h;t`Bs^1UJvps6B^L_>)-E>1Tby=N=~`Z8Xlv}~ zCA5V&jC2_&H zIXs%z1l_v0*yOcjy0*5iDq*z-+*;7*lO?n|T5Sgf(!VhToMk8F996^Fy%nox$G+Pv zifz~0>q@P!xKOl(4xs8mN|u(uKRyx@)V^|YTDMgG7+Rtdx7L;zi?@z{iLluih{#IWOvkkku;=03;@R(je) zK#q_xgMyRCZu2{i-ru!QhXTcGXgO()6Fu;cB@C31TwIsX(Jh3jT$T1Y$*k8!I%$=% zE?MexIFH2MZR++7nD8rn7ZbeBS(@7rXC$=so+QA!vqIh?)yz@6O#&WDQ+4z^VBd%n zu?#XIRuohWke~u$4DB;J014lU%YEG3Ei0;ZK9PRMcVokQ{=-#HyH8XY^@E36o(4d+of%clw{y?Mr#A#+Ra_1 zq0hR>b~$WCTypF=hF6mEz*-BbN)i%!P)O?Zm$O-;826_(nT+yg7?(S;z5X zio0ixyy$JacC7E7ZN|eoR#l(?RNyMEi#F4t)Vw4qI(Xd3B$77(4ff(Ao^mqRt2A_f z4Dxhfn-Pnv5N0sDP?Cidtxrl)2bfeArKu}Roh2E{k0!B$!+M+1yVY#EUFH3j%@JOE zvL0CS+i%TjXQ#{|90TZ~$0wZBpHXba=g5yPR5=#Oa#>297JE$O$eQ93ImBjJOOLNO z9Hgjc0<12YV^j@W4m7zkS$IqlB*vJh8bf(FQeW#QpaMDS91fl_fn7TxbYG=+c`M>K zC}qB*9Aew8nAU8~S-hdkKd0rRoNW}#{{Tg{DNCP3ZL}?Rmp4rmyi!KBgqq2-V3oIW zTX_mffhyigR6soQ9DsL&JDuY=d{}DzKdANfrOPb1cQ|sRbiXN?4V6U8$p@%5RyzQ! zDMKKPXT4s1^xlhBm)Tn-k44|x7w6(ej-Rl+lseKC9m2N)n0KrMxBw@N5;AgY3>ny6 z#D{o;BK)f?MkhEV#}n@fX>wUok3v~PtdB7yV}x)B$3B#nw%gstvdT@)^K5zBL(1rf zE--RMxwI$b0Wt$Yp`R^Ez~J(6oadV1JpuT4%(g!bIVb!r;kly+=uO@0`ELNYyTR@) zGsYsdDFLiOw-i~ra@e-Cn_D2^SBA7LGpJP$ptgJzCDbT2zRCnTu5^w>>||_t^%Z@5 zD{ro(v}>OyaP?e=tg>S*C3B=LOtRvhXn+gOlxLtQ=Yz?`QQNOKNh`$7zf@W-P~Wr8 zzec#fEv2$pWVXX>o=~!cpOBIXPJ5ne-pAVQUn0o%q+%>3*`nY}mejtdBf6w@2G-hI z0WKE)V4ko_a6lF9WZHd0s+ga2wl^5DX-8byGT-u190VZwg-Sxk08lwmPks+<^p^TF z(HtA9ng0N-Id?kYIab>o$oD-Q&Nv1NFpC_yZFh*4+F^HSWeuBra6J8?YT3kvJmQ%e z4~!;o!(qj|RvRS+r2s%k)c_NuY_C9Od-_%J@wuz?&bzqi7xdNQJ;4^eKlXWU#&xxp zT}-!J0G6Cy6cSU8${_yCp78bJM@74}k!;$&`x@JLvc*-jeX;i1T+mYEsdWi;#1aw~ zma>qpQ_0P13SO0#nIWkpxzeKFn$o<+hy>&eVFYAwYE^pE{$G&t{L}`}QjU_OC%OG# zf)b<0uRT7zVzrFb@E6kT>+tc@-hyH|j~iAU)4XBado5=MShE<*Zrc1#N*)Re~I8rUQ}@B`O+cbG(jZoxgt-PiqI;e*1Zy8K$0&?ZRWD z^qoCPTgoUYDGDGd1QCp9JaO8eOjXKnmdXQ%sj<|glYxvAfO}_wlYl+yu>6$M>;UhW z(Z|j5bsU_K7m_iNgOi>J%}k={<;P?`;3`4^QdELgqJ0NPU~!y)dyaeNz2RV_VfGew zEh$bW%F^1^4lC;^HteCK5(dFVC@+w#Le&sY*f? zg)%Y*4@o>6^<;6AoROZdxYV`AB|P(Ngy11LE6K{0)JNTn5uQ)s&0M!)NpUp(wB}weC0M8)f89m3Z?!6bhZX^-F6R-e@ zJ&cc#o;jdpK%H?Q2<|{JyudI|-S!%xMZNX9lmvi;jud`yRqOJU;BZgMKc1${K@J?G zW2AHw(g#AboZt^*-;ZEW8SFcsYK~2f*(@mvSCXY* ztffUAKG`6QdUAd7^{A@rQJqN<$3Dl`k7xthsX671M0VUysNM#|9kwIy6-lNGqT!bc z)Rd04h6+$eZ<%8t4(HSp>r=JCaSfLOI@OM&jD(=)9XSIBwtEb6aaCh6W3d=8@=04s z!C4(jIL`q`k`8|!N1-M{tBh%($cakQ5-JlmQr#CJ@_P!d8$4#jKBg&jpQAM z{{Y@<)hb(x0WFwHp+txpc7h0v_%S14$*-u<-CG@Nc`fIjl&d%$hDLu5!1o_Y$>naq`3e0GE34hsFbHZ1nVz z!0OZ)0{-(rh zIfP&kK2>mV1_>GCKezXK;=JF4LQWHsPF2P*JL4RCo()|j>86sS<`oV}NX8NZeR2;c z81KobgxFYF$ZYVFfb&09o7a|a-f2uMG1#(2(ux%H@~BD!R8@SK6B2j%eOc}6?dicaaD)Vq z0rot7yT{Mxx5}v<6of|BI#y)Dlt+uHEK*)^SOE;Nbc*Y%R&Y8}gs7;KfCnbG^|*SI z!w&OlJwK;4=BkGLTUr@;refWZ5ly-f!j=@M^BYotEGLCzaf6JIbZp&$NC80w7*PI6 z&&qRH(DQ@~Kn zZQ=)Hc2jFEnh$7H-O^0BQeZ-5mlE@6Gng@7l_qK&ePwMSVbq6GuQ6#UDjh_PVeJ07 zo40$LXxA^J%_87jxR5zhGT%?z2u?XAwYWgzoM4XiDTifo*PCf5NL$N8K}%OiDq4b3 z)K3GG-`hNKSK%HUHOzQ6>y+VPgBs|NyK|Fn$c`oj1Skf2*#X55F}$TDkW>d5=TOL9 z0q76IZconHJ2huJyl)oklJO*GOwH5`ZMSnCc3SKoYVC$q3DlcgYn!X562csKE!Qqz zaY;%%I;m*`WhnWN51WB&k)t?JPOZ7o|tpdUoI9Tv1L<_c8H5bf%YVz!rCQp1Zaq@~P| zP6ts|YYuOk16yb*QQ}`t%cDdCUM06Aaizp z>F!L-zfS13OUE-6h1OBS@%L8a)=c2#rL7lNHqNr8rrOahEyddYl`zuLQ9~^uNh?~E ziIjjqAefE0*!l=NOaWD=gR8xpL+(Ac@qfCrQA$Y8 zN{$YD)lcGEUP9D*>SV1aG8@sIjKUqbB(zZa5`rFOE+?o31d@;fvPtX(S*-!JdYb)m zoo|H|sZB#|QXgyWOJ(FGl(?|_VF*&gNeC-+q=cyn`H2K#Uw_h-5{6w|lfXk^uw31R{b+U!5D_A6i>~zFH=hU6zdF}168=mNBIbIvl zrV)LN?e!LDpulcRVf4JjMCS=hq0g0R!wNW$25sTwm)m(y zHnereTdiSvE+CG9&Or6eau0^CfaJcIX1t{4Q~0yb8Se?p7MQ)J8`Ar%(PZXXy0v1? z>kWN|@Y{F1HMfGlaBmBC=uot|wnCjs3P}_jWw;Ak!BB&s5GQaVdH2{%4Zy6T*$vas z_N}DUcG)*5@MlbqJ*Hqj<876x#^fO=Y%Rc5)>Z;aQR*rYq`0-!la)DWG!T zP%Yz?WOSTVz5Tt^cYRL^(xl2Q+8Sk2SZ!|fmm(nrP30tY`Q^1G9Yc}W9Gcm2--UjP z^=k|Ggv-|bMYzP8>o#k`GtOdn4_$@Q^3CoJqSc|oozhu-g_fRWPdc@UE+br%S4p#F zWkfsx5~Xl<0DkkqCQQu7IX$QK4xP97j$XUF%waI6L!84`H>3oHn_2=QC8mmlWin8t z{K0A90+2`*kDe)h$EvmU*7>JpCR0JEi4QRMS$C?3{dn1UeQ8z)s9u3RPrnEfV9cDK8+o;RNX_1i&De699p<4^ASs zZDH5CRu_h0rl#H?qWsOV=RU&o%_T}wqbW_bg)gYBV1(nOobWPut99V>SLuB|{;$2? zrbM*3q_i1jZ#JbC(1}S?h%PC!(tcH8AOH>t?O2JVHqUW414}Udlrt@>S(Eum$um^K zn`kR6ss8}cuMPAzvJ|oaMt=&idp)?V>BPse{!O{cbiCxmS*{;$lu*KsS_oy5rC}vV zQhQ`zj@8MrzlTnPVRM z(dWo&D@;guHsdV5w$M<7xDn9mPzfo>AcI*^yqk}scPGPzzTYoOLItW0S!IVz6Gvp|wTc;HxWe8)EF2zR&hcg4GSzB$bo~$6IPzoKo_T zjAX0jBBs;!_fXqzUz@i^5XI5QSZD0$bgi^H^14uxvXtX0Kk0bq*S}TGpAH=e$1(1U zx0f-j6C`GyiCtlt-v@In+u+vtt}%k)Y}&AHF?BZDUAw}c@DQi3Ql%s<)x!Ms6y3Ln9!Vckn4`fZg^-V zD{(3%N^v*=C%!!@#duuY);6nimnP*brpI}C=+nVo8EemX%VJW`S4vRQ99qdFCmAIA z)g`Gmt5N9!+&iLTGUM!_^u&)RtF2tw1=mlB|H$T0WKZz9KJL%x`AQx$!}N`BxiTKSXxR3~?3I`#M6w0N`V*jxqRD z9GG$-Af*XX$3f{zvH?2}fXbZ5Qq{^+oM5Yx0q?=!3=H#*0I7^?deEFK4yP~^@+TojxGMC?KVCE2 z*0Xm@$ZfJ)DlHWU0)fJSryGg=@l@o94yG#nqNL-eC`crPjP+x;B;)9N zaDvQ9LnHK0OkM{w*=-4 zFKLrIp2Y4AYL1kpa2olMTmJxZAWOLESGF)lJ$W4QkHbH^=j@$bSV`$g=OBW9os>GEiQ9q4 z987;7U!T=-UqL5>@jGJ6|HI^B_v?up4lFxj9~pg8ve5x#W)ZZ)N()= z9Cs&>P6uP04`4Z~!fXggJuUECz$H9_NaH@I_vg77sY9A@pq_*&DoH&n2{l3)`Z`1`7XR0XRAAw@p8&jUOXGIPgqj^pWH z(H&_1K{z=i=Oeh_W54)gHFKfbTJkzb1!Q34aFucDPa_%nWY@y%o?G21PEvU)2?q!3 z-yCs{-lM%z`f6l|`4h?CV*`L{{T9G;A~zGn?LWuY)~%vqJDh-iVUkXJdt>hz_r^V| z+(mUHbdI5ck;v|QHrj0P!i&EDz#Or8u`rvHWin%lKw1h+!Cu zxSkN(4qr0v4|d^It7;X6>IVsl!YdH9m(L&%$qsh3onG6(7Qqv%)XR0y_Xit`)l5pC3a1VZK(LGh@=SX^n=fidmbBkGS z*QR-g2h6=R!K`j<)U3kJT*BG6wXtYilI_i_SbJz&u3JlixNw~(RaSAfRuBTnK-mDq z>=P3(e%?mg)!y){uk9Lda4z>vV|a%G8`j2KhT`36q@@XcY7HqRNJ>f2; z5to}5tleAf+RrViwq>*>IVUKPg>}evDRwwgmW3RYoZ8WJhLtfPwJlz1lGrK@Hrc^g zO2JTGGr<_*wi{lHWV|!tdlu`Wb6vpB`HQD^`DgP%YO@xpz_7=#<`;~xfftJjeHQIF z=>?R!@J_}BK`fyP)K$5SfB--!k|UWt_}{+uv%Q(ryMJ)l-7BXrSD5RJojVPQ$n#o^ zwDf>VNK3=ZRzMvff-pVtRQ>iZhR7Ccj7Zl=rc)5)se}udFNqDZwKR~gwDt8Br6Z@; zk%A5@4EC2qYb&;wy+X3BnG1}Sq1aD7<+&52Hvk`#+jW>XNO$O#i@E{JmJ$-tQK~)yRO5lggpE>|i8BNc2pmr0 zIpeUY&eH3BJqvJbv#yMrb7EJRW~>zDB`W!q)zA=$=nSusJBMdc(YyE zd5e1x$(qBhsR-$P;Obo{bEE*;jzW*}hqg{6-1g^C>j@}Up0+UDTVp6{Ow4_mgkeP? zPQKejB}ytt!jB}UlblqpcG+s(D{i<;l;cG+P?)T{4qBz94+j!fg%Fgb3n4&bB=S3o z^?r2ek4v+sU9fEHiRUymoGdzdn0jORoZI%VwEiENrvB~Ap$dJQ)Y=s~&AQ4p+y0Zo zDLSgUZJKjTI1-`&0FbWW4rJ_~Xy5Iu-SHo;^rpYiP1tOgsMi%GVcU}tkiotBet|jLX20CV+A&n6c*t}N&RFb z{G+#HkGfN8^^|=u`GXDB%b0{ql_Fz~B03Q2kPyqStt$-_EhSulNl*j~ilB8y&F+FB zO1VRk2IA~wspsu>S%OriSx5)WG2^xtppXeuibyy?B;;neMqc<}=m$+O4w`htbx>id z=|5M|)t247xWgK|y~i%Yt=ZxD=8w==TCU#VmMzxu*?GpvU=0?!lu`Yh0oS0Uq(E>ixAz>&zUuZ%=^AwbhRGtEW86QfX zc!AY64MDwdp0`PbGS?x#Z`b{lbRSB6HRRhBF3X>uB3ZQvpI)KIeD zLvFGbq1K#&kU{4jwc9hNO!`}Z{6k)K$1!y=oWGoNFH0ZDxPAon#gkU)oz__>nTd5SC3eR?mR~wMxxSEl#eMVx`(dgk+BNm1!f0|P49_k+xuj5IZO3-t z`CDzgOW|#&&wHSbkdRJBLBSrCj5@nPZXJ^1nVO2%Bt@Ly`y9ZB9k=wT@ETv&p}r#zuHJhd$O>FDzsQ(?5FD^Hk!q;=#4jDeA!S%Q2g z{4?8pIlIxZn=etjQv?+);pzWzv+v+GvdLt+CY^V#?%oeE3#b2Q)*bqQASgo z^H##uw7QD>Z=0zgqADQ3BuJGl{{Sb-P$4BCDJlf0s3ZcU1G;DfX>uZ4RZk972eF$1su-K_dX18q;0fc#`~v*losE>G^u_ zu%fPUgpzu44gtp)^&+b)#e_2Rauyy+x&ly?m7xj)I2|DLsGQ)EGuWDfU8SiFC(J8p zX9FEM9b?g1JRIXW`0)1P)Ib<0O9Ch5U@R%G_-#LYf!|!3n^~2dg8~w;c12dbzAv9E!ksV2~0z z6oOnON&sgZV0+-;ebp*lIWk{%GTcG}5|;vkQiUiekff*#50N7$zBA4SYt(;D+>QZ= zk+kej?%sceR}7P=wxp>d0=a?5Zu@L|^SxVw#HkWPWg!S@DZxF~PI|HwJ75of=A)5r zeaEm;0uTlX0bt{Wa6tg$)6+a-y;qHC!yzEDuBVz&>M8wIC$Px}9P{bx$E|pNMVArc z3u|#}B%hf@C-?^QQTwGBactJ-}xRBsh;U`sY*ybSRkbYFQ^QW$8Kt`qn);mfu1%T)sv1TCty`4PSQ$Xh=~N9Q4l|sQf|U^w!+fcQOHp|gn~#V zAYi0{*!J|}-bb!jaa(z<7Ppd|=`BWj5N8KX`Daiuir$VeytghR$h93NBd_}4Kt)INY|V@2rz@_JGE zrKbpSCm~%{XlDeR0meA}yw_jpDT|D9Iq)oh`=!rr{{Y)wr1)e`tL)$HX@ma&_iRJ^ zDsmtoD5!a09y5fDg$<>s}5-NFXDnT#!abIX%5bKbsu;8SIp@mJdSG5$*^i z9!S9F*PM=ioYgd!(3e(_93&7juv47pB{?J?sr1j^UAYQUfgp$g1F=4y<9;^%g1v&v zYEcn80rv1cI6b6|``6N=7Y+(B(hngyJaRb4JOSSs!1T>Z9Ey z$pm$E90G8DY<3(B9&_q`wL#a^uq2en01ikX?hFIl`*^IHZPc(f)22HTXP-|)_l?Ij z=+&qdg=Go=VDtp~U}KDOK<(c?qrFWdS`wgAl&9_>6ZPcs5A$R2uc)z}OIRr(YdJt9 zgVU8{k=!0qMh89fUrLWT2qCiIB||3%jP}9r>&`x(jiQ`VjB5fXp4@IX-+nm$H4$pH zC?qHdAPu8^xAf$nO~iMohU`q06sx3@&H>K=06|c}&pp2J#YU#e!!4wOj1oy8k3u*< zUVTaRsSAx=5B|&wN>!B(ufNmq=RWoB%NHjSP90DnWgxGNa85JFZ1d`Uaa2^L0lUIB z@9a3iwT{mL#^pfxi5w?--{qtFK;ZiGSaGXq4hSue(Vp626At7CHhfYI{nFhm(KQ`WTk>r=~RNiDSE+UiIRoTwjR>ycb--m|ve?pmK~wXMOo zbnc?M%zDdw4+#R>GDGiuh2J@9NNJ#nOL6Qxl(wXRLWl%>(CfwtoG~7y_0@P~h0WGR z#4;C5u+E?}ZI<4@#<7f^#r9?V>t0L5ULw(W<^_jdH+Jo-cK6o>r<35ei=P8j&SB1l zItQ5vLuyjA>M`l(zs}1(bq`wAkrD|#NQk14UYn9dlgJgt~ z4oE#b_sQheTZ5rAu)JzE+=nsV>9 zyOm}|=I8cH9j@^j=KP|*mZm^q#HqI;xTPdW3Sq>Rwv*6Y%1I<C4xDuL` zZJ#U6YD!&5Y&Y@(mx_9l+0K1Q;QjT`EbNt<`s0`JZlC81+#@KXpjbto8JY0RCf^ye z#2U5Cw{P3wuHWJ;J7_OivMt;_ZWb=UC&fS^KuVl7#km39Xz;?riAYEw5JG2wuJPVC z^sI*7J6u>hS)igZIWrQ|4>rt}T*_7qr@u&*A!qeZ$_UOe++wKxjMD3#<-<@rW|4Y# zOApGBTk=;a*;feZ3y%~erDbibDOqpkNgx7+XNsRv>RmH>(wZwzLc73rMWHH0E74Dq8-+@y*X*q_I)FAj?9TOQ~7PkPlF&j9d#{~CAD=pkjjTx zDndesNe8`XnGrd;o`f^f>@}Q1*EDDTr}Y;LyT!4L8xga&w9B{IyBfu@EMF9A&AUe| z?QzT$i(4hK>j`z-O{Chm3U-iL+$98fZY4!(GC>d$W>o`Vq=*nmGC7*4_j^Q3)b>jb z;7hklvfmQ#*k-ij3y;w1-EhV$K@Eg9h8RLhx<)WP!KnR}{FyH8A^32fiKv^lKOR9x zTWrc~DUe-A1SM)A4ds0V_vhd1LhDOh>t?LBYArAa0~f?*i4@SIO8=4G>q;kYIgi;ty0boO7ndhZEx%v?V6R?S(c z=|a*47cHMk3RK$3Mvw|ZxKNOoAV};+{(dseFX>U=-(? zwpVN0tm~sK>zvoD?1xN+Bq*@bl;T!|B|c}CL2o3e;Eq5ZwKCGlYFpZq@}{C@-X$zI zZ&b;@XUbh^DoTi2z-4Ldkb-hPZ1=8_jP#=gUIUr4#&yj1)_2*jFLd(_IEGlkT(w4C zV<_Th6w9}X!!%+}Ufo(^x2ES3dikdqU8)mowi;mzDy+OV;VJO~G=hW?4XA=tKq5Gq zI*8**>g{YtL;+Qz;=SC#67qpyQf_oiWWfpx)B0-M=qgCfu8O zPXZ=Kff%;%>W?W(b1va%P{3_iD#5@xgqM4@Ptz~xy;(7qASEeN5|Xq!{7EZpsR(ro zDClWH)vZn`e<{L3aBEA&E_#ifvVKX&GZqr|VZb_f*6UnX9(M`AZ!o)_q+}c-(Z;Pa zOLb$XRq%hr9YQg zD%)Mv7X3)w)&YGFF(J8;-CUQ03Jvt3v=Ty+>uX60S`-ROfhtOf1Y92wyKe?2rnPE_ z7KJpc&AL3RiGs?TVmzLPP`8)lq#>|+3W~6wnOQyaRrZ(L+a{crZ~DJf+oak8!|X?c zIjJu^%Xkg5q2s*q^ReTGm})#;_gdY_fx{N#bF46CA7h75){)Rgp{Ew^HK+J zfL478BPY4(u3~7-bs(;;E^tmVv=lCzbDdd-W&Z!nBM8n(vm@Y8{1R)yO(&f@nN zUEQVLD)>W7YYJL}Wl4VNO`N+;ti0Nq1h$5p4!V$1k`z-2-Xx@?l|Y2f^%J~uTNaB4 zRF0>uwif2M<}9V;28c!*B}Ho)U)`QD#z(%YPmw3RO;XPTRCHJ?N3Xm)FAbyHDh2q}XlyfRdj^%7e16(BDMq!2I( z#&OM1)}-mZwY1%>dLCSh-PZXTaqbf2C(2ZGE2I_~VJ=8R!CeSgT8b1qV;t8Y%k4Lu zejT#EOR$`)nMY8DHpRvs?dDa+FvfD7&5n3pEOQm+HGO4kX=2T-qiVQe#_o&vigC6r z+jUPA)gf+&Edmqc_{CxehNPtcENW7gp##ghW0H2}4Lh{Dg3>h|*Ke-Gw`A4syKO#b zK3ftLq_&6B6rhwTEwoY=3RD7!&NG;e$wXbVwG9Vscsp>I^5)*2j_JpD8)bzl1ofeD zmcq&vgc1SiI0mSjYjK?1>rClSM~4Ot#yp8K%8LjMB_bGRo0zh5 zS?4uaCdtP#{qIq-o9Mza_1)FzXTWCm;|sY}>$ZwmUCX#G9dJPKNL1>zN77nsYbbG) zt!XF9Ttp=rlz8&VBfgM%s~)XE(&}4F4J?&%SAo=d%2AXYk~;+R-?;AvZ(>p%LZC3U zB%kuaJL7;7P(UCYpI=;K2EV_P+;z^hHVPB-6_5c*&u-oDc=~qczSjk4%2ce%1~Hq%q^h{aahO9tek<#!QlF1&gTIMw+ zU)JjCTY#lX>cSFII3$8Q=hr;`H52*1r$A~;Bq*okbD(4_lZ7c*1Pl^0>w%w4Ojdyq zvXTfVg#)^0Y1?2v@l})Ipa4Kz#1MB9B#j*#@H^OnMXVGEtHGsTuVk5(4l^2k-CiimqI-Bskju{N+HaFGeuPdGed zpTeMJSfwIKQ*ppTRt8iuN_s)=Qb<0-Bx8~9Q-_-~ya5~?aZNUfu$jVd6Xr7d$lpmWN#|cpf&=c$GYG5?gtR*UWDMOE* zzb(}cIP4UnPI5b7dsLs$w0(Klp=YHuQGDkC3iYm?Sk|D5MmnB_g2RR&Iqa5I5 zXB|8f$0Dw@J2k!Kvf5GRvXV(1XQXgJSs>>mYgakVI*(AmtUt4Pak2cFJud0#Wwsj zFk8VMQe*C*e@~^zLl4jmU0rbbgdmKQchHILF9HF zxUMa0l;&+b&8oHg8hpX6^{-wmdYiptkTtEaQka7}I zoQ$4uPb7VNW50U-t{MD~=6Kt9{$9LIco^j% z{DEANk%RYVj>9~2&Uwv1Ooou&6xxX-9uja4eH1zFF^qbBO-!4XQj9j=St<$cag6it z-`@cK>YudBPc(u)R{};thfvN2MihNL54YB!r%Pl^5%Z1+JBd3U-n$yfFxr6iGxl3wHLYL5o5Q)(2G--pR?rG8WOd%Lv+jdC<2i&A)Tu{Sx%NzZ^eSV|L`e zO{`Fo@kA7vEeQYld;j8$M;2wlQK}u-$uzYG96UW)dTC#0h>aOTc4HDy>-17W+u{8_ zQ>C}&4@%hL#g#VB>JUld=|y8lzp2w>)oNCop)sr>cL9N2%b-+t|3aS(D|e3^WQKB& zPr>7z-9L|BKfOez(=S#YAbhvnn(JtN^(=|UA-u7oy!N9{In<52D3Z(UL1p#nJV*1i z=IPBHgK)SC@4f3CHF)EeTmmS`5A(tcAq#~-D~(N+7y}U3S4M>MGF|Ahie*C@Yof2KUZ;nHkr6|WYW4}rS@&IYxxV->@KpEvfaLf+TG73ZBhE$eHCs^XqMAC_N~+=Ku6B=~zv`Z9 zJuy7)^SRs-x=JZM{Y25I(0%Q<9;94+N0Mwk6r?7h#Sg5TdIZ*eu{iDAcE91l5J<^=z2O+v$@&EF88__pVwt0gAH*`Vt$$T7$7*A{dG%n4aA zL%9YUS~P?>OMfd1M2-QOmcMO$mqFb~d?1gRO1*Q>u`qod8Y^bJ|J{qD8iv`za2Me) z0Nrj~(SGI+@;(nj@#FJNzgC-3jlTBYauKIuxk-qtn&8LKKqN5ru0&L`^_tUL<_a+qA z&t&T@oc7kDyXDfu2;?l|4RW!WN zz)sCoId;#;K9I$p%CHE=+3wnDU?aXu6E#(MBpS7#{hv|8mc^!w5>_R*KSF3;d(E>bvs26S=bf6i4r1 z3Y6S4(>^;7RFs!yVS)IX9`FK23+qm1Z{B^&NK~#__Q@)*T6;C_++Sh%;2jotH2Vh|h1zOB7?fB0#0tuQrA)!xOs$#}rJrtUFQGu5l*D#lCL zI1XbKe^u6uT;(na%`)s6BgYEgyw#MI@YkDww}3;?~D6a9&S(k=ulppNJQyAlAM_R2B>sq@j~OQ=jsQpynAfA z`9ae~^!;?8e_~0m!NX?uRd0KDW;}(gAiFUXiXQdT?eiXa_2UY@te@i&i^6caqRgtw z#ZxDVrnYfuv!E<~h;%s2M*Bk-{7Q|c+@#DKPU}H<`ZY(b#IJ7J!&4*1ES>gU;vf9u z=a)~P#d3xJG2NejTJ_Y!uN}Rb20`ND7er@!(&udEv}UD{2UVuKd`mlHX5WDJXjwSuXhQ`;Dy3iMcB6#Jqj&%Ug=B_75(%p5Mx% z8olooarv0 zPTwbIr^Blo15cm-A>VeBv}ARt5wL#>>(R$oY*-lM87Hvs$rKnQh`y75EpejZ`upjv zTkuLrb>+7>srl{EHj`%=cN_v%T84dVq}S#gD)BJ9A}ESXkpZwi^F|1JeR-L+j|F=6)oXH_>fPIQzVOsj_wM5M8^4!6efa*{hZIwL`Kk|is@~JKStTr` z@`bD}mTUrmaA&F%^UMo7)Q|I*%g-L?;4*y=ii_p$(Y&!Z;dFTh7#k$S7g$j3jD{}N ztcW7h5U*wlJ;xs&4kv3~o*_2Yl|x&gjM?Bu34B{qc})`ol(I-6$R6*ce!HReu|n~j zu=Eu7z~YN2!-J6Cu{UamFwFBHd?_S8I?Buj4e#Qobwob>+oJy8s6%#DsGsxy04h+_ zNA=YKD{In-hroQ19qUiya5N?>##BYwfmXbxq|I9y$)onHQ0|_#S9^bw`y+Xu>VQYe zX%1`F>SX-hMV^CPg5PPiqU4UEgN|d1zC74?7!SnoPfWUB6kh6|k%kmE)!L7P=s|)~ zA(Eh^9m{m?q&4WY|L6C@QA63Kb2g@(4(6h8^ij9lfscWmxoRfxn^}+)}Ob zIZ4~}@8iz1k~VC+X`yGcx->I)3n^Vqcq#F7w6d2ciyy3aX!A1g`n7kK0*=otp5wE5 ze4A&VY21-CbnC|u&xr!YcmDWbZ1=EDsy z0M*rUHM&!)459}ab20`ax79w{n^DBMd}@Mw38CG}^D^ZjX@7(`U7F>O7PNFO;N{Hk z{NoS;!D3?haQs3g#<%aFx7oajVMA@RX>Vs|{cftL@%`9W!hT%b_^+_Eo+zZ$i*3&G z>D_ONnTn*CZ&ryf9CW8E-4*z+HcnU-q4Yf^SdiU%NPv?|rTeDp#6+s0O?)NtA(1ZK zMiUdGyD9#vcy~Vb`B9dTv2B~x>#sJjM57o6PRS`jdeI?IXk9Vz+ZlZ_+^#G8} zoRk8>_H?q2)Y;#B*sBtj*Wz-oy3fXDqoANLi^6j_%uy4b;aN<(V0u^pz2}Q6N%@^| zav?LnXwlUXBkjR7Lhaq$3XipGoB6mOhImo<5XKL^zGjOYiXP=eGigBfu`Hx#ggHe+ z=bODD#b(kN78Gr7bQGJpP)+s5$5Cds7!qG#K7a)x;;Qq#{dc`r z_JLk%0C!X!x8)R*#a|cJU?~x?b;RETAZkLp7n;$-=h zSEa=5*#iD_$LdL9ex4w#p`dt8+TO1q|7wgCGvwlss60>b2B}LVjGwY8)Ih|R*H(@w zoFq<0QzfP{*%c|X##w>iy80fJnuaKN*Vg9PW~qS)?TdDOsfs=&sZ@etx~40YT}>Fe zTrPx$IEA_`l_X}YqRnN~2BS=X?etq0sShmq%aDLLZ`4E$VFmT#_d*9KocyC@fS661 zV7fa*uBtfNk1DYskbuiX zdi?F(@T3Xl%V{Jz7CiIQJX@QtYEaBSv0Yjo=Iw=d8OrZQ-4S{DPcTLqll9&gTHZ@F6{Im3%s zbN#4}(<-wW0TUz;^C6gVA1L#Tzwe*4`A{3|c{Vpi<6GZtj7Jv3J$WcNT#YFzqHamf ze5zC~+jeMZ^GPqm1d65W2nBJAl$%Sx6VYe_f>!)XWf0-y%?jY;?O*Pvdf%J$W(!fl zasxZ<;FvD@{QZ0`-JRL@Wzxj4AquVN3mU`P!5R2F{rA?_2npFqsbIndzQ1XHgWJSESl~JPM98 zyh>vV701{)(ka@dk=ck+8X(tqj_~>gO<1OPv@?GWRTIa9}Zj1?ur-?>gs8ju82`hZ%!5;J`kls8Qh)uDR#sN2Q`9 zDbHLjZotKj+@0&$?hP(rGnN+vpeQK*7uWD^ZN$QU$F7tc$e@4V)~W%PQyH z#kQsEB~~JtZs-#J{p}D_y~{I;B^B{!ZcT(RHA5c#EsU<5fFagM3C***B9o2Z{gI7=0P4tVr7yAw+wf-(>R@{&> zbiTR@)EG)K2NVS8#QrnuP;8ggJ^ZEFr+o=KOuBZFmVt&U^qRE1}I**=P3aV(Y55+V>hw%fqreb?eAPUPKHyEEI(G+feCAD5 zN>M&t9StCd*Qz~nz}QL4H19Q#N7aJ*-KT>l<+IM~^zkpekOVes`Mg-tm; z@NV1bTkJ8cXcZ1>7~1Za4r&2IFBK1@#`kSr1wWJX7lbnl%F;_+Uvv2UYiF+`rCJ@X zwG@=8_VDD_9J?!9_a#?a3s$;Ul#4)!7Wd$c!#RzHe?u^XhBbdcRMG9rycv&u|a>%MB_%(~gP%7g2hl|j9&-0%!A@M$b5 z8`BFo!8AnAK=T(<*@gO(1bLP>YTglg-bf8M^m{eW`u*9!xN$Gy_>iNh0i9)y>PPK$ zRn#TN)$7tZHMR58FmPsE6r#KE(_og;6 z2C(Kr6v#TIA>Oo@?OA3Py9oPVIX4^NvpPcYAH}8LuU-39O@$MGgd}X9l{ZW`WlqfnI=igOIU%~ zyzQN)l&pLhsUepIditOKnUbSi%aYa=ubf}x^tM;M?Xdmn=;KrLZJdjc*Zp>_2Tx!> z{@ebQm~QONfd9eq!`n{ONCHB*A1+lb-+W8FQXw?mJiJY{$}&60gHPf2SWrVKU-NCt zG)gf75gsCK2m9!)LDOHOn#WqqdUl%1CzU=QhN@Jx+EB{pOMc_W&B`{PkCB{k*6$4?(^6#&!cu{cc{o-hG#=W>B zx%n#8y~WQ0b8Hs=w-u>{Q{DMDs<~Cji!1D`g?RbL!+dY^+Tv$M`fk<&OR`^piM)^ z@}t`A>koL^!$3h7+cv`-ANby8E3spL;2j*^e<&6ho0qOC_xXH)0nb%37EQ zuf#xM-rEvTj!q`0@OKQi5yEBmmm@7_F2U4jNdEjI?UG@425FzFVs{ME*v^M`JGH8g zi?yk;@%(07wmv60g0D&|2-~zm6$fW2kP-O;ZYGHJGr54{)_@T#px#l!9se)3{?yX$`b;d3VQh6uT+I||k9l+~lz z)`B|6QL6B*tSZ!py^F^_zY5Le@ZGpemY$e=)f~VVlFk2<1dxh} z2n+XpQ*B2*HF5xE2)%vIb|{`2ue#puT8h86oiG(I4P1d7fep=IU0uB|#MUaCx0nNS zjVml#-mFWn=U-?iNVnzgHc%9#o1Yzgu8XUbMaoD(4(D zYbTtTWkA24KA7C3zCtCC|B)+iVk{V>QU|VOr_}!}Clr4Ueg5sbLhTN5sq6Vv!^Pmx zbG)Z<(t!h}+xj3Lbk6!=WyF|sW7q8b1u--++utWp%k}K#jXSTpQt7$-a;Bb|Oh0wG z1P}S6cwFV7;M>1amB+5O$~9l)R5GRRzo;@`?Y2|%hmo8Gt%>j>dtuwHMtd8Wq_6O| z0t_Cs?7fz4U(GzbTN8htM-A=i?6J}2q|8@{`rTVl^a6^<$x#S2ykc< z;$Q7i)^hp*QI1tm&YRblfA~~&vGpl;eYd~kXsYJ%k$cyD=kSn3++E}M94~rbnzlst zS$j7@tA*5oq)Yemvfz~pqV6z<=T7PXax9o=c?C$gm{N;=#$=B#dlL&^7*f0Nv6_dR zt)Md*UB2tx))VnJ4x+YG^Dx|e|}CBETXBob=^kSWT;e8ujeXo7Ff_*h4fy34>@M+$|yt9J+A+mPE8APl=-R8d7ge0o&6k1BA%wO2z(>=Wsn>&oho4%1 zY`YX(Z*E<3?OW|p8){#0deyP`YCK;6UuiOnq!oN+Y9a87YVo4s+|$5hhtJcpw|3S< z5L~7~F#MX0dFX9@V*(%XOmj)PFbkfSM388+bOaT7^e_M}` z@9_a&a)v*?#$Ivw)o`0cqY7lP`mZ(KXGuekszB@>v80G>f3mG5b*%&Fe%uT}V1y3^;`kGuI#?JZS+ds*>%=oa;- zyBFiPYzHR}g=p)Y8X-#-{~pZL3xkRmxM|S93U?(?Rheo0OLwiLr7R;oo4M_ld`wsr ztz}yR8W3@3=BTqWEd7IJkw&*lLN-RYOV&c~4H0nG!la#_en-~3f(^~lPvFZ^Fj$78 zHQ=Kv;Y^3;DOq_|eT7(15EEb1pUurq5P-L!0JXx-vygA=%%%&C1#pL4n1GGM2mEc( zUKD;kI-m;J2EDWSk^-FJD93lc@^i%G8;eSlxhZP#Egdchrx zR#t0Z&Ci7^6sL6-IGl;<4GUqOskOa~h6qYXp^mky2(kf7j6Y4EfnP5%{y+|{1U$O& zWSYnK`O(b|5i0W_0Ib`&d*})U4y>mySQI=trq(KO#I;UkpYPca&TK6}6;6aRdZuX0 z{_TJ~a%piC@8|k~QLF}flw~x;T$I3d{YLTw6dF@f;~OU-hXTDUwMR=JMMT_)O!QMJ z`E9c%W6fHI^N(odqc^^y$CLC(;2IpJeR2&zKl({$FWjk<%96d>sHjEbn@U-+llj!8 zbO|7Q^GI-@UmTahIV6XFQ1?c3ySNlqEhh-nbuEvLXAgLpS+MpxXYE@lC0)H(@hWU0 zk^d0t3YEs3C7Z%`=C&;Ms_UQ{vl#6@^ZbJnFcEpy)(~L5sXWilYjr6&A?9VH7k%JB zM!o8m4&_0Uxmi>R8gEXc+5o)N4PnFmmKg%{7haC)Iih0wAoULEVECynl*Ff(;-^_Z z!O}@To_@#ed3E-q)4RnpNlwi#uVc(`bhcAInf;hh5^EGnAwkHqZeatk zfzA47#mVT`;D=gSWG~Q~F8c1^W_!)1fjDGS_dR*qS0lR%!{=Bx$|Czy!vZZd6ulFB z{AJ(7W>IDRQ%@BNZMcVR1rbaqE27ksX3?*5VD2MwJeWK?vz3eK;HNLJc|ilw!lF?! z_>+YKEf~g@y*8=w|Bn^tAGuBcbN#tC&S!mAULFmqqHqoDo$U;YeM>}C!b zuBDI~0!!tEG?N4XeG+@G zBW11c`(JsfFa6|Hon}KdJxRI1s2tI{bJxo_AXN8;@_?DBHk~Rhd{EdS+EM|@m?$^0 zzyifho-mfQzcMbslovN(N^^g?+3Z-*GVf9AtLupR{0or_hu=2y?52N!TF-S1wOHoc>nL74kNvD>eiL%sgD2^HqM8%>Y8F)#6vii=N?3QAGvqaU@?pZQ&mH-3|z1;{H?VvC+=By z8-4B#%Y<5>oN*Nr^h4uF z765=>G?;1J2shykw_rOnXH-%F8%>V$5c41b^ay1x-tl*|FH)GtBE}MAK(F< zcbLF$4bE%vVECD3`r04=bhA6hd>>ckh$RU`a?!sEAi{o78XH+mHSWCa(uM-o73Ih6aPn}BJ(7y zcDF&65hV2dfwrHHWxwL5yXI)mc$t`z!U+Gd-|Q|$BaMv>Wm&5g&L|uowlQN2tLGOQ zJ*pIZx_y6m*c8puov0l>wwzgj9F$3UE<5Pw;Di2Ca4?hE6I9ytu%##zg$A6*c8&%g zzhb)zh$qKY@USIWrS*tc!!tHM-c#6q?gIcZ17bWTT+9}f>k%#QF58Almwo=0ZZo^= zZR`yeBK%2Y+W`+y)S2UixOt6g!rv`{1C9*G(D<0|4P|5ZNC0#*8u{jPV+qP z=Eq;u%n6_+i*ysY3P5JyW)@dOiA|6Ay)SSVsegKB3GS7*4REoQeb=LuuL^E+{qOtt zHof_G8WKT|)V|1BwOr(eJ&H8G`+U!l=elxt?!Ev;Mpvr}t;y#Rd37!d9R<`9TB}Nv zDN|^sy`bNcDBXBLfb)FMcHm6p!_rsc2z+p+B0scHEt+8m9xU4$o<+V^x8i1`|3K54 zSkz7~#?V8>!cNzU8>_cWa^h^ccUosg0mBxU>^rzf`+i3@JZ95Y!<;4zMZ`O@5Q}be z*Ix#<4I9{!?`+|+M5NI|cB1ZbJ8=cSwU@6qyn94xE2_DVM^7@)LSNS0*G*qs72myyhH^twf3oOkz1b$7*Y7=M{{i*k7YE^+yQsjUTiOM{T{u~f! zxBhyZqEHSTZ^%M}$Q4Lg&|@WyvWq2*JE$|WOT2i~ zFA4>!itb!asW^I*(g;&)_U%KO1iX=;sO^5uB=2oG-)0-LwRK2@_|LxD%IZx@z6fe_ zcr3)InM-qllVLZJB=fI0hb}WSbD;t4Si7EY)mPPMsis2N0T?1-)>#|*opWC7MUEb z!_y55y*r8$?)SS>BFVhOd|<3b^Tu!(b?l9Rfl}gxT^8*veK+}|>b6M3tSkn;7$ev3 z}os_nL9-QC;BCpTT{0 z4-f5RlNvcazRmIOaV*M5rUD9P)?iuToYtIFyOI}I^>Q=)B#FIc!iJSucWZ0QLW%_v zJtp(c>{q>D!&|@Tkf$eSUgWtn0k^h6eRraHkv*V-oAxKtFEwNBCH94NgF#>`aHORt z!N}5Cp-G932?rk1@yN#{v&foTh!thnUOC#hN(7g%C&d7eN{=%@omvI`bvFX^xND5i zkWH2YGOgR5;NjJeS514S$()u-U00Zb~5hfL*ndoz>eLL*vXbe(F(pDb?>0s&!@2RJE#p|;?CyCMTO>3jS4IX7lZrj! zXM886P|*R*62l3AS4Ag7qFGV7ESVzF^?;E-9lpoL zefNh<&&WpI!&@z5)%Ae*^MLY-yoj+DC_**hVXtZ%`GbqXw zbuOkMBs3QIyebEXxFY{(UR3)yE{pvlcB|{}e7NK+6~@XMSSu|k;kjO`E!PSIvHKZn zEMI4E@#5xN>E!*c&dUP|(%_5$03ea)p{|?5I|1;I9DLRx7@iQu#OIanVua)Qspt-@yFG ziy4;6cxMLL?zSW$2EjAG|=zVxl0>L6lZ9%Vn~GPZ+mHrp-pht!t&4 z{3}_+%+D%TnnrF_b<1$E_VX`s)Lg5$UO#yhE+*Ii>Xw;LSQOXRlb0QlMX!52102J> z&~mKZbEwS?bBv!};6bWuL-5X~x8vW+UX6*kAR_Io>o+T_^LrAePK61=zQUE4Qu0fd zTg@C&VVbKYIOl?7vX%00wE}oIuwqsXN}aD5Y(SzAkE4atw>aXlSO{EHh!Ec`^WYK?1}6ndUCBz*XBSN2c##G;@OR?qTKY&o{j2kf&wop6r*6C^wmWAOiHq3r3hwV%^c@M{99US^h*1n9)=AFPKwf9ikSg0*UR(f6 z#vYxomC+RU@!WJYom74nUut^oldxr4O!X!qo^eWeouz9PS*RQ1-z5Vd`&{lx$l|sE z3&d1&61Db!|J7;oZpO0f(s$6^(|WQTGW!Ov6cNU>37uHKG@Lra>E7-e$~o@GHgQv;1(&da0F)AlPX3xK+_C*ro)@1c8c$ z`vux;VzDGEBpn*sNl(K;!`&6OFYB@bVj(XEP4MktHG)xfp|j=Bk00Lu9L|}oo@jG; z_z<0Npue|rtq9xLGFj|HYOK_+8n!^2LoV*v3nv!`-7u&sdtqI+Jn@>kGyKRHOp(4J zd6>W}q}ZZDp~jFI^4-|kgMBBjE2{s!x22F1>)X~?5a+C`_}CO^Xd31kyJ5@!4Y4jM zL~WpiXVgotQuvQcDW(15aN2ga*cgt0iN5(5_{5|9tKo(6OZDalk7L(j#;Gm+ej<9y ztZ#edXVV07+XMrD2ESn@(Hgh!2dq{~+f+FlRN9(4S8GuILt~a$$7^I`7>8pS&49Rc zf6?_*M!YRN`)45Z-!1p0t&H)goP5s~aG3)xn>rnzPuo+Ea!J0*DE<0p4 zJdR~WT@R^wZJ){=H<)on>!<%6$E+G)@6jsif#QE~D2O6%5$!-fhI!*J8^~W` zO+jYemz9-GOCgt7xxF}xsire_A_=3HO-k=7ba9)6lvP%RkFtHbjK2ytVKTR(qrX)e zAV5!GXdc91(A!E(MST+MvRm%&>Lb!VFS9XoL@i~V>u7cd_kl^8mlkND;*Zvt$)W#9 zy)AP%{{#^)P%Y;%q*7J_4aC?oQPx{PC;XbXUQ6xIrW__24-QIw+`=nTb$$NRV6UoR zZ^#%gbq8_F4h}D7@lwe0SGkco)?E^n>}Ry4F2WA)ZE+QiWW}@! zYJA*D)8uP$R(b8*^p0P;#kZ70Bl70?&&X=+y8Kuod_ZREv*=^Amw}odF&K?WcqT;x z$b1Re)<~i3#*G#YVIAgq7#@H!mB-0>esJ4>r^3XH$%fiQx@b@mPv8}4aT z13U*AgC9^|X%s`v&7yC;-soH^W*S;1IM zkP=1ovbsq{Se8EiBPKpkeS(}HhLphoiOPj$QacUTTNhmI9_M>%INC#_FkteK8&Eiw z&W(;`-E;nV!w53-T;`MX_)wmcaZvqg&3&9@G`Ru{+!T>xk}UOq*gg9;VLp3qfBkHb zE#!MS)AJDBO<4)lA)OyTDZIXScWTy0-_vnP^HcL1_)9e<7}`x?ySRb(ClU*U`l&FL zqUG7@wNzZbx~rWMOUiV{o6ydCC8Y|wgG@YQ^kO(u)&woq_Di4IXsf}3&KFud15!~g zQnQl!K{fFT?~pza0F5B05P=z*(SXipDMiQlERKI>RQ)Gv5+SBZ)s%-FpH^$`c%TJk zVFPH^&B5jCn8w7s`z)ywysUZ!p{j)v_Cej7W-utoX%QIjMjvg@;#aTlocKUhPvBzI z=s;jLWw~Wmg>TsG$vabdX9N(=x)>n)?QgOd`HD$1LNwX8LoiX;9AigpNVt6##i~Hn zBbh~72R@H^5QQ{Io?<1NfK9xmoU2?OC%-sJ%RZIxVts)F&>_El;H^(vd#w8Y-!c9_ zm6h89v-57sqGg@N3V%AER1UqBK^VBL(@~bR+=A2!7{OLrE2n3w>3e@5;2CWNH%3zc5o!l3wLLcz;#8UWr5tCic*{-j0yN2hk2@|2n>7&Dt}w`GBdISTV$@!x*^m{AOsNsv z+{}DABeQ_D6@ln*zj@uuEcb_hgPyKv_+dV+eZc+>xX0PFnqjRk&DvCl)X$v7)kJv0m? z0|hIojsn&R%dJaEJ>xG|9e3u5Ts%;s8bQaw`qct}JJP|mu_tXr+*0@#GY?|td39Q4 zgNX4{<6P)NGKe<5Xh##_$Gg3H9S@8x=Dnnt@wcLil%wiO!Xgl(h_itz>XBWNQ*jSJ zg9vn)wG$*KxJ!Vq*Hw5x>Q}Sv)26eBpV`4}Q6SFH26#W5MvsdR-2$*kqA@|JOeW`k z(C8m)Wxtw)EFx#oqscn-l)5Oz@Y5i_M{#c&?9FP8naoEP?EW~I!VitXqPkJ=!S(#~ zke6v{^FcKj&f*ff3G$yV*cc1TZs$2~U)Z$Uk*5#D$bmUEo|a6N2DVp|LORVv4OTJ_ z#eUby?0DYco;0RyZ#ZL!?-6|g%*TTR>(43GreNzDXb+yZ9^iwXM?H%cChR-b$Vyq2 zLN`r_y28GE5R;u|Zkd!~!RCI_Y=M)wg6U(qHoVxn@uHiS_0A1a#}H5!mukCUE$<~9e&MU+=D8tT{GTV zNlScKU8|%kOw>Mxl7M+~X>)N4sd-92e%iGZVQ0sJK&JaF&k5ED|8eEcK4$EqvRYZX zeEEw(0l*#``dy-X0b2WWDL;Xa8sPyC-yNteu!CeuYjf$)8=Y_|8OV7m`(e>o>ZII# z%WZXAG^YA(95)sKy7HqGxXlD<~sBKy^&#{hzQ;UL4eQ%+%}pC=?UZd(|n_BJZww zyu?+@C=%+YHqXI|cm#Z1XL<(!W?XPi8#rC~9X@?Lm<=S#Q2&*Ci7kw+TKvSf%K98T z+cDj!>V(tSBb|-|qrvC7FWI9SBCx?4=ZE=RmB;7%*raqBAoDM>AEQ-RW=x1}N3)2B zU@ZfGrLM8Gc0o}HXkGhcmxhrld8Z_l3aNnau_Pb;eev$Jdb;MBJs>GbN%wMeG8my2 zrMBjM&6K!koBQ(z^huEjw;_tYuAlxK5t*uB@aN?kw5lRA)m7Y2O59JZX`i2?Wog1< zig^H%PgK%k-wRg?f46VVcTW>b_6)+@G;V=53O*@*!gW-DYsnWRb|g{fghs zT{Mvm>%a+b}jdi&Nad)(th_!x&JQAvdY{GhC!oH zodf+s0N0Z+k5qr|x7D^&KsQ3hxD{oE1lQ(G4II2t7l^g7JIJS@YI=h#RJUGLTZS5!lVktC+S<{LcfWes7BrT+MnTW}* zYdxF<=Q%u&&>Cr+GY+t-^t80YMw^in$W6sacc?r!8d0D6*Na2uK}*B8p>KXB$t-AG z$$Lvt<`DD-R&%0>L}xuy6h4h)ds5|%i~4P6lJ&}rOsl96l@T)UZf}A$sLsg{yx&ed zW;5u8av!(;>}S83WK%YP>bHD-tX7)OOKQyQl&b4peS0Hw+tTOb>hXGlgstruTUE{k z)cBW|4-5m1{!!crWuXsW)$lX+W|oZw*N|QR9{mbWn&Fz&ubMpdxZIliQ9;hqe~vI` zTEVaiB4KA!P!=eenMdoqUc_mg1x1F|4vEh;B^XCLhXEs6`v3sQBCX9ceaIf>Qv8gL z|07o`=e#eNd)PR@6Zd5$<|nWf^cPC72ZLeIXV9RTkymvyug0t$+_ounJmM z?PQbvm~LQ7RyPi+85m?x+T0i?%7$<>Xk=t$s(bb;NS<#C1)>L_l6VlOTFeL$VRl3Xe7;niDEoa!tM3}(ZbNgJ%{$L?6Kj@@#!ec&*@-Wt61cuZ>8#I162 z0)v^s4gor0Qu#zqt!XCziBILPw9*`(CGl-Mt{hV+=Q}59c8Vg4j-s9vg<1}Zp0&2Q zpqq0P(!aHT5+yU3=-+BfAYDpVnBoVyxk`IWKUqBCPi|>N`nAtPE0Hw}Dl7yKqK?tJ z1y0aFfg)oMH0D8c%9xAQ*Qx3($>t<_O+EH_S%QB?%@9JLy`T9Vytvppxk}d*In7Hl zlBvHGs(tm5#_pK^((-B@xi|P&g2Wm6xvBCdC#jm-vJ2qxJj?O5NvB+v5Aj=XKpwtf z$%amVTn#f874`D0gc8jeO&R^oQ}yEmT^{D;4}NSoM0w@ni!p5eif(E-!%6-aKujqQ zNkb~%nqQ}xltlu6?<|Fo9_va4JUX58?$KGkKUq5zWmptPGTF#r3V+`P`K8W}f8c-n zSxEL-WYx~jbp!{L;pGlj8zo;1000;X3NEr{+`jgw^%F6E)bCbOQ_f}oY3Wix2ARU2 zkukV|@dNbhJYx{PWsvEWE~=o;sDIyIzWw0*^UHDKq?`ifEfz1Nhq&e&` z+Fav8HH1~e)C!XTM#DQ_sr~hJHhuLKAGMwBKs}5 zf(&*r9_AG81%N=IfJZRxLul74lle_7u_T1`P7jC*hz8N~XraU-KG*KNQP@VuJu4+OMLl~BHiPsK`)xL6BFrx zXx&}NfBvs*MxguuYD)~N%q6z*n;eT~DTgNtpl$X_I10~A7;qp*}toq4-O4=gf zY7ihG;6{N}i;fe|GIf*QK&&q2^~-|DBl+dsQG|+jWlz++yw?)eV1R(0RofL^e}~=A z07!XS@%tE{V#hjYq7o|-9)%%-3v%>=2hM_-;D~gcmX*Le_zy55_pvb-h^km5ESK8~bWyJpPy@`8fI6&O!&CI^Tay>N7>>Oc1AugmV<;LP@*} zvAYDn@J*ga3HE*i`tFJ_G@5mVXq|dE6X1V(+ULE)7RVu|0nK;gTW^j)@1&dgB5vvl z5^WAgrQ@#_(Wip*>HHU@2&R7FTK<;*EPTY_Vs?_K86so<`<{#4(PJNR8Mk;C6gWc< z^)cDjP;oQtJQ28O^2i~Vd+Vv*ygl=M!10mDVi33G@y4hT)7G_nYs+21yka3n4cH0qeKHt#L6#FI5Re~z z>cMXd_w;)|^puqam$QZfq7~xUOK%_>FlLD4m{~YMumF zn^mbeJigMHJzH$=u=WT~6 zKH%Nq^7Sn7Jg9#`f5#X8o7=i&6GbRlV>eiYIUnD(C`b%S1*JvzNSO6a&)t2$c7s8T z$eVVQk>;gUq<{+Z3gw@1 z3IOWyKFlMEQ!bD7=NJ9bf%CiQii}lJRTr5W1GiVx+)==R!NJZkM9b^J2>{0^93ptH zllIg_y=_~g!!NwAEV2S1DJbd5M4r8uwqK_Wqyq4hGEb+S>JT@&%tR#`xX;^j4ps0K-dwcu8Ta z2I3Y2e(*-8|04)Ol%`{Q?i!|S!z^Z9t(@3)(DN8zAiwxqLJ zzr6qC_|t#@oQ0OU(1zFYe}ZPGY*RT;fc{sA=3*l%XczjJHzc~A;>x)=9k)+wt8AF zT*rBgY3@^^jKkM~(&%1)8!=7RT-g-kzqs4%%fshq?(Xtbp}-&`DX;viomQB!Uj**u zE{^_CG%2gZPox<13jRBCb$)uciaLoTC1+Yj2;$>odc`xIyF<2Jlx+Z;Kup5j5>@!X&9GDU%((Mh94*Lx6mdC;*LhcBT?KO*Pr- zQ%d!pY=bt9^^XVC|B@Fkm);4PI=xg{1d@SR{Jl3WR}2sBns|g3buiNaOpyq$9zMA43LSND^#ZXJ znvu!wb2UCe4I%&R^l?&eH1~jZDqDOn#nNvo=;rEveu2B4p%ubv@K?BqDoU)E&tk#= zR+G{cOE6~P_@Gf}EnQ6eR>*4u049qtS(`5XEJL-h+9vrp87C%i0+k`@Mc$Tqad=-d zz+jIV6lZ+D3fbopH<_eR*1dH&VoK+SDsz->DdU9(vsCa07F&%s^o6X6<)T;eis{VOuM^0QGsJx!3 z(m#xVEqC42D1O40%W-M7AZ}7iRV|3-lfo~$fItwOtMVC zFCsGb7w*odkl!A&MULl?-S(v*O!_b>+4^*##K^JeACu!JcfSz~Z>uyLar(B+1sK}C zra~7a4~WX);s3e#JN6IXD{=@QIEc-AUrJRtT~n;tY2(2UAQEDVu}v-kJ;t_ybqIW~ ztM3gy3_``?Q80_FtS!X);F0ZXs(ih3Z%oC<|K8Wqn#}sU+0fbipX=X}VLL8jmaF{s z947msVBx%MxF7h>MN2LsA4{h@$-ucnu%^@|QWNv8Hcm}&%gRRs`vz^~-}LagKGvD+ zlL(V9#jlp1`g*5S%fgC09@@5w&|?gIL#N-4FsHCm z3nTzaQ?u)`9#*FZKO3GGpKVv$9XlPP>p#BCP=|LhS`vhEBuw2wmxZp&>o;K}UR&;v z@*jJp-!5?)o=*8TcS7lfmkX&A0#wYSk_){vLrLq>eYOR{X(Zh)W zj3Yjk<`g<#1F`13jt7JA7Bi#q3&Fs)W6GBSzfgQcD~awRs{7GL-F3mu>qI zXY>!?(UV4!d_QNTKV=Ew-7fi6{$EsYZqWB$nhj7o&r)zsw0`!+dDKTIN_GH^pRB~- ziB)nii-4?vhVRdV%$9B{=ez?Y(hOd_x=c@8>q zogTOc)htu}%P#v|S^PJ2eV1RGk^b*jec3r^2i!PA*@sDKOML3OP=uS)a>oK%f+19^B*30Sm>ZyrzSv^?B& zsd3=6wWOjHQyHROqBc;#2N>t8CFg}*fu}w658r2;+W6r8R~T$(1=cio8x$cDerE72 z?z|y}`nRit22L=a=#xn@3dy1yb^9@AwV^k02hO03pi%8Rp3vfbp^k|S(CH=uJboc= zPrdcS#fPSdM%Zw~4v1_kK;Wm=#dYY%!{aycCgpE0J54B!B3nax@AFl^K5}!o{;2Ea z_*SMviP--n`cFG8CGjA=rLQO{RvFK@lvUp75*GB%p`A zKb@VGWTfwYQj0jHvNcUe=O*|c2MXeY z2+PZw8q=~^gWY~4mQ}O1&Yz5#<}WYr_*6wPoGt*gZ-~O{X2)0~*(D3AA4af0>BiJ> z1c$vpg{#)Q+<{pgBz4<=*8e3+EkG>;xQV?K@svPvCnY%`hap;862xcU0*48d5;pfY zNxqj2eR!fxi6mfh!q^UBfM3qx!nj`vXC8l4Hp?q}bSyiTqdL<(d-#Il0kwLJ+t7m3 z%+DWYlOvI?73I=Iin)7}d@+C{P%+7#4L>P40t?zNa&)%Ia6)1)@es_Yc=Zm zMaLjSTNYut7ZtBOCX8d5%}>G#*&-1#rK!y!i5NN6YfWpb9H)MX-be^(ZEgD>;K`F4 zX6%AZe^D@1ekdB5=e*}Ijjxf)mW%C_EVgU55!>*84S(nlU~zy;CBl5VX+_>M;^xce zE2SIJi$Xr*`Bqa>F($A0%IA!V^AtDPabTNyLSAwHYNzEE=EakHx*JY;CzWfPek0fJ z4%ClO!Ob!poX~D&%Hl@D#vYr_A5-6|?gv2Kmq`{zP-oik1TCFsDgU_|x|A-@p;Qnt zoBsotmwbMWYC?3#DI-Fv!%eDZ>H(d1*i!#MR|a-118J4IvhsJka|mqb#ro=rfr$V@ zYNIcGPIsW-@t5>i)N4em=j9mDKF;)zdKnqW2dfmaCAcO0TfnIt!Lk(YSRaN zngcC9uEW__XcLCJc?vnbkfLwc1oU@c!;BpQvJ}NuqiXJ_Uwfp+nRqeQX&&OsQ9!mQ z(=mjA2-Wl8Mhqcr`%zDW@_jh{7!SLi~>TQg-%S{|#Sw z=I`Uzb|_O8`i$eIut$&4jh!K24Zk3(njT&R`0RPE$R_C5R|8hYK6&*CcXn8RDVD~7 z=u{~`jn34_#!i|{o@_(Xve>_9XpB?0qW*3s^t=^ zB2Y}cY7K2Q7t?KS=6jKWb_tYmJJ!aA{os*Rb&+~nSD=PU&{;hMa44jH&cZVw?87Yr z^k&}E_DeoFlZzE`^Kp!mOT3t=V9f+9HZ0#3PYEsA8tY)}9J@9G{swbt40i zF{1*GPf&`H400Mmkd$3#=v3WgOReEU8Q%-;UBAG8cc)wQ#Jz8NqFz+obLnyy5PjuP zw{rg6T(b9`!ABt`erVE7$2Blm<^tJ8g*4_dVY&_>p~;cLM%?IgQ5TkWo9+(*oJ z!d2@qksdR9XDNtEXMH($=x0*fTPKLIRb9T-;Gw(a|1z%I>`J_LlaCE;cRym)XX6;` zC#?me4W~NjOi<(12+AW~>DaT9&Rd}&Bn_l&a0pPIB`Pq0Pywe=N){;|BUKH@ufc}< z=)=B8n-NQk0wv?Hu>E=ey1D*78F$+-k5jw1CqHz8nhK2&B z>!Fh-*K;@_9Kj_9&o~_vl~!Zu4!OS$t+xUcrzyMb=k?4hPK_bDXkw?#lJ^-4BN3+j zi9R|lf$b9fS>sVKDxCsnM}!Z-Hxv)svR>_F)ttrw-$ctg)IC%Vl?1&cl-ir7UsX<)AYK=i948+;owFiM3uOYYu7l0T(*{~vso|}Q> z_3sptbg(*DIFC(nHZ~CeoRz^_>bJ7Vw1Z@Xv>=DLSe_2~La@pSeXQV{;*SztAX&Le ziz_a0UCe+@*_UG>Z&EnxZ*;I-(xD&|zxk!++X{W-EY{oc{Pa%>z<@7Es*?EW*#PHQ zI1zijihNE7jT#rgNSZFwli3d&vIh!kIOzm}( ze;N6A8GXk5?&WrWNkiYQo5LQH^mH7w8H0gnYNEt-{+ajQGS_eZygud7%@vgJzak_` z7Q#PPlXda@!jUk?GL8-w2w*-H_y?c@MU(hm@T8vG(QAtqH%hUw^HEy3JVQEi4fEaFCR2^H0)sPmFd&K?y zMdgDZI>TkTHG4}H9E;!5SPt~7Zj7+-^%s^akBDxtcrm^q8_k%b!yX9S5DWbI)UbyT zlpvgLv|stxdo3FYdOXkw=PTL?^_XVSd4mIdma%7=VpPoJ$w>3h`{sB2%0Y+>a`08m zhIcTpd;BPw>f6JK<@g@W-uF8euRA`Wvr|Q-`c&{esD*-sokQ22GRvRHx<_2) zo725CP&!g#h}6{TeB-g_eC6Aan;mf?rp{3v@-d_=Ug{rZDu4dZ5xLW;ujSqFyC->l zs3qN02Fx60EPX$fzxl00v;P72Iz=DPlFobXh2)6L;a%oZdnwZavDnnEQ}#42S#o3G z!G}vI=@Dp9|joZk2lLp`cIWRex*==1}rQ(!HU9GMN23E%Vbp{n@h z5c{N2D!f7;S+4sT!Jx|MitkMJi#>Mz{r3BhkKvTr_Sc#|@!|bpeMlrvu!@;|cx~If zPvFkJyj&GmSWe)imcywlO8)~iHhldvh|LMUm+VeFfqNXPOMfK!AAn`IQ%@YgnwRlZ-Hz};cW}pz5Z$^p<(i02eU`N_uuJsyyLp{FV-VYfp8t_ zq}IxG(%F*;zKVndhO@mE$|hqv9)F(U+PZxhcT zwo$#qXLuXIN8lE_Y+4#GS1Ex8yK>rrn1*)zN9i=>`0B&cL|00xe@&49LD% zsuwfnZfn3D!j$Y(W9LX~%Zq72*k5t&S3-kClPthjj6byag}1ZQ*fQUjL{TH=3 z5I!RFvo~MraXoWngofl6njc@IVxY_3GW`q-wV9(iFCp5UgPpU$p&Kt^vS%uu9$~_0 zbvXEq?smVR(074zGxW2kTRu5v{7O1LKVY$t7}p8knE&YpvH74NgC_v87~w3cUVT>5 z^u2?j&T46j7&tsgFFIkhNiTW1WTo=q-mQF^f@J-7Vyt*&eM|0>vuHif<>J$jUUBBD z3$+?O)wLj_-sge=su@5~^wjk&+9F-;h@PHN-c(p~LqmzD>7zgAxS2nKq`7*vFD2MkZp_Nug-JctMPyV?7S`%Tx@Gpi zCrvgFS{#1Wg^MznMvRMKadRWcdaEzjK0J7D^rjoXn5f{|fl>({`970QL(#08#MY7C;C4qo-)^wQ;7%!Og|CKY`T3=#=8E0i{`lu_$j2cT;jeZp`$dn0Ocx5d-g`WC zk0H~?m6WU07Dor2GR)$*<1ZJ4Hu`OLNokl0etsqC^!XmsM?VM>k1xhg330aU01OP| zDrW6%y9mg^+7_1}KQaKWO{lCv_3IvUCeDgRP;d9smM9S?Yg~0puT~UnQBgk>7Vu5W z(Y72qiJNYr=n7x+-kj16&%fN|3IT^!*4$&=sFp=P@apitWogH##e zUo_B8GKtJ{XW>j0_k0eq)5kY8W9q>PlepO{Nx^YDM&G_T3h8pDfK^#zUwg+m(r4OS{YN9g4d4p!hlWSbg<0Iea43Dko5oCL4tk0^2{%m+Y7^GDuTmvy*@R zlxD?O{w4`BlkGh8Cw*uTsT2SrmLVV(l=}t0T{JQppNHgw?%QMlbg9Xz7F*NzC*+y$ zasAS`rHVW<%t5;q*g5mu^XrX{6X45pt#u4_$(V zHm8qRP2554q=R}W(-ahgB-Rp-yz0jXo~kTi@ODv$?ukR1c0)#Ln_`w9KdTCy%`qRY z%|X*43)~##TW5KQ_zx~N@y%sE)3*OTHF=t*+Bd8&TH==%;UJ{xZ-%spm!H$zcOHQR z#b3&ZJYC=}G8$y(q(#A?{E8?bcYhXe#;Z}i)U|{CHuGFaGM_>6vOpC-N$xF~8$o$& z??3wG-Z7w*WcPy(xiJqg4jM2ZIl74?o&3T70dA>k99bH+G6Ed=hu;qx88{}bqoQBLph*AXO9Fhk0S`0JAm&3)p`5^_QmXC#^AI; z7*3T=Sw@IYKy>dN59+S!dwsx}s@0)8r5Ae|8?v6PMZBRIalT!ka5<&Q0~4Q=mj)&rh8FEKHN*X53(bXwbE@EH+b?TPP^MS8||!*)hCkAHOn5C1OWUJqMLhbY!^oSO^b?f);`{k zM=68ggbw-s^t*`T$Bt%@bZroZ)hcEdTvWJR;%hNvPg&A^A!l5`SALGd_wZcIXH@sqy~xW167WAfkhF(Q>hHemC` z*4ANn)5>0hnBYPTsjD zjufAW<5Y;Oof4IzXQqr{S!5TH&9;A2++*fTqcaJ|s5Y+36a+>vp3Gy%X`I^$JpZbC zD)))+KD3htci00+MJ*?lxsDm{-(eb2qpEFCmsObqbPdEEDLb@8*<{y_dk z&|c$}FNQw%LZ~)gFBD`+=_9TxKsUDX$k1TW!Lhe{<#cb>&%)FThi#DsfpTqh(LCG1 zTWk40=R#v6FNwY>ec>*Y=KE7N*o{JQ^P;8q7*i1&0|i+6DcRa%KCg@Rp}>6U7Pyu^ zPW6g^nwo`@jzHDeqTLYg*K^l0DGs@h-Y=_|lW}6?0I<1z%uOrvTPXIUrxKPSYN#Z| z`Yk9`qJ4p2rsptXfHlnEn`Qj|OPBFS{!~!$lLg-%1{EMf&)g4aJl*aXVdEQ4Vij__ z!W6*2$?^@81c`9@{2`j<8MBdy;l&f-z1>IrDpIAGlm9k2P2OH{O<;y{K*yUC6z8XOeB!4{TE{mE7you)Cji7Ue8Yp+h*^ulah^HWa8=|9#-8B27=r-o*% zV;^cy(Y$Ccaee=PJF;H7t*4(W5Xzn_f7JlRQ4X0y}Y=?aT^{lurFRuO0 zY)(+`8Q1I&NM1lu>suM(#D=^v&x9ShGo?`4aR3IaRA~Mi^f(88`E^#dlTynA1;+cB z|3aX$2!IL|GlsQPRPFp5H}UE^cjA66J-?1FG8k8s$PC1`*(WMXp+s*Tx6Z!Jp!hhG z0g8F2j7?_Re?6?4Y02jnQc;m30rjhE@yfO*XX;+>ql3C!{7voV8#wi2s{<`#sz;Ee z=k#UusW{C^EkbYTm5YS7?an^wytE~m9HrN3GM$sJeob;s{?!f6yI|(t;o3~u^otx2 z002{XofDhkqqke}57F3d-Zw26kJ*LJ@lt-;>VCo)Xp+m@oRtRV~`9cv=uS_6eeV*_AwH`lU7wbmacFfhy83c{>3jdiWeLc3c z1PXsa%p6q2tCOF8g$tDZvp=0|0mUC{ZgKUh@B_u)8X!oqn0Gutts>e6y9s;K2dw z7745CANv(kVeu-7KnwxDBBR-JMc3-;nX8uH3dqFY=Jw$m7E0|KFBZuzGGB;mGYi>A zHp{p(9j;a(nA;K+c?k1-!2h5Lo<1kc#I(zJercMQ1@N zseZLRzQkQd6oo!aJSIzM?(G+>N3Umj1STXo%b7nO?1A8ko9%N_m+LM4{ohAdc*JL| zi!Z!SiwZr^bigb=Bd@N0E6Omd4XA{{A{lIpXoPs$B`J3OPNtJb1z5$L9?4Bm?a-Y> zf;Zdy54s(at@-*ibzEjZ>fuVncS;hia$s*tu}76))jY0?EHazi3}3}$J4c+~9-b%@ z5fG}>U$vPx(mL1zwU@Nsd*CGH-TsQs9s3w(LeG5%n^uU@KMMv5hnB}T^qfD}mk!`5 z7YdH)L#N4Sf13=*x$Ylh$!qmQ5YH%_OP`S&nRcE2hKlT?qL0l2@RJ8MRVU+UXHV-N zACJ}|SpVA^)024K=jEGB1mZ@RVuyu-{A8U)n1hC?h)FUJyq)|*NgT_mPMPVrKVz50 z|5_Ba_j2;F_6tEMNz6u7Qf}#s8hw2lP31v46=1L&6=2DLNEJPQmx1UdEye2TtLCOa znmuU>9*u06nI?3Ws5@R>yU}VR?=FCSrG)`?NU3>)zwAwTOByBrihqW*NHZ)kyXfF) zh*OgqpyPJRFkS<<_rp&=Ef_R|o&g0j4vmBX#bY0WpZNMCjPg|`vMCUvLteozIr*i` z<+|FM%RNT^CyryVo zNc_(95f_!{>~T!+z1%-x7Vih{|4_b27Q{;rDAqSfM)Ji=lxAF~v$+k+**;E^*G_!N z!z)WA0LOfs9l9zch8P(ngZMOMq^`&*NA)?nDetxe68X4OtG!f=N{n5>`wCjb1vs0( z*Sh`Y8UW2)@?^-?mk$NHuuLyBbs-~{&lz?qJ||~3s#&5i|6$7IM;A;jM}5*VK}Ftj zb3do&aH3_+auJriaBU$2G2dP{CXAhtcDT)Ud?Iz(D^cKr#L?uG4U@4N8$7=@M>Pk@ zc7_m^H9IA&t^2EjBR!{m=qfoX_FU(OHWYRM!!Jd9t~;E%H?2OJ8x2x6J;bHRr_2BG zJQ&7mC=KE7Sa9iG3v^YDb3lHPWoIaRl-@sP5MX9y)HxF+KAT2k0z2KNkQnf|kV;%2 zta~_mRloT5Ptgf%6wa*^b@{nin3(ABq3lfMk8uBXDJ>bL9#9U&jZ0S69Kzdwl|E+W zI+eE8ec-V80)_`MwsVHl{^-L)-|lsr>!Ke}Srjdu!laCJDWgDEf65Pe?@}3B1OS=f za(`W}b$Y4yC#ELdGQS2mkh&l*1D|I{3Vufzt}`ATl%g84sg%w(hX`VO=+MFMXAWTp zji?K6w^4L(uH-dxGog7lfpsP1aCIMl+T&A~^kOs4%@1E6%?p!Dn$8&=e#^qVPDBIG z>3Y)Ap}R7i6jDl@a<$K)cIAicU^`~$yBQZ$mw+sr*aa$;42uPvzAD@8yNb=zwEQ5> zuP}Q;J0fxD;E#Mhsu-ekynuR)*rtHY3K;8Odoh;o(c}75C}2ah5Mm^JvLC*&=(?~@ z@Ap&Lp`zC343Y49uY|TgUa@qKJY>o&EZPkN)#QLEDCH*vuLf~AxknaDy;+Sl{Nf4;Z zqX-kbNCuiBW{TsVg?7K>`k3?6(718At}c=s1>+QfQbFaGdq2 zI?7ULN($Ed=nyaULNX$ce3J7}?Q>nl5g-0fs6Od$z929{5anm6+5VN&#t9jyF#j?= zZ?F&jM*5d{>8x?#S16sv&zTzf;eFdc;yhCJ+oITLM*Rtldox4Uu4Ed3rra~lNP)uC zqvdenCHvB&uAM?yiAt|L>{B4w19WiG5r^=PZp}Z3*K*HT`W3&^%z`m17!$M}4-bzd ze=adRxyKZo8897<_t|zYgt3d%Y3M>g1=YDBcmgqNaDK<=?J}15dbo4#m;*@$tf3L5 zHaI$2tht0CRGj$eS~U1@SlrftB~#}Cq>Dc451P?9IjUx*J&47;rn=M1~Pu%DLp&Nz;zKcJHVHjzV+M;W6W{X<5evf9-lEtUmX>3QZfY zY3CP4Xk#;Um{PxRrP+WE-8&|7U@(QDpan&d=E>+5Sgk z@uRq)R?FLQ_!NcQ(;Uc#GoYd0ckpJHBTYTb_dT*XT}bpE|0!k&8jikKDG zX6SRUTJLI#{?DGd{AI=A>X>eB1n_Rj{*kE-!#=|H`2A;;3aSZFuT0Dik@Bi`Ey|Pb z&YM5uaoWnWyjT42rPfuKHxE^ojqEK^EOj=W*zr8;Z)aDV`TPh{vn}ZrtwfvA6!i9o z8yEm>sv;lfq^vxKyTCnwX<1<nk~o6#rlJIPGOoFo?v=0ok|Uhm!&(Ej`-6 zva;aXYs&Tt(|TClmm*y`@^V>aedjU9M8G)6o&cW2XwGqZM-$>N+g+}P!BWf1-?75N z2h)I@e!=EgnnRm^`w998?GFap{x(Hut@t}`Y36A033@lLYOcNb?W?h=V`dfiz&Q*K z%wksIHi~H`er0v_g%(R2EXQPpv#tE(t)ERtBpy!OjWmoR`}>7afAnHrX=%03ZA?eE zhWD9Hcsv|u!}4R-D3o7Ha@5Y1)y!aw`|W?Xks>)mX(CZzfX()ut!i4?}P4HkDDj9+ig&)qmX-t!rt_ELnqZRISyuY?&cl@tjtv*z7|}N)gWX zIu(5mo)yUSY?l|T!`D|zpQ<3MT_-ZO0aiKzx~36vZKe?2KPBJ2KGFNs(sc77Z60hX z??tB8B4J(8>sdl(D*B{9YAujOaXAML1W^e#!D(_{D#c22BGXKH&Y~iJ8ppE?Q=th*psf1M#jY(;@ z{yJPxFsib|96nA(mRIVZoa8YgOgei^MuyAINW;W17gxYI!&G6`LLiwJPU>7cm|EXm zmMH%^9QBLW^~z(>x>{>leBQJr*MneUzPX#EHf1ubZ{sJ#9oA67I zW{U6Ge&laW+xc%)_v=~^VwD3PV?A}%8K5g1g!NNHVj(SLL;y#y*n3&B@;`vguSJAP zp^Mu$fCNOD;o9$S+DNxr&WJRre9$paSmbcmYn**3efErE%y-<~^ld(Y+buK!x6`Ev zjMX8Rk_nOFv8lJYv`lZdhduOJ0I|g-Ao-2^)ge(M+KT@O=Vren%$P!fXwJ14SQXzX zdtd}Gz#{N48;=O%3OE|6Ng(z{wqL%pc3r#s!g8^ejK1B1%PIK>)Ge3LKQl+SJ?~m* zag8xp)DN6afq+X?cRhdkE@mvY(e@;i#^~D#JlRNuk%!4}kW+Iqs#8)x=9kKFRiLI7 zDAlYbu{`plmiX14{2{+iR~+@{Hi)L!Qe4$uhZ#$ejxF;Z7d83Xm{Y+~Q3m4+(muO# z=#Y%n;DPHG&VrXjaVfGBRhHp(V?_%8xu-j`NGSy=- z%t+R2t<{ZrH?=eAk}53#_MF2}OzAPamz+t~Bz(Sk-NWJ>+2(GDT_w@cMOxp7bfxRt z2`!Yrm$A-_Bm%1~*`f>|TE|iEK^Z5id}1wE9#r`<>t|k(v}u!{@+4V>>p?MA7zTo8 zosQU5jorD~(q{vq+cYfUH3`dN!m7p&Eyuud5Ivz{DW}Nl^M+EZqF{3Oi`@=%PFl&< zLpn)J?9^PAU*FFjdPm7(Ub7?@ArqcH$FFJ)MTMjS)^ZA zyz!ENI^uN4_nV_p`}AAYJ7i&}?as+l!1oxcYW19h$49Uaw#w=!&w_v{k6D2|^VJz( z$5|>(XbUCVCV9L0K^_XgJ; zKhyGyK{;JH{p+GEQG}EMJ<0l{Go4%i{WI=B&9$fNtYvH zPn^BU2?=$7ZB!(QTH>XOxU?LmF#w(?1@t;iwHnX+z8|su-QL|b@RjO#4IRKB>%_H7 zA925M_sY`;FEk}On_G2x_P@LNuBx`f^h)`z3N=!E8gGrDmTv4S{pO24hJaA%cQ%X3+iSJvqbC+eV^nIyEHak{eUR>`G>F9 z{61dvzwU|!RQhRXfSZ9nb|{r`Q)!pRV|`n(X@YTI5y)fGmHuwn<{lvE8$=%878bj{ z+LW)|P}_ZbRjSJJc_+HG^hgpy`sNp<$OedYHtuZEZWf?h4a#xyB@%)DQA3n284i!u zYHFH8DERiBIPGk%>3-hfUn-j11aa*FHB!YwU`43@z?qX#Q029h%^ysZa5k3aRb$R>GRk~ICL=BZ0uTJ8G}H*YQ6q7uMt zHK9v5FH78?<(0j4AbQ29G5fV7nl#Z|e>za_gZDQbROGMy()-l;vr}zn8rq)P@A}-r zJy5u8GGS19UNfl1_II%nTwC!Bvuv`+eOA}4IKAuJ8mHYsIB#;QY|h} z0i=|d6Wh-z@r&Z=FKs@$mOfECRCJ_tVEG%)*#q`mNjZ4B4GPr?h zAm%D;@5uW7>I&rp2HALlQVO(36U$e1qZ+VP}tQ9ct{=u1-8cApvxQY zepN|!$SwA@X?BxQ=z4;mRX(K@W=~xzYgDx953J|)$@v;Kjb}v)+g~R9jubBcQzOk} zmY$Q2a$~i&ZL}E^V`o-Z%ChQ96>Q=O@*?&B0ls^PU&?{PT*4H_6VIzJzOiE@H1D^x zBX3yfRe2qICZA06P@piVqfOaI@73ltZ3JuT7#bDxI*@5f;Zwq`yk0lwYW3{fH2@8% z#X*YN6lb@;iqFc>XiZIf^p+w6p_8Y3A|S3^iCtF{l#}+f9Rd_XFAl^n5b>7LD=Xy( z&4Ga1t3qqN7>oZ-JQ%a#eI}*sCe5s9tzh~C=;^;F5K#ugR}xV%GjWldsH{1j{oLcf ztGno#K&;M*j;0=KCFMUseFh|oqDpLtCg3-N;q7|fIvX-K` z(qkc&twNIkyk27Z#?}7;f=1l;jh6Y9MGv)ZUl~QITK?Vmcz3VaM<}8ME;FQ>GL!9W zjNi(YIM{Tt%;R9gv?0#e=7M1OmZ3Bca05%rgqm4pDOZD;B2yWw2k+&WxaWhfz zKZ8@|+97~a#5mKK&bB^U-ms!&{kdZM2b8LG*?!xySuWt*jpF|SrVe@-Tu^~nhc?+I zzIaueUtnva{)OruiUJMSV`n2cT*5fOBunD)d1b<$?z#){NOKy)aDhwXaPv#!cQzc8 zf)Ld_N#6i1

    $Ac=^sbml%Kg!97W%x9z@dSXvcf>jWSZpB{svoKbpK;wr1L8^yqR9 z4{(0sM47$*`~E>H5FJ=a{>zetASp#rcc2reqe>sr^(XApWh)VAjV^Smrj}m6yBwx# z!@>Qlfef>PEfVSJB?R*|*Vnuox9#Z~gj(0}k()*25~yFC-D4P5#CAKGMMa z&A-ZK*SRwua-XJhpDT&+E5yy%rLwDP8Bq^DctPl-o(OeHv*n5=VLYZXvHP8u<$*@^ z>WoP@6A>(C_ZccoS{EGg)<(Yp!5UP+AT#4=-4pp89PLd0%^vOT2;iVFr3pD8(?eNj z{f+0jr_T>ecGF!`MQZPqTrl{VQSqx#{G3~6=CF+yf4rR2BpyXY@=`jiZkJRej0!pY z)GVRZeS-D-sgb!>Lu33qy1>m%Cpq;7*G~DBYwnE>Z6LL22V1Rbg2I(Du2_k3?E@($ z^lO5!M>>E5`}-=`lu3YCMza}XDKw#5q8pCz{N+|FTU3a*qvjnC%x5s5a{IH{co2`O7mO_GPc}r*T%UY`)GS&;4 z4z|K`AygvHhsUN8u@b*kOn-ybviCmZSuZcf6Ow!p4bE~VjWezkC;xk4gk7^-FHhsL zVR2Yj)Yjw8d;bF{LZ1eiHnL&fhPJ{$CW#gp0@tE4<&oh;(J>q7sKqo~m#X^YjLRvz zY(1F?VKl%X1$6SO|Ll7dTKP4bMcV zHBZWC2_r{pBWHBeOMcKg2yQLX@@Sg^YE{#6el!c7dCfm(?K8!@gG6_TOoKtD=LCtd zx;dsCbv~-VZi!zaf?05Qvt+G~jTez#;)Au?XlHrL?=TMl@+}G`B{UUPM%#B4}bh&L2IU0-Sr|#$hBEQi+oaru+KX_RilV6II^!1Y{`xNzV+v3nxe}(%y z+paXYQ(x#CC}0=WLA3p>F;?V;$-f4B^$`~RA8g2zsJx|OjP+G#haz9%ojaFO3)Xyj~ck zOOe}C_?8NF^SSTKh(-r5+_1={^7cbUldA`|70(dQqA?OIy|E`2ES7SkQvP_d*(HTD zyA1;#Bt&>=hXjf0KY8t-b`o*_uf2z{E<17}*~GcokY!oy*du|THUL`Vao}D?KK*HI*%s5(U4bhw-(Wq zd2aGP3Se5?dV84R`KI4@iyAKR0qm<25C%6bq8=O@%vD;{eh_p00jxWACarb^0>Iin zs>Gd?vuR|sBUx9%%M4wQzsFX;X=O0}&~K(*%12hB(O^T4bXWOqpkXKE;fQPLb^5*2 zhn1AZj3H2@Ugby@OG)|xpr7k4ZKGH3v}^*|05C@70E5L*MZx^Yyyn}jqGO}JqCMl) zo&NRI)Ov9s_~ut*1|8_aPubtF$No$^FsklgJ!sT1bJx`=TJw769_Ph!H#PunzYIO7 zw$}=|{G9n#u~zszRXwZ7u+yy*5jZ~rAJl97sE-VZ+6b|{Jow|@Ns24YoKe+_o6;g^ z_9)D`Yt~l34OiB{i(k@J+_QQAN7cDMGX4H>f3q=@92O!Q(wYe{AO2sRXiVI9wF1Bg&CJGds@G# zd4Yb0LVcTXjjx1PLjQj?G>$kAnPrpyQ9^vxGy$jLM^1)x$ zk?h_3_-;Vt{RpR$j?HKPDFAp?VE2UBfdWG7!BqlQazH!C_Zi|tvO)N#Z?VPi0dMY4 z?~SibEWHc}7q>som0A$HJS6V?fM?%5P&zufotR!ZLbraPpj)lA?dyS zq{Jf(dk%rIu&^R1l(c^rI$hrvRUROla!J`LleEn=POH!TD_S>vyP%jC><%buYDar2 z>hKnL)riss*%R8GLhLOlSuk1*fvpKdN&iIUja@o<=nT)GIUuNl12}EQm^k4Gy`nB1y7e3P6o@<# z;5Z;ckQHfCZF0PPqRId|Jd24TgGSaGJ7s-vQ>xu?@;$+M63J3Xkj6PF*UmO*KB@IxilnACyFDs87_HInb%K=JyNJj<1-UfN_=!& z>pt%;TP@@<277SiOI+;y$esvxt55IeqkES98GvkMm`S$|kHe7G8WiF0#yl;}W|Cps6K)|qO)Ch+{kJ8Dd#B3|)0#JQ5yv8<)Au^iO0j)P!>Qr6;~vdk#BN{myb1{0>*wEB zxiXf=2|JgSZG9N6hN0?TX+Ddjy$_-05f%)&2hJw2!#ATjX_ikwhp@ zAq}(mG>|>rq^?d+|8?Qw(c9@5PI*x%Ku;O1U^0lPC+l{ktz5Hu+IY18>DPQdGHMnz z49RO*Yc~&h;hz*J_q~V)x66xIRg`w^qq5V^V;BOj&XE!AJ*gC3eTAcrN_HyXW4+c;n@5grvy8ZMrq zRN2BsNB-ElRGvTS&F>_p14`|PAz--;2Q>$m$~$Dem%zBOS%k@0Wyi1*Zy>uNTXNw` z{aB}|%htVcWq^PmNe2HvfUX9u*I(+F>4nLv0L&q@zCM(P#7`~fo%ysLVbs_8dhLFg zL`)#Co5nkckMQsJ#7{ht_&B+F)GxL>J(me=0UG8`I0SXTWn5mq{b~mm6HyNn!?uUu zc>zdgh4sx0v~~tNUu>lPQi`zLjOz|cud4Y;#!>23b5YCgXeH2alR)JE0L_nE-{gh6 zos_+Q$Z+(A1htTL{F2HbD3~V5+;V(lA&jmcp=Pj)6JsZwtDcEW`qz-N2L4l+!5WtC z(tZHQJRr<&3w(O$i^<{4kO<1IZ7b#)uV=&XX9aI^A%l$u!V+1bY|W!_#fAQw11TCA+^hSnpls0tK2Z12GLE-rEw!u586sODZ#|rWsidbI zg^54$(Agcr0iJ;Y_)0zd*{h3YH(0rx!qYNXth@t*9=%|3E8aMC0_9G<9pYG3wuM*xa@Y@VqDRBT`3HRc>hqxhZt29VP zX?TzF>8H!pYN;V==KZ)>u@W7UPTFyT$~Qr8FASd``9~PvdB*uoS(r&k$&vnS%U>iR z%U&R8XiuDNLfGE}t%!YmQT>hbc|b(ttM3t}j-2SrsIkX62TA?w>G3WyR;}%%P2G`p z{|Wq1t;4xpQ+{)l5P@JAyNOO~4|6^8=gad~+|MA65M4!|YMCbvJTxd~FqEIe^*{|B zelGAyG)^JZ#ZnVEbV8IbfEJQET%&!Zzj@uFo_0NVu zAazQ7H8-1zko7hAmxqZMjFqpakTQr`$A4y=4cvmmKV?aE0}gWz4RTDyd&E)9vG*38 zUoyXJ`)l1a@9@^WZuLMS#-KmHjAcOFf#Bg08rRgS79RQNWeAZL=!#f>RLb^(Wk<<= zQ;priA;gFGZ;r!gS4k@R$e8L?5h_ro<0X1EVguw}K?*!h&G=;(y#R(;$HwrHk$%v= zuMIv*Ljr@L8_TIW?~`ujuqff}K=tcnA*Sp68T(IfF5c5J$k8{|;gORIi`(*(SjUm` zX^kRYY3fqz5)b>we;H7+u~eo%Qv@rGoOE)3%#6J)_E{V=qec({mzLw~QYoqQzL()n z?lPr`Y8dAljH)XAU2Y>-pmQ3mG;2eI%i;Ry)Y6RL4|i>qOUt7N;?L&h7D&*vUkgD1 zJKT!0OMTW$YTe(DV*Hjk7}JXiDo}D@je%+S$tNfDRV+>f(w5(~oJ~_!MYdA2+`u5A zAC0I%xu~sW%k3AoL!fzYSFPM76)5o1n`FXG9F2vi?n7!X^wBf8?lot2lw>IrI zrnycLnuoihr!P7v4Oi=mpSc-Q-uqO0?5bGx-+mHVk%9@4VeyeSB5p_#6%;gy;tjj+ z);6xcL2v$NxAovsp4lywepe%!1d{-Foj4H;w8#FrSufM{`yFSd?(@w76vqon+2c&K z>((PG7b0uQLPzD_X$^h(y7lH$W2ohqm&PZHVS@kO4naU*(T9a1Q9?T}EPZPpt_}mh z99tv4fLJZtqE*5mi_8JlCXE!qZ_ z=9<~lrP3{ZK7<3TT;=jB2j8tN;ea9_g@8b($7sxttVZsYs(m$f(SkGC0z`CS_;)20 zg)2C;oM>}6?WgZ*bvb}jq+atAr$Ct{Lu#RL;pFA>($5$|nTW|13_O|ryALOT2 zERA>`vrJV6qFj)PN%AclvNDo~MSz~qnn|Ib*2lXIEoo=MBybckS}txb1$%ApmX-Eydo3Q=PTZUzqxpH0dM; zXz7VGCC;c%oSf04XnMUF$G!{~iNa!1!6|SGjJfUa$ z1gRXSidDj@0O)-#WAlrb2u))O{o%7(*$m^OefY^$QAFiv!Sdq(RuP|sR7I}!5D>hh zH2fP2T6vb?JNFui4SA}7HoO0Fg;qIQrY zh=I5+49<<9bLP(LkheVm%ptcY7O#GG(h{_#QYw4}1koZb@(%w4 z1bngGhFw{2nMdXGc8q5-eZ%(MQzF-dvSbaTb*})0P-P;<5hgwsFT%p;Hs;rhiP~8~ z)Azd)SAGCTqdZE_p5<^1%(@b})ubO`p)2dhIX5&TOo%uhNcYU94E*2P%GHy#SxE+P zaO~fw>H8ZiJO3p0GR71uc(q6qfb!aw3|D5QT@)WAY0)6wQ$j#T)(Vl87$ofS{sDdQ zvV9L-(7gL$bf1n$%W_9VLU3tmn0J1+QIOn}Szfctwzrj#zoPPnB@C*hRQWq?Y=TMZ z)$#A?s9@(;I>zEo^P0zs_nO0m}Q zrq{A;7$N8*%zWdJj_-MfDbxK!_$#9IJp5y{&}{-TCNE>gU3wdoYIg8tSrS#O1)Qhq z%WyOK)J^2gqozv!OS6VUn4v@Y8Be9r{J;@b$nyUH$QSNR?v2q0k?hpND*WU$MIr+g z!U{r#l-8E>S=^P?{)$iVABTy4J02$9A}N8!uM0Wg7D%8%cfd|Bz2&!`NW|Mo*Va=Z z9(@$+jX=;~bgNQ|3(^Htzb#$7sW{w5<+H5G6;KCA_nK>}&wMAA7 zB*m!^^*Zt3YQ8qFdsMvV&s4eXRDIF9)$@=bk0sy^cV$6)O{5{i=QxgAP|=efz9pOz zWIm4}C4e0~Oz zNnIV0<1Vq6%HQZ(*jVosDzkO+98!yaM~N0S-VD2En^FB#h>SyLi(vuN%}S_R)t~g7 zy9TfXN6K*YKnepWL)e9{<^B&4X#X$doR7qYPp0Ni1?3@;cmuLUh-?>HF#SZuX{}R= zHJ)o)KU|eX)`JsANNFAA;SC5+pm)PyZb5Lb;EW3|ZL%}HhrDLJ^t6Y%msK{y)qO84 zcVv83FH*%@t>xW?#!U>b9&TqqF^Qu3`dUOeQ@^qtT~&+m->i0{<^D1Su!vOj+Ss{- z*L``MlO>aBlif51N9@TnH_aC&0c$9_<&7?V-Q$BLK;j7_7#k>e`?i6b{fg{RHODVn zR9R7oTaHqJ)OXwx@5OOfB_;XmXcd_l^Rw}2MHf^ZptLBu6SVm0;`M?vp8P~fH)k@o z&6<~K=F0th+k3|9*Z%S8vUBUsJ<~u{oRwU{U|xxQgca@^>u#gzU8tq@mb3~K21LiT z{I(FnZ&;TX1fsRxYVtX&A3^hrSDvZBGmPX^zudK#Ii*H`@IF-(nGFE}6ZzHyP^0 z;I~cr)^9yjAz>_VsY|-gstcrhaIh=alpIqy-KF2_>&qg`)ng}x_e4yT25Q0@H+*P_ zQyh$4w1GsDHvWnILp01t?hbnI-W+w%`!0(?agtQt8HueO5MH`rcI!l;n`i)VFItd7 z%YT|a0O1M2E4%uZl~QZRcIm95T0ALv_VDm8_Ej^s% zP0m}gJ}6LBU5Cuv&&zX`P5e>P{zT#lMSs`g1+a^-E@BMa`mI|p`Pc2i)7P{N*OReB zY0+^^eI(pBN{*-L1-AwmFW@@of5Lwn!Gfn_{PelHZO!-Y{+6|Y_qhcWW{bWV)T?VQ zlDt*6a4+zgMD=})pJibQk_|Lb#B5s438!02!SbsqBrD_sizN1B++uArvBY|SOiB8s z50X}yfzvZx&f>dqtv#~dR;5h?^dStJ!uDPCtfR+m-Xv~mJ0$Q5b_M17Ln=heO%RuA0qB&Ty`2raok3g9b=_mfVyu zm+UW%TB;9b`3*DY6f+t13uG)tdVyEinKgu$rjFp6t}n=njw`HEe|j9|2g5jCVCaSq zi->GB69eE^u$Nwo`)pkB@KBL>mZ@&Qr>_h{v(XIX{leA~)wGxl_#F-UY`oN7=xhB> zZi?%j>9if&k2dbYFEzG#iles9g>K9IigboxyM;8;H7wf$)*)9MZfXvwn(!2*gzVCyz$$AIH zso+^@shJQS8;jVvzgrFtzxmkh*wc6GqtxNK%XLRfO#2j%BG75kYyh08k$t0Xd~{l$ zJ~s^+9(poFyTo{!D++*EJ3P!}sJSrJ@mSFOFH3ZOhevx)YUf z>dETMK4Jr{w9X}rGotBc#w9AA21rQBHJ@OaYKFc_*o?T zX^Fj2&*8iMkjjz6cfXjAs`_Mg1T7N0A`D1kSbEn=D)DgSi0 zuIUecJhT9{CH-F*j&v^(_dEFD>4{=h33BfF-xe?CqJs2PJ~Vw)QzS~HYofiF#|2Xj z`ywIdFsWWP+OJt^aV@vQ2 z-Kom1TMy2%&F&}icJBH}f894S@S#WbiH4rg-6|{wAUw&PgKxK+M;%+gqvJ`HasV*Z zB|A#Ec@w=+ZnRrxyX|*bPnbgL!F!3)g^IEQ6-q_lFUS==$mRnuC180QM>s-z>CjX&yLoDe4r^zjVNQ9<<#^OJ| zw^MQkS%3KTG+$fz`Dw(aHb#8wsODJZiYVx-wC=v9=T4@i7U8n`dD+bbCYRViz@Wuo_X=f=tqQ-Oaqh&r3mm%_(@M~8RI-}JNmfCNRuCQ_?(?-?e~)bnx8 z{K>1y9MSlUSQJf69Q;U$UdMYgihK!9A?Sx@fSfHU%*lm8K;ldXe5J!H>&&4pl!%&& z?+owOm=Odj%*8=g`*aI#%OEqgGI<9NV9C{e%C>NdSlmSHNx{4}`%P!YWgZHRHkFQYDOUMT8lV(x2pG{n9-;Fji zHR1F6mO>PI>k<*4;>5$DZi@y>oaMLNC);e?Zycot zs~GboNY50fPgHKpr!)*_Y*xS)43ZR2rU)~AMIe@wav^;$Ln|8legdoDK#|u#+01A_ zWCdCC#MzuAP^?Y)WmPI0(9fWm@dJ3M-datP`gt_8#6Pu(6XFyjU@i*3)6&^FJuEjt zSzo6Oyt`i?-a$I#0RZ6oz@f}AQS%>z8C4UH@iUvQrNuGOWfsH=RKLF(wq5d{R~+0O z19Xg@2g74yUb>_{;Z~MPaBpmCbH;0ntprc;@&2;Tnu4Q5+1Pp6dolgy8Ba1j2kS{< z#gj07SurTS3*wcK+$sqE`@ENy=|{#wnnFNh6_RHHj6IZ8NwzP0LZig9MSU&84@MH` zh2>Mns2QP2yCGP;`H+qu*v`MIlLmp5%f1*r4Gi@zU;20icF%pW!_%?PCgt2Tx~e(` ze}QB4%y|8wy72oIxS!XD;E+J#1Bv@F$!4IY-1k1e@x?-v zlt6V29k(ta<_HQs98N~H36FuWKZ|tQXZtUBy}t1ketDN&D5sG-VZh5Wr|oQ5%XeL@ z`35-BIS<-STf*U%=yua8%CDQ;VF4@JHf{wzt$jRVojbQxek) z?+hVy-HxYjj2P$2SO9|{z zu_+Q#e0Fk0o4D5=@`#(5{A|^LR#5oNj07ez9t!A>5|BAb$BQ@HHj4-{DTA zJ>C)<7pdn3UO~U2?6WuXEQ5cC9a*I1^g?~4$fu5;Jy~1$KL9NvP^mtSf!5b3yluH^ zJ&VD_FpNt^os1q%&?mgP(bPEIDOso;Ur6VvAeqg1R1qQJ`wF$84@+UkqbexrLt%1~ z8Iw$Xym;!Zp()o3yC@YpSa~xZ4KOywDK-Dh9?J1`Bw5#65x}Y-g8WsBmqV_p-!5lc zl$hQU;Fx-V83hY`t=yyx8TqfWNhA8tYwPX#FSJpiR_m=v+Sf&uV#j$nPR*I3hu+P~ zNUf`OqmJeqD^UJLEoR`3+-vh!$5b^#pj$=DH|)qvg3p zcVK&e_hYgrbb(6WBa(JsGq}0ABL1x%gr7%m+ka9!X+S^u1WRRDSyBI4$4-EB|47=~ z1?+dgLjQCVBe)?5y zMd2@R2eu7VL63C`5uTkLz>V$*zhrji`PUcM#;#O-3#qF-BrxV16Q@|U&wB`+;I%@` z7OY4*Xz{n#*LrXt8&!5EtL|g(D7;Kmk$Ta3=n()U08<*eK$oOYn?4S6lmEXA1cjP8 zEHyfkIJPXd*KPIBu=+E}YW7N1o2}gUfBz!<9hk#jI&OuZ>0^#>JoU}gK8Y8S(9Uj$ zDu`4j6a*IY&)!?JKjqkT@xfqn>TdftfSDJ@`ag?ao)C=y2 zwZ1=~kZ+%t3=PSa)C#fOd=?o1%)Y~Iz)g?LJ+BYN)jTOMF}&GjVL5MWLnMKC?`4_k z8n;V{3o4m~3udVrzl}(_ZNX!IA`Uftcx)s-BLH9!$I@hBWTwedsko>)SWVS4<{Joy z;;7h4Ll%Eo{B;_>BP&k)SdNy2uaaB92L%6>-+p12#_vD<8W+GRm||xt^J}q0_pCBv zOnv~TcW6=v$A9@}byKVJ9^{KiFPrwb zacV1QX~>Y|y8EdSE%;W5cagtMmlXH%wO^zBSa6^)uw}D#I`8|=@Y7muMq`1Vit;H8 zfMu+g&cby>VsRZ`56%?I2@=&LV@#+qkO`)V^vwRw@-@=HJt3-PtBpC$H+UWk@C}RI zNqu?l`u54H^S;r9IW3guZzyPp5qXJ$t*N)vjF(^t)tKF{(@E61g&J%qi+XS!%@5FD z|FWLK#mg1sj?0(GCH&wV9+fxXEi5g_EzpHNnYLGBO~K2&rk3lw#~S7VF{vm0v>6M# z=0eSvBm2ImUcT1zK0cr}{KlBndgSk|7uTg1OIXq1Tln#u!n7{9e_ihd zNBilMo(iVmG!4Gv?u$S;;-7nRJK1LTtnQ6fO&SU9hJwQZ5Px1ZbmgUJS9$$}b%d!U z!bpUK*pHY9&g=h09ApGjm(8YDQ?@bc6Q zxZrFOJK4pUIhq}^0{{w=x~@>`+lXD<6p@=!&;C?=llF6ef0J6L<@T1heum+g)1}rb zRD>EwGuo238;r7L13z37O+5MSKii69$(PDEU=cq_HJtZy@wMai>+j-gBCBY$Q9<6Q zTi$L?SfOR;QJ&}JjQ;^1E;2dp&YT}2~ExTcx>>e4^u+3pM>X)BE9e+Ghb_}b~@ zQ}IX&K$abJy8fDY&LNJILJr71J>pH%j^WY|v10M#wY+u5WTX!JYY`Bt_T*wA2F)&< zekAufoD{J9d)halVcRBPgJNfUJI9+&?JtaT_K51!picBYjnVESqFrh{FL=>!+GQ0K zkXxKt zyMN8>!pHVma0c7XxS;==2It}$0oCW&L zrTIT6*=1j715l|%C|It!`gvMOdBtg|%#iJ|>^p8BE`x;J=Gq2A(^=+{Y)#|H*mHEJ z`9~rF3L4SiSs&mRh6R2yEaf0_uX0r9gZy1Y{U}01#eCXxe!j-@qqU!t-3_BhbhkOD zYPcu;U;j7L91q@_BwDC*r(OCsK<+J&+Zax!=$$ zBB7D1k>c|4>lKT$H+1UMR+o^H1ivdohYk$@@(ZRu`prmnz5}y z251i3&L>fmfmAdHKx&u)ogsA!-=0c0dkw4{rjstbt38eXQ*DK!_=FojQd2aFgTSQ5 za}=$Ql+FBToE82bAaq+Yp*h4oQIlO77H;*d{6X-5QOTWnQQZeZ=T*EZ-u?K|vukNR z_6ZNCg#B2ILq>f0lc3!EFg~&js1ASUWkYQMrXVzwX5z$x;dDoYCY{(gB)6gT(XK3y z@}Q3lkjr+R-VW;AK4>ejdWKd5$CZ$DIk*lV?w;r~{}qQ9Ty?#Gf$Tcdl7Rv~*b5i4 zbdC`b;E8N!9}ciiK0Df9Qx8)>FX+heptkSjh;rjZ?4P&zQ8&)&C>bn9?$9mVnFs8< zuY9_IyQLbIUzK^LpYm1jTB&XlXe8H9hmWAL-psLCvG?Y<&A_7v_#{{&*13P#I-))i z{{y52@ZKM&*D(ftDi}cYNObhpUmiKf(T;oUO-4yvd7jMs1gGFwhqxqIkj_oalKDZY z(r#e9f{JxCPft&Mg#5LhLnP_=rK2J{LaSbkP@i>Q_I6a!R|xJRLk82?qC=t+S1Z*b zRBq4MQXxFGF@YJes^g#c?x>&fqmYJ#u4@MRyY}-Co^T1b^)HGSiZLId4Jr;xBlLOO zcuV#w%U1f&AzqgXkQ?28eaQw#lfY{_KFzJ7w!D|62()k?e@%lrgrfKEt%psxu?@AIX^_w{jaB3@c2_>3j)_{o=gv^fp$JrZ$L(gg+-^sWp`MR7g=*+!SH<&aN9T^m z{yEUO^Ss3{fLlUP!1F-}m$xUyd!cwPk9kpeAz=1Gz9nf#Xh|TV&xe;@I$nv=J6ro- zo$eWu<(DaT4cd#8AyLMTk>I-_fjv|Ioc!9(h7%8J0Vs={XJ41pjSU%(rJ+_S>1`C6 zK%|oiDkXH4D3|lE=0xP#cx`|Ora;#%jg@D}Fcw8A;X^k*Ic(YIg=4dhp#vr{L!niE5f&~f{_~x^{wp@OxoG^9rPMlk19Iq|pcT=4bb#XG z{~W**s+87NqH7BfeDvBoPk9rm*n(9hDPgglt%$b?s4;=!pG8Ar3lqjR z5~i*xN&pcP9|+;!)6wc47x}=N_>!sJtNE4*_@s>c#=;>CRB(Dv@7CjYYgqxAWU#~n zB}3neWnFjzEr?#}Kzz7@HVpCTcBD9}zH9kOx*4PG7mRmyNSR^zC;xlJuhDW#(G;lY z=;TZ=%1$YqjLqIyu_cdb(lYC*SuqatB+3yUvW0|{O#dCWG1|=bKHS*il&RqAv{@CG zSL74%=AZn*) zZq8|~U<=7bA!;-`tr(ov%URGA?u-TB-z}ZfnEQ4|@k|5dZu)cUU%+vIi9GiHQkcz* z);p*n_*FmOIEMei6&0oIbmsS#l|QTJt`&ULwyJ*F?Qltv@H+EL|4Rq92_TRxPvx!?#dU--SFQE`j0jogxx-BaHc_)1CWKnKo{!VJ6hn*9gz@OT(sI)aFNF8JD|Ku$oRv>?Z4P}*lCQbM6}_wIv6yQ> z109u~YuA}gxfL|_*1LY>`-0)gdQFD*Yltm)yTfPq6Rmbxi*r(IO5zta^mT?%s4uhy z;G!hGBoK^vzBU@5s$gNNN*(wKh0gOUKyelpI4)KRhidjw`RJ4BIh&ys&tLR1UI_%$ zP2NJRwfxnw`8uaM)ljCasQr{=^-%l=Q4{y@Wu?V+@3S$m$l_xeM<7-MWGnG4^M2tV zjhf5b)r0(w$F+wU*={lz-IfjB{eQnh(%OD=nPZ24I!L3Jf|0jv76{o2EXxrUgc5UP8dV$l zGiA+Mv{L(7vMoXd`pc5kYfwdxj^@!f(_%yi&p#Gm1?mg^(kss9fi!ZzznMlf#nFE} z^BPcXqt%kGShXn6caagO+OW0M>n5mJuiq+_#r!OQ++?hnh3x%=rjFTiT=H0~5LF_O zwBaMtemrRRe}FSKl5ccgJZYH{l8hGzXiQkQ7w!fFn`n2G#pr5K$VVAr&Kl#6GbVPNfwhb^%Dc0{UI zn!lCV9h+y=s*DgfQ%n1Dv1~OoOBB@b!TpteWTE|!j6G_e0&nlVc^F+>gbxBl=UJH3 z9m@XKY~H}6#@4NA-$0(YocKHXxEkrIv@MWw;fYGiMMi40aosbR1@F9`8EFd>_M z7j)rE`EjYZ(r(zOBu^+OTPy~c3W9GuM_AijXt8_KTK=%|ncfOVpeoNncwdh(% zVH`W4&5o+*WI)!`65|g6;leGpPHl*r24DEnbj|BCdFb9vd)smP_rrgZtm_2BrFfjc zY7WMd^0(UxFl?nGIVDTHJs@|}xGQSCTCmd9!Krb~!%u@1U*bU$#n%){Wm`~V%#URI zM`!s4{4LUk2ePXC2Xbe){8;IOrz}yqFC6)JshZJ?(13AS=Fgj|QV;XA2$jXkZ+osE zb5)GhE7!PJYVoi}(Gv_%r=?{-9{I8SmlBR$u6ou|*%Nf>$8m)V8AP`SPq0(bzn~G1 zaxU-_`Q=Y5;G^fS27bcX9=Uz)+U(NXgzFD#dkzq{uvnk2eV4;ug z0rz@4s1M|yv_;aytlP=(X{BxdKWpdfz8M*0wHb)6Cq$VQuj6*wap!t}{|>)$e%jW2 zeS2AI8R+Gq2Gtn39~fyTxz6!9l%a&rL<|3{k)wr7UfNvsUOPXuqh<>+TKUwg)_Sk{ zSpjjV`<}?Lhv^CvCv;BNAc}hw3#RXL%q4k_!%}c^b-Ok>*S7~V?k#MG_da%7C*hUv z^a#TflUb&9_v0xH(u*p#_tgZ-vc8PhnQ})?jZCSd9Ve>pJutj=-Bzo_<&b?hh9Y~a zeuv4Z3gC2jv&Qd#1%g-9?eVpH?|@#a<{`*Un5Hiwl!S$(j}Lv8%D_mk_gRo^t!@SK zQOO`w1r8j8-PtT#UX`sMd)k*=sCUf8jFPfpq*gT5$e6yZm9Ji?#!)1~yfqUga;N#2Fa>71f;Ry4M3oaz}(ZTOiF*&e`(Lzr4in5mO~E)(()( zLwR81()3JUiavoU8u=qLVs<|BDaS&=g(WD6Fu`)+p`tvAlCz2pn>~=RO8MwX@a)D} z_E`#T&jZ5dYVP%TEh~5(uD^GeZ9V13K{6?1yqN)GLp1ID(sUW7y0$1izgYQ%nIsxb z)DeBvzwx6SP*q(aG^+5AS&%Wq{Dz%@L7H&AVaF1rE}dPo8?&Sg=w1!w>C$=iwDFye zCtIaImCUx@ZBSW*&rvF%E$ck<;7)F>#-e5A<_t4~^FCoPp4X~V1xdFbS>D>59;mQ= z_+fO+oAa77PNKSc0@RQn-%)AMSOv5N$L2P|osuO5favSzEwIRBp`5ycbNO9J>zQT!Js5 z8L)2(js!f~KX+FyPJC;+A7~|NKPFs#G{zu=2g@s?r(XB(ll#j({><-4$yLkj7&<$x zJC&X-*U_-jL0J5A9V>Ro%%8;$A9agx8>Nju>OQgq^VDqXn`)R($`}lg zOv&J57ibN*-+oT1xm4U>rEF<=%;>;LQipN_-|X{_SH)Ic54N!VmBsQ6fzh26MkJ%rqQs1FWY$$_6t?$RHjP_00Sxw%F}k&c?v>va=$p z&=2A~!qAI~)So_H_WR6JUMq|KB_<$2j5AN96Jhymq4OvB(nrl=4m;E&0t$-G#`Z{p z6cx|_uvsFwuT0+Fe*0X&QF#MkfX1Rp1b224`d&`wUeG`9<(=7(o`GhOJoy!jwioo8M^f7w<`f@6zUp9) z@>fme%TSVR*kC>hfDrhhMoanqGB#QR^;(wXzU8SPa8^WQY!FeZLqQCU@ z2pKHSPa3{<`7o1mocRvrIhr z8~b8UGI&-zi!ZWl)jSny<_@(0kG#w>XgRz>aTQ^2bh*QpL#A4RkwIXr z_8HifiPhF~k=K8H9XhtqMlIwULCx`4@q?YZjqYnDeM;h+wXC^;MN>j)KaW_dSoIE0={*Jq|`^f2edM zPi0h#74s`v$WW!)aR*MB08R$mnPyuC6 z?;ezj2Bwq0%JtTOHe&_U% z%W6r$cmD18*=L=d9b^kjR1(vgneF*IIeGT^?^f{xKz9o$4?BK+y) z50A50HG-CArOqI4=0B>@x@g(MXr{m@n^F5Zi>gVSVMe@a*Ub(aLeHVb%3qxtRuUHi zpRhe{mf#B;yU5_2%miq(L$FF;t8Pe*k-SVE^lKv!!6(7fq5xeQuqaa3fK_oC~UY z&D6c( zoA12K%F0)lq_#H8J6@_?xzL-{NGr*E`i%SG@M|&l*mFAv0dq#WK;8YSC~<<)^J|gc z8noVfHvI1KE_GR@m1xW)iJblD8DjBV**n7Y3ISsU0Dkf||8V$YnaBE*RBd?Hz-Hj| z?8!1`w0LgW%s=?dnnU6*qcP^IL4g%Taka#^V-AlLT*1{JsiX9pR*Fa@`0x8-Cx1tG zvo-&p{@$B}rpdXVBR*?WZJyO1Dk%xR>`X;uh}Z4q!(#d)Wf2>2oa~Z2LQL(oF zK4xL>bx0hAhTLroyLW>&@wxwG4SNq_U zRO|S5k#kRHc+}THCt&NNhq8D3^1xj-+_MlILoM&!@ez0!(KZTp?$`T>0HkGy%H@uS zr3mj$4t+mnzts0YV!!~RY>R^oZRU#l;jmkfl$;AIN*}hzfDKvY{|%V+*xxUI=Cc#1 zBWZSWRypLE<%y~l3$S7oW{0oB?*nf?Vsgm3?ahy^0QMTSTzUEk7_k+eKHs-Chs;_-#?Vh1kj1&8e zbjOFI78QT>r92wnYk<}Ipo`hsLvOv>D2!9s#oo#(v{~L$#f^M1Ac{P{eXc@HusgJ6 z&&g?jr>=5NJ42y^kCGucKULMLWj$Ap{y$uudmz*Q`~ElPFgYKJ za+qu}QlXghF}69697~SL*~mF!4p9y>BZ?85Lk=Owl2dXl$~kk&NX}4}kBhsW@B1zdu8E~3=em5vmj8A54*+g`-zg_!?NcLTJ^G1$jw~LZW-Jvj z;s)wul$`@f2|`F^9@*RL4*heypQ@-IYFeDQs2T$=U)g)&xUxX&I>V(HRvbgTJ^0Y| z{7^pzxd;NJ|6#}GI;Q8hJ)`iId>gI(6;hyFog#b-g_ecvgwPUv|L^*OH=1u z0HR|sD&J!9EDv8&}A&s~@ zQ*sTwwcHCBZeoQm`aB5W5^BB=fu4lhC1!mi2z;v|lNs^;rlTjomCexovNsZlw#@E~ zZ|S=g0jPDv#&7w^z@uLan$EjI*P6uy^{veF9cbU^yNA)goh8nG+GkFfG)L|{T+Kx| zdfvU>nJ0Dn9nBs-3cnVBr^Mg1zJy2RC1+n|C2<&4*fs)#b+owojZ$>0)26j2 zyXRROe@7S0!~G`MtR>jis{kUq^ejbtS>xitvBxgGP#h8u92sP0tI16>^_#mhd~<=Y zn^i%~&n811X}QaE^y%z8=l8N_R6|xx=}dh3Fv--HIU?U;Xrl3^V2|Yh+IS#_4STze zi1jWbY0t_hfVWwe7Nzib;Ii6@d+ML%GzU*%y^xY$ zOyW=)AcR(43P5z<2!kG;&~gWhxS9+-*=*ek%zw&uRKm=ewC#eR9A=K(fKfJzmHv(i zlTNgZ)sh}-o@(NjM712fC8cz^vNBUNYrK{KHe&%S(>o_La(WLBKkM|yRRqCcFtyxd z;bpvmHz(~x`OUGM2&ByDCn%Sj%bF2NV;pHK8GW{WuU7ZB|IP1oGNBPBz+j#;wD(|{ zp;CB=qms?WqTd2%sD1m|WoXhxidlvVjwmtx`OfVfHl$q!1;TOLF{R)2i;tX0!(pt| z$KKlWE23ss(@C}R+RF0`xmdbH=xKY4Sf9||hhgK#1KE!K`Av(}u|vWyTfA-U?6{|f z17mxtu&F)5r^)P5)17y3Qln6^C6vJoTmjb?775aHxXZ-12h|}zl0uA_8XpyR#@Mo9 z!w;ktk(QuDJoGp@<>*$_;EmzCI_Ej_-|#~if&m%O(+xl%!{wS;quYv4FX=rQ-X%8y z;T`*fi1`4me(k<8PsjH(6gUV{Dtn#Dks=N6#3MSok#t#0_0ERMHK z7CJ8)sT#~Uj9$u72ZxX)u1P^*U>I6(5ZE0m0ot1zZ=N}HWnw!gjY2j!bD04{e8OC3BLN zsdGn3fmzE_USy_6QYGqI2ksIQ|Kt6K076dg%;!AtHAJ}zymn;Ju|qC8?&`B{%(ZO} zb=mX1Ij38}KrDLAlF4h4OJJQZPwOF#6)xBobYdv4rL=y4=`phOVl9(4rR`Xrrt&Tq z4hpO@WRwkGI#z>Z8zm>@rM8EYyhJy%tY-)>EUOZ9?9o)zYF{6yywnZqlI#xUG{$@= z^E0%+)6_1n3#QK8f64~5wtKYH16>R@?@|8uv%hEmC7Q~DTi`W}6u;;2>6g$}-{Gcu zpx=>~VZ-={Z-J#7q_OehjN{u<2+`wtVJ%w|3tSN3qIO4ELXicV%TxPPTVL`inc)$u zlDC8+Ha@PBN3pykclRU~)~5P(kX01JvBHudGYnM_@N-)x}D3ij<1 z^ucT_(CtuR9KC#!?!3A3V_2356@MKQA5Ir1xYQz2<=PkTXmc5qHOLo-Vcxyqi#x$9 zB*zSD0<;d^^o7|aNf?ncWhMcT@GUjQ*^eGUd6wLo^eVkhbmH;}wHyzmI<)|8HhjNJ z&1V_-rj5~)D9RFPpBkh7j_D6RGc-igZ``E;RU||YAh??=>OVkO1<~C=oJn#3;PkE? z6jds{uhI4)&5pjQoXeBm?LdkQ-=Ke0Ci>$@V@zch1km4OP*w$)I;x)qzog^tsz*&z z;A-(wNyj|{exd7LLbTsURtq%(+VJ*O*}r8V&dAirHXkF4!p;Z-ph0xF0rY4{wCQN0 zTV3B&5|tXAMeUH3hVYGzbhCCV5Ta;XD;F*x87eA^NDQ?%5GQGyTojw>YmZy}8lj9e zw8o*-@66TxgYB6zpTU60d-|4ozNXBPN@=Y%wlmyW{{d`cqDiKv02!9a!{ws|O00JC zVO(h~Lm(cX>*%jd%M|8`Bgx2-J#OY_%MnP&EMyuTc-)Qn_q!}~1ir>*5elJ^p}cZE zQ8dK<3HIfLUuEYg9>sGB_f~Go72CSg62fO8M|wfgQ*MzGg7%P~Fh~ps&z-lW77*y& zgOps_QeSr%B$zf4d#XVHKgppdiFN_MO_WW*YEf@R^LMSxn@c(sP9%mkoQ6pO>NfyG zTl-%Pp|%X)qKriS+5EMS)S@7CDciFt^If>r%~Gu~@)Nt8d4FadP31Q3yc~SNCE_3@ z;dIahN@>t|y0n*F6&59@qH135EIV@baAMKEaQ1a`Kp02q>x6GPU1gm_JOC*vEdk&R zt+Kv0RF;MB*0E&15PicGn>)X)8ZCFfDD|h0M)tI^Y|yYu?d0ddp^)TtTXWZ&ndH}9 zWo#I*I?{n(3(F*9Vv6d!HQ0Ujm${x}&*JWuszSy5Ew9|QVVkAP zxhVBd4I8#bp78s=5@TJ@)%%w(zHnQPdzPM8}6%iuFk&`Xc6~8j+-dgQ$V9W`$jtW zt$J)K93Pfk(wR*cvlcRC}wqI&xVjSJ%_r zn{g?#`N@L|06AW)}Av}uuze6ll4pVLQ!VG@m1nr#2l zbC=L+kJ3~94TR_<{{5ctJ!nk(SGUUS+l|lc0T*U1Hbv3nr3BPI60PSA5LshNTu?$p zzccRaPj~X1NvVZ^8hs~n-~dj``pVCIzocymUcXDr+uEw>D8k5ttSGJ)eFrrK->Wb(+1Oczc;7k9 zXy`vQ3mCx84)y0m3@U8BZ20Vd>iwX53*6?UbY5wE&3WnE$s3;`I9e;w=A_U10Lc;% zoWS!;GxomSLW3W?d`%`!-K(I(Fa=*w5^1kWL4pJr^iGcUYENGjG!7tbg1VDF+P^)& zmWpVO?d=SKi-PkCfIwe2Ff8YBijeMM@aW zlya$o#zdPKfYN5=toPe}Q0N;0%9>Jwj3;5j*g;jx%u^N2kvY9jh;c}Hd7`Fc|8Q&L z3-+oIhoYDc*e)wCy!~5>7SEC?5lY?`fW&Os&s1srDkL<;!7sDLK=AA$#Rye^wyNI6 zcZ8cKyp0zY3GwWVBmJR5qVu_8=IN{QOsEVe*)QTD_h0a zN-=5FI#{AcJyEpvj9%LP!aEuvw=#&l+o*|{rnC4frwzdz1_xvgh^w@ zGb<(S2sb;TSz}ZOckqp0(D9J+fM_sVA^^Yz!b#vY^KAR9h>E-kf?#yDrlq71eYxmW zZz_eZw7W`MS(5QAL=;es)1Ol{wp*1X_xb(z~hFW1^Y@ADTm>Y56y(;K| zbc|~r8)k?G_w{WJ&9gLw308O=hi%tuthm)_)bm3wnKO1hV%qT$>=A2pEq@$o;o5+G6@)75S)7#e#9;cOV zt$g+h5oFAokZuUeELEq|N|>~u+G9U%XjI5Jahr@-l;0?=Q&R$PtO7CG@&+#=j}92r znY%8b2Q$wdqtAKhprEev%QbY5IU2x1k5`P>$>f$LR__jfvcsoUpaGl!ZX?M-at=Po8?Z;#Dj$6S zA>&G%UBY5(E}@bpwPNeMjmGj6?<~DR`l|*gfRi+|1U`hI zClQw?(v~T1nbVQJZ~iE~)qN)W5bN6rX0uE3)j@$gEPZb#@wME^&cg8yj{>7JN}+zj zjH2^B8@HThzas(p@D|0^qFr*SqdSa__!2oyv-pg5_@4aXDwNC;u0tpQAT?qogb@46 zr+;px7X$P{EDMODq<5qFCd$V#tLtI(4B(7ZhPS;>!{Vc-A|Ws+*^Pyy)iDItG$M^v zI3II%-K5CVdm6=+xFodAx1UpLv%c|PDAPqFm=vynh%?k*&vVlFlgxEYN_n*;F$L{8 zqe^&}>{slwnZDB1Ym#5gSG+5@EwQ;UwOBlO2-|Tokuy@o(*Gq0H|0NjX%%du-H&Tx z>)RL-T%J0Wc0nyxCqC2QxvFq6IO#fyrlM2Lk6X z{QT)qBaTxX*QIr^#zA*DQ}9)l=8VnJ!svlNHS`j98)oUWsXJmmkCZ&Vn@y);K8gR# zbOIvi{x)uTGj)Yig|8-+)X*&9>z9@HSmAI;T=LO!F3yxiq`Bf{ zwjO8TfjV8fBHgw$zZpQ>4YyNQUR8&bH%Wqc+6OiAn_i^}Md|Xf^y{~yOioWwi)XJU zzJK`gTRXyV4SWS@E3dUtlQN$YeZ@7e{Nl!m5G1#nnECb`z?DJyfU&Vec!y5v-&b`5 zo4cq3*g*l|*ks@ewQ!w^r`Fs)Fec~5&Z+ka2gqfZ z>!M^0mq27v72nWfTBF9X+MV*jum|i?9L==I_T~HzIlI%GA23H2272R&`Ls7gWIh0O za0P^{aJ~qEOPjgGMYHrO4C>3X%$%;?lhi$qPMD-Kwb#_upO%2aPr`R>pUq~VZ980r zw?ox+QxQG1(}>aLnXBC4hT;&qVnXK2f4jAKvYd)&pRKPHbphV%Ff+RZU{UQmN~MLp z@o~>s;92lx9hUSd5LMQcle^?&lRY?l{IwzerU8_sK$>1|8kCP@aeA2@<;-kg@Z&!K zfgwsamY%>vEsHcQpLoG}N_dH)L|%h417!RDx;}z7(daZKG9ZZhz1vGGC9s&7M4Ikl zbI0FBct>Z$Ez`-Rtdgr|BdUF|Yu1da8ms-7-_fX%H%fTaLCOO2e*kftoYF^uWi>Ol zDZ*7c8y25fb+lJQyFvy4z|~q7?_I!Qm9QFm>UtK(_H_3xeo1lTi!XPpM|v^VOU?c- z7MtT?k`!GKPdBjRb5z{!%@gz}+7ty6rvDkzIf5rolzeZw&;ox(HvW@t`Ks%lRViLY z^@b@Su{pcGK)@pc2m;<6I|~cECx#}_5OCb zPr8ddmH2nQ5^RUW^3SY!uy(mVCX?Q zM=$HcQg(iFi-z~E%=wn^zlcYEv9i})g0C&k21QkVe2=UjXs|Cda8`KYe(l4r&-fCQ ztVUsKajSC-0f?Bk`I-$6pZ$J8B`-8ik$2!F=5gCSPc1Z^3c($<@pmK=|4t=6ENbJW zu6Ml30`U|bga!RErg&n9^*+!)u;i;t%aT*U-b{c``+}lv`VH z;k7oDB$qa4-sUBhEcoz|0Pj5;G5P8!Gl6OB|C|t{1ONIe0Hb9#q#X$olBUNveN{6U zm=BV@i)>}gbEEU6rF%`f%mYEMpf7-lN#{wg}#TKi}7 zNgr^k!icq<_sp&}3l(3~d509 zN|(7JVxCl+zVLoeb*-1aI+@n=sTcc#do?O}lz_2{JAe=}Ir#|$?X~5fsyl3rf1cKq zJP3gu(l&#hN#^(v3)6P~{Satmn`VO*me6i|LWjK0bo2e&FUj1JG{%|w&!p>`f(bLQ zGw+&eqD>q}XvC1L^u5n5kzOIfGmaDou-b14)AAGd@mu$&rj{z)(0<%*tj?X=O@*(< z&G1C|!?)!DSzS>A-B!J8BAGxV+rmuiI$ z*Y4?~2ghmvCoZD(5jKslTMIGO>A7waYpH*CSw`k21i0jfpI++MhVD_n9d@hcM*bV? zynZ8i*2C{1W<2h#KAxAKkHHaaE{t;ysroXgwqTY1hS!n+&MQJKVwU!+3nZtp# z$W2iHJih2R;Ih7PMOU5b`vVcZB&%OK?m2mlhpJt7h|XVn9c7IuzH$5U`?x4`TtL{6 z@G`_ja9{qY;(^`yxvQC_AYyV<8RXPa6d^cN;`#A+Ed_CnJ1Ze@7XXwziE6>dzI^6D zat-t91};INfnz5v;mN^j?ll$CyBi~wbt_k4=ht7KvlQnoBA2^^=oe3R>Ss8 zHpd3$tI~)*h%hNCXJh@k-ObZyk)C$1WOsSe#zDftjHIY0k@7yPr#uk(0d0d)xG&vv zqsFb=u6DSH-EUtIO*EHS4Bz zCf_)hz?Dgt!&lJsoDYxH^y=jp08=%c~89Y^azrE^SO+e3$U8fPc~(r-gZMfyb}}<~q{buQh`nzcs!2Q{7mK z2?_-jl@j#%Da*v@h}?`Ag6hQ&el{Aut%P`pn=TZWp1gO&*|YPj$lTkHB3?}RWzlYelu{o2Cwaj$JwOppLc^@PYj8ZGlXm>fH0j#@g*0l*>+Mh8?JxRgMI z@d06i`$MIW|byDL^^bGj#Gg{<+L7u6*1jHRu9|sT zn}GXsS2gtmHH;N?Q>FOQMWlBVcMVj9TnZp_D10%H95Bxv@+ZK?xOvYbEwj@njgaOC zNz{~`kv%x8PkqC8do+f=TGIXs&h9`$%cm#Pub$f-*=hz`rgw`GA2H zu#;?mOaLM_8gZneIqF-W9T)h zYl$%3QITGW(5X+wy)FgNI{P!rqDnH^O}F_-{dh5XTPADh=lM^*qBYy4L^8!|`9ly_>moXeek&=PY)@+$>#Kn1R zcSK7#0?kt8c9@c*%vP?sA3Wb40gl_9A&vTOg&hKs9@p={hC0^;w8ND)3j zOv;#p;2k5q^ZPETfOT~P&x)yGDY&Wby=sG~Ka6R`tlg1OC(S##kW64RC`Mih6|2^J zMm@2fe7?TINpDk*fTrkmn!Y!9Nb`@>vr>>NXy3hL>Bv0?mK4_o(A2`u{{6k<=KCGo zbCvz=k}|S~Md&2Bqo-J7wKw~vx^dlv1w^XUu_-tFa8S9Iesic|_N?$S5U@nyT|T}l5BAdJGX1J5o$xE0ko5X-M6!|L0`^f z#NGKmH$jK+ZK}EC2P&r&<=KrTeulGkVA6XWLj{!=!_%l=UB5(%|Kcv3Y+sI6vOU){Wo zFDqo;417)W}2nZGHePX=6oZEg?1|95NwNGW zjo=bB4?By6-YKNnH2#XI1oqV6O7%u1^E_T9N0bbe6cKZnliND!Sv%l6AH05(u$(bU z;o5J8M9@GkX@3Tl`&~WALGkc+gfJ0C$eC$l18Nx%c;94FMc&YWwa?ObEkA#kf#L}E)^fWJ~h?iaZ{*w78!_;Y(&5`?m6>O?gCY>FhI1 zX^MwI;nFAxxMWEz*_rLU630DQZ5xyJn3owZI|K8)7kECf^=x5||MQ)+pGuSVLs{`O zjq`OmaJZD6`xp#$G3^HG()tsXOI`7b#A+@iBgGMjGYH%oWI_B@m(Em6UIyj+&20?% z{o1d}I!c=tt9-C3L!2Q`obRcqy>C$??y1Z{9TPZb{z=@}Aa|jYrx!D7f)W&fq-@il ztA}e1Wizs~QG-iaGnXGXO+|^kU*=A@CVc!sp=nc~{h*3OO(uKVV_6Gx~AgdtP50~Awkf-jqr z^jrp_C2G30x>aLI$EP17Pw&v5h1GSr4WM4a@QXa@AhbF5zBu}Q(mrGIcBF!ZNcV`< zhFtkp*7K`nd==kH)IPs|lt366c2V?yde^7EDXz@M3KL(7#T%Q&xtC^2EyqUdvo1?E z{s}&)%#IY}@f}{|t`mRQ?A7?n2#RN&5jSvw$TZ$66rVN#hVa$w-8lGk&FRP)l*Kk& zVs;Dsls^uq$D8JIf3Q`bMYDWs6MNcv@-Ik$Gw?|GiNXFvA2e9#SD{1`)g-=&RGo|0 zXhKMCA~fnBQ%a%f(lHR$jNg)f&JJ+gmP7(Czhl{5s1NjHJ%Uw!nbnt&=J#g=yjWqbzb4+BDqfum@$I35A8G$e$7#Nk~D)O+X}|GVKpw zXRAUDzBZuVx*=^$^WL;@`P^c=sMeSPd^(=)E44bSHPaiDe~!;7W>JUe_&kdsUT^Y3Lg(q2#QR5$<>-DbN5LfQ7mg!KpH1}W zz3Q+r4o@jueqmElnMaCopB-wCacVRI?x8NKkKxo>Tpp2>I9!&Pe9-oG*XNTsE z*ajd50qu*pFwr8>1I)%|)o=u*X#L{R?Lp%a6VHe2cQ-6-STXmK#DP9n_hq}7c<6mI z5{Ah$nCnz(K_5YysFa~BYh5yLt-3rK-@aTtg1$Sj)O0?Up7vA=;ew_v)u=SNERmr ze|re(UcdD7(fqryYH;)lpvkB~Gnl8A9=>Z-#_C zu#xADo5US8?bPorg5`j(PY1FV?)%)9S&xrx2a$MDVow?rdTy;<877v=*0rORX(c6q z!1PoT1ICe&HS6?XqYvrR!PZD<#$3}z+WogQbN0!cM{pL3FVxM2%!`lQnGD(g>{&Ws z?dfJ!X5oeLwfq@`GN}XuJR}{O@I+|^iu z77MVls;k&eH8A0FRE-G7zTAD`8f1EN)kX^?!A3``?)R90JJwBw#3Jv*FFrnY;_1h< z77C05gdOQALBwDv9p?2XxLe3c8qUcNK=U%-UbOep5TMw6Esgr5)`8S4!y-f6EZcx9 z9Bvs4BKOysUVPR#rG2?vLOyKxW0V`TA&$PHLVN)Ll&a|2>hM4Bvc;z=V6}Kjpn66o zaAbP<8%^_xUqfA~9dfsymzXTf*VntEyh!SSE^Ca1INLc`L^%&I(-S!#|A}`%n#N=*A1b1t1o%Po?bW~nYx}ex4>qr*31zr8dTTn=@V0c}cB=L~p ziqDJe`ydKuEQp&S^;=~Z7)9MUebZZx>hME1qp$gug4^22i7w~`0EEanL76Z5%|zZ(B`OV-^Cr3DRu>S_#f1QWT* zQ#!LHu1DUJ`n1wEP$!KDujGCF+5}oj2coYqL8M)Isv#zWZgzjm?OX%Nu?^Jn(ZRi% z)762?Ux+0YidFNXb@Mnv1eOr>^zh}sSCX&BEq|DsfdnLhklbA0dU}r_G-A&>>8mC& zAeo|@2_;pOjB+ZF6sWT;MYb>9&^oPd{P7Us!@~|*Odrd3<()6hNdv~ML`7WLby*d2 z7y!xxC?)cNM8xe`^EOR1AVgWTSuCJ0T5HiC6!ugOv|;sl1-a$E1c!p0y5cg!T>&PT zNG?DOPMT@sd|X`lfFI?Y*-r?uJQ^(69XNf$zGM9}cp6f^zS7grkOUzv0;8D`oBm#Z z_CbvTdi&5tUN!?OOYETb&>Si3uxv*-^XdEH^M5Sd&c6w<@TmkO!Jx2VC2?tKxcL=u zhJxM$`?ym6CZsfYLB(To?jkhU{4cOi*gWsr zv>U%HPQ)6_CMMtB_fs+zxZfSvEBNcQzX8#2Z|Mp%QNdTW;?er@`dV~1K57{>@riqM zlRfLwIc;m55BVQp5{%WG5hu>g3D?toUcEn24tp46sBv-h)3e6y84R2K4sUHf5Bx60 zzQL8r58`H?vuQb7_|Z6;59o+sL;fqHj|ukT;OWz`1QQbWdga`^3Mmj&>?#~?g6&gd z0wAqeScI1ad>ac#=XR7nlw)B?KkuMFU{(vi2{pE}p?X)zz6M^RFXOwgdueUh2V?CQkV6i#_et z`c_TfwZz6xwS|?wKI6qzS7(CPM9|Yd&KAE|O~|s+I-Q^-Kx+0)St{Bq@v8C2JDRCm zcjw5WEGHuM^(b#OU4q(}`*yft9i^CSN+gyE!6k4pxUdt3u+1k6-4(&oU*-D;xODTb z$fO+KhU;!Vk}PmxV!Fo$iaGhTH5C4XlrUj2_7vwHTki22_)Lq{S@Z{)%#Cr z^EAO4pDJ8r-{gJp=CJPg%=rh};5{?f2wuI-T@#%v_fUtA5Y0j#5<^kU8Xx}yG~0CW zlqjC>1C{c9^JhJP&@@VRghxMf%d=Vmhx%1_s3Pw+hKltO6)%Z+onrT}l#fNzTr(Q= zUDxD7o;?#D&4jLCP>@vhV7b}gDa5a2-%*mLjvNlPc+{cXeiEsXDf--#i`7G&-W0>B zeFE%ZoNJsIe`DON2PFVlB-GruXd{i@&P5i05j-k$r54rwS5WUaDVAFWMe~JDzid#q`U%2YgtlqPJ;BOEO7i=0ADJgqkVrlW@Rqz z&zjDM&X_PS^NXxMrD6KPIjSkgB10|$)$h`Hd0G4{s!u)yC_}aeyFx1TcHkv%EgED3 zT|7oV9J`%K2I+D4YWPjD2QhyNWrN+e=~fu>9XlTK1=&v<+m^ka(ftqL`QR)>FJY1C zf^Je1^4zNQ@zXC{r9x_s)~j4s#;KLEC9e#Vo7jYP6VLfPlwgw9sU>igN(j8>N^I*` zMr|2hP#>QBBfGNEi4jf5)>PwO1s8@=(0k3-1$Tiy4TNc%UPF@ec*|8c3#SXge_)-l zDFdci2dc%E!+2zdY8>8#0$^3$qMplMpY7TBUc6+C=(i#^WP0e91)}1$r#B!n{o0q3 z=AJRth4uc1(8Y_VnortjW<9GnB3v5%HjgTrrk|X9RZ=(AxM%q2X5^{<5Pq+s-FT2O zy$xR0sEe}XQkJ?-84idC5CT}cO|CN6jIeu93G?-eY7^Xe5laKcFAcRs60U_QENM_! z@YsqUZvw7*?w(?I7a`;mb$K+!3B_i2bDe7iFszg#AIjIlM$67}Xsyx73#!f{RHuwG zBt>hXQ5khuUiMUWv(dNiHrS%!n%ExEmgHs8$%D5gkbX}EjHr)K-LVFGoOLf+nh80y z29sujE;{K7R`ev#Wmz)6V2)QD-*n7| z8RcF#qK8gLiCsIg3jbsG=Xq$-cCk3M&iK#WI&uhsHf8eL{svU>yj26{qzIWVz{y)! zBwT^k`^8)1Xu`EgV_yxsO`-SntiStivyJq4-Q4)?H4JAIAWuHS6UbpaABZ6kdpUx;@ z>py2vX;O74T6Qi`>9aA z4i=UswcLw;N2;vWGE)PqeX&q#6;Bqq`JxX0mxNsH4mQ&@nAShF)3ME|Gy(hL+7Q+W zk>=^V+pYoPObHj{`o5(P7ZiYm`ZF#7QsuwTrzcc1YkO08ltyhaXx3^n@H8>nj*xHd?X&07*$8}U|bqw@MPdL1X1Uiry! z71GvfYCgU48hYZrXPtUiq^{u;6p|=!Swhv5JRtJQcyyh7jFEF$_$k%=p#|$0dH!dK z?6)aLPm8Ke)=QuSVxg%&kS@a8&u;OxLjFNRQ14~#47F}t$sgy)(^<5B{ys8KeoFl5 zl+G(Bqk~Gck7NGTe~l|F%Bx9ocydW4`GAO{Fm{;Dr}wO@_srf7H25pValQG*#-;qS zlFx~GEiJ86MPI|nm&nhjT=YERZ;4}{mnRB7(7E^T19(qs0ByD^qNtA%Mxo#^XmTn& z<|mrFoMxh4j#{Puu@|aD-1^vX@zvw=of90WdR=W(*(^e%$osgs;(2%{KY&Ti?~=EO zX28xg<8I!_k*|8vOj{^<8ty9pLRg%mnjdvFu23g!JTF>RP_wpXWSA^j%I+V)A9;M1 zvyAAe)KLXX`9#@qUT8PalsZ^e{trN;tyqdzwdT6y`^!hFHxK}D{cypP)c;PEa70hl{T}P76Bj+j%9Ato2iukfgV=-ERUR0 z<%HtG<|0oo+UVMP=#k-uXJ*y(8PE<1q-vUkeh$w%KcyvHA<3k?9=494<%)Xxh%-sR zkLmIvvx$LI+V741Uv017?v@B6)jhf`k&wlC9_nG5>HT|Qp4AVQ4yhI~9_YuP4b9M2 z&6sCDUCU(XF)spf1K@M#AHEg{rJpAi6&N4lF9Q^6Bsa=bY>zQkHwsboq870aHS}J0 zN<_T~wEU|&Tqp3Ph-FK#CvatG&$=aE@dbA@#(+szvNUjHCEwKO@a%hvflqq@e>{`q zv@6%!>XXwm8sb-tD(}fl^s69hP^hQf^lq6QI@*r6fd2s~+GLGMuHLAGpF2|uE^cPQ z=ns?d$mrplVoh1lDg8d>e?5~SaM+AMj7`s7%yn-F{i>^ap#bi9K$dz;c3EO9CxecZ zJ5IP9#`tde)RtUA50f&Y<nBFF#0NakCJJI0)31f0Q<4RheU zp^`YyI0R)sS{C_JdLdWb!5Z_PNYWPkYod;wJ3M{%9ea0brd(!+uEooqix?scAXZd) zQDfvvCol$Rwp$){#h2<`fyskJJ47??+y5-iwPW3O%+A6WpR^ ztR$ZX-Y4FQs*Bj-+nXr@oxcAn-9i zhV*y3;X1bQy{paDX-7{P#X0@=pTp1w_KL?RH9qiqaoD|wmJ|HE{HcWiNr6I7>VJUF zYy99TTCiPq9c8G`?nbvx%~P(a197Zc?lM_YkR{_!T2V9MU|p4|rKQpT=H8XjhK0mv z>oDK%%)u&z+HAEiA(hrY?{j?GN&V=0RY{=nLTKbjPP)=n5nVfTqTCTiTbm7yU*KM_ zB(PaVmrZ>jO>uGCJ2fYF>7jh4Hme$N>q4#eGJEHh4ISUt%Zg~lmCl!gTqGoJY}kcm zQW{?ZjS=a?b!2XOWw=IxdvlMVW>jKuzJA@=9Df9?zNqo29R(fCbrF5w;)QM6=AO zGAUJ|OXVm)u)xM_#o{Gl&ZW;w6{y{*;XC{17WmDspm_rDgqwU|PDuWp7+b{@HJ~_$ zd+7;;!6dA&8zI`_E?8C(!D|*Cu6&>_0T=zzGwL7tao31uI_K;lk8_m45(RGBVmpM?1}7Tj`+iyQe{slZ|lfW~lm8v0#I@%tfynZbr+0TW#-|n+#|C z411E9*Xr>KF@lz?@!O4!0;FV|7M{EWT0d}L>jK-$J zXXyu+*QDA101jN6%Qc@-VsQlIN;kj0DYg$lOCPE-*tk_WS8PH6?`}Xhqz)jyJ=oy0_9gVM6}_&@93``|w3tk4=D5?FDIu;BBNN8MlQWV zq&{S(ks`^G$sMVFx-6lY7EY|1A|BxRa%p08*v#ai*mpZ-$%L{A-on>_4rrbk=>V_RCz+R7c+7Ck1R@=vbOFP@uYpj=M$W%u=C{ohvi24LY<(T3~aTXE0BJs(}TlAeHH-C1~{ zJAKn?aZ;#>Mes588XZ=>W;8r^P}eQ$(S6s}Y4PR}q4DvH#|6|Fq`AOzCk`iTpIJ)b z3JPO6l~8C_&08tYW@*tM5dh_DkyCn+Y}5SU{#B;m7!hy!V_VA)rNd?tErq7_5?S_4 zh}1XtE$Fy$Wi!E{iByedVL%|s)1mH>7~KpcN!9#n|6(U^yI}4KXX9zhh8$%pe9P}* zfL``g!Wb}sU5XWxd#;(16)X42`%zhF&=>ye(EaguRrdZ}#u^7pr?E!NGrRmFt7y>x zL(kvgYL>vK)*?d$Gnlj&Ttf2J_p5sx&l6oK0m5({vuH~)T#NfW9wyh?jo6L7gsm`@ zUSNkW#nC5~ef-!}02v33|01;sJsbsJ_8hhaUdD-;v&{ZZ3>59;K$U-Lb);8AZ|Szd+hA{E8A^5OVjFC zd`{e-?6(--dtjRNX{weX>w0ost~kJ6LV=Ah27cyNxml%>If_ns1gq9iRZ|Vkc`Be9 zQspz3CF&;akpFVu>e|%Q=crAQBit2L8#MkYaG|)VS;nFPQ$MKuY|Irs9twQ97?wuP@w;I^t&F?-57`tXjlA=6_$(>#NkzLg-lX5q1QNiY{c@%0 zy2U$D_6FOoZ&=kwpnhMB-3N|#M#Jh`C-I1>(J<*-C!U|d-eoW_ogI_}A%#h#nf%4} zE`0p;fmuTSicw_tcbD2`uGU?gom(-U@}Bl=oi`3g;k^M^k3^{bxc@vYSvEI~cQVB@ zC_X_@$G-0V%cMP+AW){M_}tzY9sq4I-TL}thij*7ho_b5xjCy-{i9(=i9gKx)nI<} zmrND!fPC4>d3548H~C8zNp?AQ{HYZscn<89_ZRYIePy=~l;dV5-^|A^_)N#a64;Wi zT^s9CJa&kedZ-m_2^f#6S-Jl+;;ORu#y8_rrKE>M+sYINiPsR2_#)f~V9Anq`v#Pl zD&r$|p-l+c=rjB3J4v)rapJn=%mKd*-@-~nEanp&-`~=~L{EX!jk%_FZ8qDVe-4T+ ztZkf9AFA9rzVOki(&<4{hwtj%NYRVQ_g!J8D80A=Q^E^0|IXElNA7Tm*b{}{zc$V4 zNa-n$jUG7&jaVsO`<+UB9TVvJgQIyRHBNC#_p;d|r8vl=M?YByK{Hkz)n8Ix__Mzm z`gUC`cypQWeJeJ>zDuAq;gy#T=*ka=%bn;0PpOn=o(a0$Z{L~7J*N%5S)D1KbpHCB z|NX*x7kYFalUz2Bfk6QH+R6hTH6yEcqxMMDrUC(cAetK@xzpqQSD7gukBMgOIA^JwoK^GF5Gq z&j;>_6+HGYT8oz<^|Xfl<@#wOGwL)x!On_B+bx31Zo~Z1S^ynPr57HbDU~Q6R`H5b z2E_rQ`5`#R7>O`RCqOUXEot_O3Ye}G<3#%8NxpC3+|k50_hfp}hgxcSw+R>}p^VsQ z|10fXP4&qXCGsb`s0B$%fJkBL3E-23*F|(yCle+K#hdn5&`mFlzg2TX7HX6P|BtKl z{%7-j<90$c_NLT|N^1KUrLkv8LeWrawAG?&i@it8ss^#DVzseq6ty>1F3q-*CU~`@XL8e7}$5b(%?0JU=u1m)QW#H&`7JCI>AjWxqo||JvrO`6k8% zX@O^_1pyGZ^s*Nsew)bk9W!1cwop_V{U+8KqlBfU!~Uc0jn0ql+F{uYS}1vJEUR}U zeH1h)Z}AT1m%&(x&x|t>9Bz9VEo^JgkJpH<-q6IkWCgp-cxCGF?_wj&?HeIZB4mn% zqtaD&-Ik9js5>z8d!_VXwhRsTWC@BW6*Oi;uR?V^FoB4aaYr8x(9jt8+$aE)vNMnB z@qkkKfwwx+O}lL*UT~GNb0w$8toq`S-4Gn$^wMtXQoc@vDPG0oAyEBsq$qqu^RX5s zk(kW^m40D8n#9hU`iuU5fQ<9DNbP|g^M!HBwij%!o#RO?=0$>d!DIc9G z_SfJFt(CZ?sT-vjh;^|lq)pC2^dlWK6?O>Mlp%_ak}$$^G*o=%Zh3EwHAg!XeX9>b zJM;p8d>FamV~z>I$83=c4`v}t6vU3{MvYp&QLtr~du?pMwoc16E#58BJoRhv6~U%q zg)6boj*TqohE8Q76RC@oG@u&scR(D>viNYyIC2xg1*UFAM zF>w+%nzjc4Sc9)pFi02uRfvd~nL0_EzuL4gZ55Nij%PlE1*Ox*AGkmvzko$-Tko*= zK358vrT8aF%E#q~pC#~1X50f(-q?-kLcRJj!x(&e)Cb1@GX1_W7#Lypn@p0UGnk;F zmTRwoBYYgJf$B{-TXB@-{P`cI@Ygorm1vW#hu@{iFFlOtnhc#=3=lRfVx{4DB*v{U z@2>zISlT_wX)gbz_tNHR*fs~a4hlmf=sZo22C*Uh`{ zGm7$gv1jod!Wdt!uB}nGIgw0JrR0 zei*Q8e{O;JA3!OV^-%nNHqb@KC^7-S^wwbf<-sUDReD-fI-}FC2#&v}BiD*wKa>rg zn57;sqXto4A~k~jFBighm38ytx3BzgS?MY1#fHvj8Dtm|p#xU$XbUL+!B%?6$KZ&3 zY+yk-MW3(0m8((&+Kh6Wlq)nuXwnp9 zaILh@of7$%j>L2U?1b8q@*itH2bP}fA_Qg;0Q>3G)a}3fjDBJ|1XfH#IK+?Bvbz^clmCMe^VuB)s}ktM@J&^Jnz`2XJ@wc=pQTeCao|^taL* zRRdK4QayU0p-}6nE-yWa{hD#a%7~j&duYxRG<{o5A7Ul?lpyr`BH+WCEIk4coqi>^ zm~_6fC-g}zjFv_OuWENfaTt?2H1kTOpl9aK!$haguJRop63pL6u%_LF=HLJ7c_=zQ zSD9TGD^}WYE$GaQzK)!0^u@5qYM8|Yu)>3Y3bl&vD=TkIRee};Ao%m2jEn~G zY~U!87xNE#tN!{kE&5}Or+o8#T5*4=ze z3XRl__KP*;h$~_d%|qoa%B!OB^mvaf+Blx}4JlMtx`yHvZ9s#Ex<{U3eUkkDVFKnO z(=zx0@+M%wg}*>MCFv@e>Zx-ccos&tk`$(T!n2pBUatFPhhfv4tqFumh!km9%3dX@ z22Xu+#l46$59>drex{wfl-d^ko5}iaNlEA7bDSo|a5z5|=JFq<=R||_P=Yto zdP9bvHnXnXZh%%Fux}gJEu$=A3!iFhMdS3Z`7G22c&*Qgc@1U`piXmXD3=QO4AVOn zk00eCXI24B@F`-UjPQ&5d}1`jo7(vmz?nl;633b6LUwbbgf2&PhmUbnmg!#!KM7Pu zN^M0sYYJF56;U?J~_0aC`0QTL5@9h zd2yX8a;DJm*m|m9La;M0zu$wIO)gZXUthzojKh z1GsT!gu7AEQEV+Yt&EZZGgR;wTv%cxG%4UCuphio4x5^SNzk+Fz3z<$A!fR>UoPQX zzTM;gDlLI3;8A7e1YXmydEKOD(RV2sYJ6!BX!UH`e*E-#y-Z@`*}d1ILDp;>JNmg4u1ahk6i(7gAaHJ)*qMLL&!7q*)TAMHp#(r^0q zCjJM|A+pE-xd~Z;M)fVEk6I5sl3BhT{dze7E9Pt4?bF{1oODa10-ndt8BO0cXiWf1cG&SO~J!{w05 zp(OP_a?`}p#C0Ffb)91>t}^!`m+Wb97#TF8oC9!9FFgJ+L&g51eT%cWt{|!?zQOu- zw{dzU!gvWP7-1x>Tz2%YuKx3cwaXr*{q&rW-pcI@Hodeot>)yUG$tM#q=Z!)C|UvH z`7CtMJ|q*3g5ZDS(S4GK>2oYRc{fkweK`F0eCnAh3=YN;YKi~M;`p>jGT$_^$*2HL zhSD40Bz}w0+y9X|d;4-5B(9Z?-FRX(B$qkoap=_F(9c3~DE$vGTX}M1F!&($XsJZa zo@=ARtdMt_skJo#-1@nuRWg<;Zr<1GQK77!N>;b~b6S{t=PgF1JeEhFTe`D;&pE@> zuD@~Y0q5vGLE4oMshH18QEAV*(_T~LXr_FF0#D1`{PEp>0T$bjREw*stVKIrUisxs zIOT=031YBg`~y}pP|)cBPoKVIUPQe9Zs7aHMgF-(3xkO))%9VU{{o^yLo2HKAe9`c z9?48dN2Ay~(tXCJ*6I?J93^7ZB;g5XYvZ!XlOOdb?Gpjix0WsvU%9+gJlQ?zFSmK6 z2j8t*kz{Yt(Bw^Sa{Nd?KuUboGti@E zxuD|^T?TnE1rq}-yD7Xnn^t<-;E(-1#QZrg{11F^>bh@SkM6Twz%TbAR^ScfA?Mc* zt`W}_T>l67s>N~Wu+coy=-C!<(e!aMp&N36omUdWZ{BKfvdwU95b(5l`Vb48EhGDme^Xv#-Xr;P!50E9d_a);{qTOJp)z)e(U z5e=?pyqo04VnUwJn=}{Cq?FI8hiR4j6gJ5<8`SD&yQVBeVyx&olwhBRQ~0~`=j5pG zKMs2aNBjZW#)AqSH*i2Pa{*!wv|z{yPSB3s+tMH8#IcmnF~$!L{eo8HHfoUZxEwk| zzK%L6!=9e95LU>e-90+}_{%=#Sd>Ad5RzX)&m(f;^pG-B8w^?#R)_GvpVd2d(TxeDp&#I1UKGFX{8 zNa|eldcn{6O0!XfvoAVMWuFc;8qrjJ~nMvaM?YrKXc>(Nk7u!a!86a5fO~p zPh5RNGL>xzH5o|sTgq{4{n;^=8KV|OjBMoOZWzmT)RG*k0hN}C%6$9qHIPVM3P3jr ziyFB>X*iA}UW~Vgs2!@)O2-_Rm(v6IF$CbzhWg8Ix}2ah$I#6e+6Ii zN!))bPeri#RwP|=ObcD&kBl(KB!$iFZ5zB{u)@fQKNT~v)M$ox@e3{*{o@+`s1PuQ zt+H{o?$_cTvaamXeX3HWrl2y|CSL%-&ZE+(YQ>zz4qT28uRJSRv)S0>RJ-R`JWI43 zA0em<1PLL&1Sd_i5LOnjn$~A*%&ZS?bgAe2)nHc3H%2u$p_GFGBfH=#ItRcm<*vPBfsx9mvV=u< zpEt7Wmk#1}_DvW@iB*tI$1CEW@8DjPp5*7T8d2Xjbn=humSwlcZiPqtG$Au!M!5t0 z$Qf4cm$>~i@3HicW4ocXMgLz~D`4nn@-w0q`E_GHU0oTaJgGkzERy)pid~BPRJY6% z#0ZuruuV>;*)myfCNE9CvHbCNE-HpjdUxTKu4mMQwmzYO2BQe#5~`a(~i~7O>}p?k+7LNd~@Y3C-XbqL=Y7c-~m*z zPuOFZkXUp%80EwhV+z8nz#zueQj-tkaK(Gk z_lxVBIJeK$)22VRhL9L-VI=Y~-Y5vGZ3^%}u!2VTR;6n*C11%mdaVhsz4scMx`AG1 z6h=`Py4CcMw1x|P)2j?|Z3&PcD?l@0;>A^U5it?)*LUlsKyw)8e)MS z=lImi$2>Ey=rtA-TUS>Rf(53h_zP>Xfn6ps3{nPkVMfoCvbd9lO-(QEf_lB)-Imr! z)JagNQ8ZL%l7OZjL=78E|Lzt_7Kk;>Z)`xD=Pj|y$_E|wMYQHg)cRpn?MlF@Y5fAE z%jS`K!%PxLuy+GQS%_*x=C@SJjN${pUdTMB>Bb+V*?Q@Z7HPvhjC35mSP+hRpUZQa zRkTzbzM-)gVZR6_*q<}<)#M2E!v(5U3I4190|e|HeOS;l=iK0h2x^^gH11q=qVTPJ zMEo_g?dX2Nzo+N^D|?kHm)!nH3BBKl+%o{Xfw$5%gPls;+%Gs#RpJhSwDoeMv*dsa z*~$wg5R;f^F2g&=O+9+FO;B;(+N(lFCl^<(VUrfzT@W#^k6Lo45t`HFslr@4#!%w> z(fqPir?l>ia?7Tm8=OTji70v+t<)VfoUQGYE8G3}%&auhKHR}wHYAm#uoWhk<=>{^ zE^|1WSN&V*)u%05Pb&P%cYNDI_-k_lsOfS!Lx+Gze;U88hHHIaOUKGfb`-V zlj^PQ_e;W?jc}&0V!xjLYKC`aY7ebTIV41@7k%>*wtpdO-+xSjT(jHQwoCdE5=1HtPyauIX*waWlRP`CP+^_>>n zsyNz+v^=;)6)9AZ%HJ^R=*+VPIH1%o701B65#8$_>wk9;J6~LxlpN*Z{bV|$OFfik z9j8r)Vu=l(VN4Ay+2>G|%)iLZmDjAlJr;*Xrvo7K$LfhEmof2zsg4AXjfx`mo4NDh-dh4_P>mn!K+KoB%|xpe^ek?va@?zbko5VH`5(dXQ0T^kQsfUb@4ZKX?&V#@LoDeK3xKij?zo1K!r2_Lt!{ z&5R5CvhcK|htNo!E*=y0NT_|9`Pf8NLGa7n~Z4$I<7HixVqPsP(FRs#pDu4(yzze1A)m8q|_89JN9 z50CfXkgI$@-Wt`CRbW-STb$vHuIJxmb`+D06?AZBs+k;(g@1=hV5-@XT~l{H|M!LY zUtr>>X`6i4yt`)OjVJnA7)y8b&XIBjY>xViN27D&y~rH{)+lu`W;&@#8ToX|k7Vko zA;PJ_-f=ajDs#*_+E>x=e*nelM?9JYjx?zxur+d7<#h4HGwXZjQY*UYlgCDIr6w3oHX>* z<0>N-&)$H~d9^+CJa=@7PPhNr46$;u$U3fs_JzRDXWEm+*uQY+dn*$I^X?;~nOBA5 z2erG6+MAoFs!6dVKP0C9(cKAB8Z$-ZOCvut45bOLS>ZA3%%_!>rv7qkVJCCkCFIkn zpQ}sDmgtjLLLcv0-5?~OVQlM&EkhT~JBBSf;jhQcG=A}tzEUp|(gc45XL+N-T|tO@ zRa+wkhd03_eP$rg0Ej&0a`fMG*_dk-dieWa^}+QrMakDS4vAXU75-*|61H*Nb;9nI zq~)8s3Bp?NqR0w>4G+_-ZcaOZhxXvcYmdu0HF z-|2(pKw$~5Er4z3_znZB%hY6&Vtw1%isz$6XX%z3+6;d3GtPDKzSqWLAfVVmG~`3E zAcvaLOsI_8dM=rP1M5{UZ>F+TJU0I6$$uJ~L0z+uauyIS5a7hal};<}P!=XqfD4OX znPJgUR~~kG?eT}m1*j@`7w(&%E;*AoOQH~%>-E%qw^26k-4k#hg12{g6I=vGYCZyg zJ5_hD?tRkM`|(luHd|W|IX0s*rLl^Du1_~kF|EVbgM!%8Q}S-M-0J|9-{P%n)bf~2 zhZT>eAl!*CVaNz5%+zQ=(frod7$@sBq&n#+igX$g^f!Eo+~ihTsK<&j;U}z=L(;bM zKMHKcUpS#V%rjujz|Is$p=NEw9ufrQSz{jnrf~_bM3*%rc4h#v|IKf9yuuEO*+>X}y(AEH~rpHZ%3^Y&4{FvkL04+~bhuqheT$&bBNPxzCIDa-`H zZ4KKx^1I1nVM__Wdw3lI6z6R%i(YZZyIL7EOtOsBmQ3U#>rg#fJ*{m1XsDpp3>)hm zx%LnWRK;e?V>)XJ4Fg|L5UAB_S+J&8<(!jQl@@7YLLG7X5wBVYqn{98m++FDrVy0Wln(>0fB&W2aRlf7ZoDqe zj-LQY3Bl_d6kc>4RfSjd#LZ%4Y~hhx>9I}xW_R0NATVKZ%B+38ZV>OVX{w@qt18-i z8KrFwDidcdi=>k^xyeWu)p)D(!)Hg>blMmP4p%@o0tLvLc%lu!dml?u1yD@U^L|)k zPS(J^#aRE1(Zjh8?gcGN%5FaegZGvX+JI{&!&lk^+s?=8mhK$z+9bB_<|nE@6)lcpoLN z3~pKoN;nuth`H1CjLpb6Fn-MIrvQzDad*Q_GR_-=_Qdk)j#6j8rT~s85qux=#PpiT zlZO%SI)&`{Eg!30YlF@)X92FI+*VPEW2JnnfhemCZAb=!4raFY-^=y%nf~*v((=Rf zL)nOlGu3lmfXP$TLSxsd2V0ZMba+o6&D()S6L4aB%1 z?(MJBzbduaPCjb-)qizTB~9G1^!%C0K&u|u_Cl~%I@vg=AYxQGNPB zmmwy%<6ge*3SML3sf-5i5cO z+{)gFefLqfoxlDpQ)GQvI~~@Tm`!Cub%)PW4ixEUS?#lEI-LMNurND%?FypPMx@Xp z@D5SyOVIvn5|Z~Gj!pO$Le?Nq!PG&oYhgho3h*GYDb-#&Ijb~1rld?_)o!Knqw?v$ zx2JXF9)2FrHMG{qG{x1uA^30pRMAWVjF1rP76*iQF|+XS3fW#^78*IlgQ4uDkPtoE z#pGU$-1=8bZWh~eE7qUc_rX5Yh+B6yp7za~XVcpY$gpBV{Hggv*vNv5^JT9X8wykq zA_CoDQQiU4(ZwO*8IhyQQ3A^LJ?$7^_dkHi%I+7dT-~bQ4WbiAG=7|^V`IbUDlf3G z0bywc7f8BQ{WsIDp(khG1IbjZD5h=22Geqv>jmC&c8vUC&+upGhmI{|(=d(|nPA+} zd;S|7Vcoya21E9kvjYZ>cusc-D>IgZD>PibNk;_!D;Ck@IfdQC>hq9Zfm{zc&xx8W zPy9N^NhZnMtH>@q)mU0s=#DqL)`;M7xb*c9^)+IhbQKg7rE^Sk{Un zXk^GCB@H!f@Hb?lzQ2_B45_}q*coA}dJ*y<@Q_XviiysX+f4p*b+(IkX5%0LaTJ(X zSZC|z&OMjF=x`qE6r@^uZ%ia01xl*;YV^fTDXRklMBltK;Ycy#@C{%78+=blN};8* zT#UM4i6C#2w@qE*n7vqd)Nv*eKTaw1st2f9$N_=vm%hF_t>_}}KY}GT8;S%~0o<~G#maxBpw0Vyd52%MG)RrhlzGM%_htis^&U>- zcOvXXuv*hD!m(ExCZRJIB&mbNqbN1_u=RS?<&B@(Klj2bvR-xMjHTZ0X!T7s|{g1wRJp{%Hk`9%()~4-Vf)8@!=`zR$}q zhC`|JSu|D>$M&V}`X>jTyt0vr66t}m5uvFy#$CpvI!#eGKP7S6D*V()Z;7r!b%{f| z6BGt=`~C-L=v0ef&Eu%g8{c}BxnQd_ZrZ6}ShkVpvgjE!L+^YnR-?Jw+X$Pw6~|DS z@xD-Otr`e8{oj_*yM-!@@j^?!dM-GRgcCXv2B z*s&fnp?8L5sgS8g$38w>-;%iHVx!0SG8(0wd}9)_wP>dr)5yd2LN0C@u~4R8%mEn{Ou7#zEX9H99zus&EZyT5D`OhFGrbaXO1CIE!xyI zyEwSZAG770M}HqINIw4bodLUA{?^hVSbEN8bQg6;O3%Z5I@@`zVd9CRvGmh=--6PV z5W)7_w+4F`#NLMZwHbLh?PmvcYQ;v>?yhGOt`|S6QIB)#isdr>R<@m!%Pc#h_iK9_ zO=WIko!C|~`L!eIQ}Voh)yj3a217!!K&zXw2Y9jB(D}NNHMy>MbZ+qH*!B?P`(9yj z%WTn_)g;e@D&}Fnqgx;M`Q)uJG=h$Ol^p5k=UjV5Y#7b9@GQHKw+675za=6s@+mAAD zZyRS_Z*|z!4QQ6-dA{lX5+>71Kd&9sZ1nZ@#%&5u{Y4}ZbWnkls`|9T<2ZFAhd=m& zdZ^*_MUejT{{Wi-mQuB>j~|TvHZ1x=>8&!78-@^8ky9V#$XRLCIprZGGSwGu1{`6r zO3BQ#N;`GS?+vhI;T!C+dXqB`Nmu*={(s;6i?5-qCdjae+rr(RC2>gAsoJ zz2i%Xgy7(p%u>f?C;(JQw|iLKj5Mx>Nj!Q91En{o6Y#PoBBInn#-vWQu*>iNKB;07 ztA=NftYwMpGeHQjbsgtl9F&tr8d8ps*W$w#TJ6UaNz*h@a%8y*O>fLuJo=P4)VIov zh~kc2?{u(Hpc5X!>}_toD5#aEI0&ZXxD;r75jdQBO)f1#qTq z306A?RQ(SyaC$QAdfrVkd;|E{X?i@Oc1lujZ@u>*ZQ>G|zAe7yKzLZRLJKY%x(=!qA zfP=E-8mUyjg$!mM73Jqh=cSGsQ+dhC;Y?PNJwvrr8eTFH{mS&HIPhrgZV+=9zqhJ= zzK)uuKuyHyrvd60JRq>|=pOD1!wT>WPc%A=}dyyPsPo z>c##hOUvNricp5SVQQO3Z&tKG9G1T*mt~+VmDa3?5f8v+e#Cjn&9%pRNS*dzqDYL$ zs!B{dpxnaFSx_&H*<;f_#}vXJ1JCebAylQjud*|=r0wkaAy~PC_ zj#w1ftu@$U{yt9ux8%gTvG?UgKz6M3lOOOb=9s=1nqOK7Bu#KvY!ls-cFY@7{7uQY zo*catWXd~{h!0x09{)KyvWE+&@S6Cf^buwLpGIH~R2Hb#@PMC^B8WU|%v=>x_2(Pv zNb%#ng-3)6CYDeEX<8mImhx`B*5Fk-%k-&0t2ZV6Rb9 z`F|6;Ti;ZW4U%?AiGwNoR(_{K`zS0W!;}Fk)OKLENU@|S<{WZzp=Y$h&SC&449bI$ z56Dk{1L9Fo+!>bwH67s1^U}I_0R&EdelLU23(9Dg5>*?3PiwF;gH;Rz6HEcn0`$n?SS)I+}C(z@FBmmsJp5S~sOa^TS_zZ~rN z#-cvrTeUM1vU}#l44~bUHL*WTT%!m_!e5qe?1snXI989j$rQ?3>x`9v=WbRS9=>%Q=ue>WZB!&2(G3TxH+NmNZ!bFC)SY1E@vEM5 z7rB&r`M6Y}GWDBP5-Ev4vNoW`y4JZVK2r*rx>Uga9<+J8!i(w#CE{-_WH1RI{m9 zMMf{*j}Iun5_mK;=;w0H!pV}#KJ2TWk3Vd;- zr-#~u4mXDt{t5i6zsliP0@9sfutCaw5o>#UiH$dKTf6NU+~&peyb!Dbf;zdRG2-K* z#q^tmT6YgPBNT~O0x(4#i%hvvdp@tY*DIVwWxgEaM!W9ta)x$SW1-$}oEN4HpcM88 zhJ*Hwjz=1O63^wZ>_8BgAZavQGhq7Uq>onhr*AC^Q%<=3E%LjtF*N1Z=_UXowgRS! z2zy-lUyF*WWy9P%TbtLAB_8}lTt2<}pU8TGPb5;)@0pZ>Kix~XmCHU|GpM91@UP^3 zBkG^qF%?L%wgR~Q-YZAOFENcr@fC#N(VmVB7mbuL2ikoqs`RPFsuZx#7Yq7V*g#S# zDKuPE(|936#{Y&n(sb#Z|Kt)g0>2D9`%uqRw;hHQMq1FKk>ZpYSis`qe(9ZuBJi^~ zW?G>-#{U7LcHe&0&@D}Cd8Vi5$;Yt})_P%4@nbOM9gw0M)e{U$fb4cJl+IF)#dyL= zAGq`)nfLfo9bQJi45i{E*qv`s9wy7)ipIj^MUt2H$&!8J_})t6Upm`_QjrD|6F5Jk z;3fZRe!->W!;TlgGAM;D%MLdusb08~6!YpjP9acIgrgh9?zP&V29z4O^rx5xf# zz~5d5Ug3u`_BH&=HJ^jkWOq6)tRH>)t)Ec$c(e|lZp0XdTOQ2B=M`^!mRt8S)-hb- zFXi{+U(5O4rB<>kkX$Sqp1rX>AM!R;W01a9Jp}WAO=9H5yved>r(&L{wDUWrrKl9+ zvV(nAe_0^ta+&7&P97SI#T67biP)1MJN~->kGq)h71GZKET8L!^48qr-e!l4CYMc6 zbCs0L?|8Ho-BwvvDY3F)4*FV6{-{xu#T{&bM!O;p8W?<^o1}q_c23H|jh^c@k2%<^ z`E}!pSSf^*vR+suF~ve(<3TXy39+MSyy1toORiIE&np$q{yo{%Ah(7K2?lrMmbjmU z37VNR1DKv3%P7W(*^MYe}sP?;7 zrA(Qz0%C2yTcLF#x{*DjcG+|jDAG$KtJ=>tQd}{9N0nyE$Y!;vcHQ8;!Ud&$d(h1cURUcGzrJz${06<1ihAjoh( z_z})&O`mfUCdrT{oJqLf6Je#RM$B^+`Y5O8On)%1`})6haj2gi0ap#Dr=XakD3vNB zK2De8hBi^E(S|W=j2M~^jWGV@i9)BgCEU`M_piE}yfi!mU!tgObU!c`8lrw6WnqR> zMZ9Wwz$7_%Fd0nVF1@_HhkHHgfBoi2RA9ba{no47O&HO;O=L%e)SHod3DqU>jDE9U zv;c}lvYa1xI6E7`LM9O_-nMMit4Mn(($wVSNT_&E)0?CTt82BD){GX^413kvtL5Dd zcmFyNE>#SZU&bE=yiFPG!I`cYGAk8?(VM5}OO_?aKS{_v-H)noxO&s%5gYG*TYuK- zXZOT)L;y&$(zBZFJDa$Xcy(yz`<4!Zrl6~_qlq0ZDo=MRsr2|;d!9o>Zv^ZLDB;T0 zftnh8MT0n3#R(^mbu>&$HN;(RF`$sNeD04_Npnw!DugfWeRJ^fZqf*yu`IKHVd7>B z`eRGNF1I+uZ$Y!aMX;Sab=`_@{m<=o=FrR?%YfgG_n*CBg2of)f2wStId1Jth}W$3 zCS(YYHBvl38oF`axh#}s#sELgJR3CHY z$^ny{*xRU4wRGn96BX{3PX0sB8`NRn=nDL<)ion@Xh=_P&U+rhG*~uOek&CB`>Tv; zq8er~;s~#m=;6EbcT~pgedj7qUlq;v4LzHrmk`KeTEk|3or?Zxq?AQ_$Vs1w&S2$A zvdr*oDGG_I@$t-*OL~+7^f=||{clRhwo;|zmD}N9W1SNseOtGY{u;w3K5^T9Er;>$ zgnH7<&)=Y)H#H3nZj=Pq<>98g%zR5Alv7djP~n@mePuYSKSNd$qV;;iT=CXPR=kJ3 z>DH^9?;^)D1^th{$XQwIEE;UCcCmFD5ed!R5&%nc4N9e-^mFe;o&WShuOsz4Zm;QP zwd?B2{*(XgR(Z-kD^k`q=k0Sf_r4*=h4`Y~pz02XxrF50xb6I^#>MN%rABLpfi&hB z@}N6&x_*$iEN4vlh8kf%EW5P~Qt)3H^v}{2`u#jLdB1D*(3V{GgzdMz$p(P=7s4dI7=+uvVqmPx#C(9 zac+a8m>zBPJ1X78e>L}UJ(8Sdj5PTCl#Pj%Uk1NPn%4hr?#!990!6|3CgBkQB?AAp z?q3voT^n6T6Jig*BD|z#l2;%IlbH1~rVIo8O^4w+H6qzA2)i;s|~v{XlD=*t(ST0(AOI~A12R! z1!)VQP&LDv=0{0O{Lz0`_7#^d8**IBTsJKd2pWU+4gO%Kd0fQPvYSP>W2lnKP;fA1 z<3YfCtH<+>j1@)?tL>x4RI~;${UC<}cpo_8*RU%~qMvea(Ij8yM~}xS);ZRKVjCG9 z)1pcW^upmvC&dq|hb$w19$yod(BWtyMm_*>0HPauJ-Y;tRb8G9Y;FlaS!V~J8=aUf zRYA<589xBvmDwk$eNf=5xzaM?qaYo5zsP5y=E<=`XWQgn1V-8X&`YdDf!hzM5O)|gG z>~dY@>PCzhzn8JTYF=1anC81S)5S~zytc@XU8SQf^nD||`-qx{R9ybbNpk0IaOS3s zIN)}Mw59=)dQeTX;sL_~mC@BRy~K9MxEv%E@csgTvMF20d(?3HQ7@*9A*2w25WTts zgB%^=WsxMGi>5f-M($LR@C$?N=staKW#yF)O@4dm+XM_Vr#MXaJtT&h)_oPvyW7vt zd`v$oCL^rnE2xhIOY{TS#t9g_$%NB1MR?sMR8KQwin>J4!z8RMMu?_LuXIB=rsPhfKAR=}Hg>_abUx5e*|c*#XYoi(ide9tF;q!tpVs5^FtVLH@ze+iaB(b}c|}hFAvTI<0Yf5h%#`9W8*1 z4w3R6>LC)S8VTY3SYm@~RIBzTrXY9TarJl`KplMk{^M>8A03_9{pDV_^xpXnqm(c1 z-N&yN{rw~A;v(uK-eI4_W|@v9U)FS|bUT719gi{m;y{)0!Mnxyg~udMP3vHjeY2+M z3OphC8^bWz!&_eYbh*OIb}jZ(w%0^$7sirjorkloNL+)b&6ySflbpTlkOF0;eK`L> za$Uee$ls4M%F#=&loys^6h#Gc2YEPHu8pJ6oz`3jRq6k4oG#*MzAtxuLMDtsh z@tv<8(Y}i*q4iODJW=3-MVhYZF=E3c4{v;&{M=(4k5Og#L~oYs65^_KHdYUVa5fVw zC28*?`Zqzq;dQV{xljaarS8`B`wpRf|LV-c z8b5>(qQ0AzwHonIw<+5^K^#Iu2M+6XSIi;@78xeY2O-7M#!|E?OlhBQLZ^~;b(GT@j=IPqtC2^T-zLskG<*Z#zi_$U2BLDzR!G+FmmOZnaLC($- z8+3?`4js(E-f+a~u%z8*>8Xd#ELe+N+4nU!fw67@i-CLS^)^iL?}gy+NHb%0?omy1 zJct-Am?J>iDPuHrNx@YliP=-DlNJ~ALh}B#e-I37tAs;wlmMirZL;mj#`Ji*t+@@E z&SWU&+>Rr^*o)FjX)dM9?4mOTU6xSiKWRNp4*n`L^EZS_i{FCF<=j>`%mt0b!*^KdS9q^Te=1xi-ugepxE0Yu{n)8| zrYKC48nxg9Wi|V8wcjU^1#OK4N*atbP0gcBiioiPX3g*;mnq)70cf>F!K-w%$A1i0 z6=bIIwq4A+J7>P$$Y;)Ko`M*!NyrABlZ5>I=ZoI{OXAa_eRxBQ{n~|x@dfaflEPy9 z(=j)8`_iU@RljnA!bCiPpCd&ZTm#p5UvJ^>9#($4NoU#i(S?EoHMcf1Y$9m6>ce_%P0>2q@f%=aU9*>9&d+DjR!lZdS>>f z6cUu$2m!nOyGJWYHzq^MGhirMhdQE^xG>BB;;*jf=f4_-TNx`iy!C0~#tY%T$3fC1 zpi=ldcEGCL{zU};xi3TLMu5AEZ7*KNN(ap!L~Yn_1VCZ6P)VnB{4dqFwfcfx222}lVyEbdtgPlJJY!!j3I~=|QrmezxO@**@4xDd=+G=KGP%1NMQRp^uQK5e z^V$AQ^DH;5cVm`(Q1yN6!aXGxUc~tkz;+Om!t#t6L!bmFx2#TAJoad>-Ca;`pJX+e zv(Qgu+{0}fhuGi9-pr*e8B6n~MV3-TFC^}6YY}^+KWGodk zIcl@+E?TBKl|uUG75JktaLyb^;hJ9zE+pV9qN8UxaGK5<`LX5)H$Z-%JeKJ^^+ViM z`yo1L%w8LJ)ax z*-NL2H8%6PbzI^o6+6}dIX=%3YWt3HTbIx$ryM>?i@iVQK6^~CyjH)dV7A;7lrUsh z$@Y^58kAH~%B9kVMO;N%YR)ZOD4qGHdB3eLLvvefed=BT({&;E{psi~nupKBZ_nkb zOowtQ=BT|%_;lBsy(CCvXZp5^NK5?a{(YDjN;g7`uD=^Jg;k_)q1U43xi8)|Ad=#( zZE4@N<71O~*JGgfj{j%wt-x3t>66iv`iAjTieGM`vg@Xv(M!I!gU|m5C}NaZnEN@6 zj;R|e$|y}Q`cfd^j8H+_+p{88scRa>seWuh)37GW8_Bw4%@H*0&-*-8=CJ*_5=*Wo z%Pw*44eCZpnHZWExT6tQByR5865EEai^k7rO0;^tqlJW#6z8PP(W5ZvnQ=PNLw{7& zQ{gL1ZQEXW`jxnr7>gW@VWjfR{;y|uL|^1?t?uHQCoY^z?YDkNmO43aj*d^g^ve77 zUxjo%?Y3LK_=F^pJ);O|X)Nm+puy9jYLip2rZsLeQdIf&lM(Emf+$*h>-mMCifm+Y z&2k{9xJW6Nf3gKMn_;Sl~l^oR&d{+$=T=Mqz?3-|pjdVFotZT+jj ztK+gt?o)BehRzzYqs;=jj0I-*TPMBYC6y_0$D7B(^L^k6whz`_1`b7ehk}9vQ-)}n_FL4c*;EZsipMw!ZWd-61rb&SGJI# z0`K*3hl?q*d)~U;)G+Z=lFxtuiI;cBgj$sAtk!*fb9=XZf>luiJ8`P68raY=pD+gL zN1JBx2_t0;?#nIkGZs!wrA4K7RlRLv3w4tUewm{mvoF?k;@YCiYQL>jcT=iIHoe_j zMn-{BR!6;Jlmr58@$bSdE_FkB{HR3?rk2OXbRwk4 z65*fd&aLTUYm578etk`z;+N!-wzB_L6Z;HJCDU*(b}WNV(2?Va5*hyar$50|)$*=P z^@=l1m^ji{N2j&)M53R6Wjpy7*71=HJL;tsD7AkDr!E(%1&C1B-Mlh zvKES)4;o<|42Iv6A@8HcBZK3^3*gFXKIBHhrqW_tEeM|3WGmk&Ks5jJ>AAVMmNEjw zY5*am=Fe^uyuf4c9_ohLZInRMaAzcH0!1`-diufQ47+cv=rvSskio`GE2(34JPwpF zE})~*V=IH{-~AK?;eaAvUg&v}lV6;<-~Zu`D++2~_A_+FG~?h%)(11Bo8-xdw`aaO{EH`OnjoWFTI0DZsOIeUk=n z6U_krlP}uh6B+Ofem_PmEf1-Rg*sVx_pvb?Wri z_JnMJ)i09U2=*!98s0xaqmf0>Sgb4%4hZM%?>}ez4dz-+FQ7>&D9U&A2D{le_-yVD zW4;&?)6;!3(#3Uw$^(0gc`Tlf<&G*87hHQ~*vaAYMc2%U8N}@W3z`H#4br=F9~GD1 z9^`DwV1rPz(;ndMfHexD&Kn!QGQ8iJe0`svm7*Rr@?&v&sxz5;UQ4p4+8gi1X)MdOW2@-;%OJN&IOay5pM4D03l1fR8 z5ds3@Mo1%!?nVTpB_u{jcgN?u=hx>y*lWA@y6)>d-|yoXmV&h2_pkGrl0Nzrcl2%4 zsq&tw0Wwk-3dXY^9W62D!IbtU(l|6rs56Xu5rgopX@YyV{KZ6cFjbp-3z35_W3S8a z-L}#@sa;|i*-)URSUiIk?Y06V-$dNXcPi%xp~Y@bI3OLtd~Vi$7hX~INNZKvyE)2m zO8a3et4fz{tIAZV?L(YK4=rjKkB&>H&ciNiz=%pi=HiMt$j&LvmtOMP9@2e7i*7v`eG-- zkTx7o2zF40^A5^x)d*_2PlBs6yitU+0t)EyzR`!9pc{$dCLn^ zDD_g{=xsCI0yi_FMhMls*$fyUxdx*57-U;*p_tjY-u=+1VYbfL&nR9wq`JMDnJH}8^s2H#@37G(*mA!mt~BY{U^12ex> z%dF)63{w@y1e(}V#EPK@-@CMZKeiy0CE)=Ad6^_;6*|Pv`lQoO!Nuu$JQ=%E^Yl6G z{Kfd+XiK1~cdVQ%_|~`6tcsfAE?Ay9)f0$4wRY~J#SWqU>~kSsAVHwwN0J*O004&_ zpBxoAjivd*MHPWL!J}AmNfLpNZv>p_-Q~0R1GVb7uEHe)U<&Br{?h2P#IQOF8Y^o0 zLq_-0{5}F4Od=xc1ZZ<86srGX7i)ZN5gzhom@-voK1=s6eclhWt z1U!fK5bb003=@LV{ElF6g~SKY9uoI~uk^JJ!X zt3v&|lAC^x84Sz9=f?T&21zzgDf9CIWC`RiIkk2Jgc}x4f7Fcd;>$jA7^&xCgBIyt zOXxkOj7SxPe`$L6P}^mByC0|CED>F)XCWheWX-D(q3MLL6sEWm?``QlD!j@)d5l=^ zN;g47zV2K-1R{0*$q;c`s~|tP?CHRQ-Q*#4TC&>81}faE%{x+i zPJwG=2lr$%ROmL~H&JAR{oYytl0e(5c!Ktq_YWG{8uztgoc+-D_F7mx)BLP5KqDi6 z@IIw}`KQAZ@GkCddv=dz|_qO0uo z9^&s8KbUO;7ZHo!8lc=yid#d(+*=M$TJzAOj<}p$7xbgogGrQZ+k;_~h9LltGXQlt zpWT*`jul>pX;$e;jP>O)RWgPMX{E3kGsw%qhj@q4m z6QkPde{*(@wVVKt8X)`-e7hm?yTY+*q=7PQp8F41WXn!m``LkTpezdw1x0N)_e`d% zoNcpz)b}|j86DbzfFB8Sif0$sRRYGZp%&Sh=u9~1KY%hZ!_T(S4wg=>tCdrxm!0#X ze3#AnE2fwzF9uUbN1sY9rN~@O5xA#@(+{xo)Wg+xH12d z;Swn}(?vV@6$m4TUkuMda3{o#wvr|1riyW~FX08EJXkUx?c&Ah=QXBA=VPM;V6o?HM& z>G}6kxKUy}g`GB>jCPn)X55X7syy=2!27ul}!67tYHwKmBs8gTQgq@x6R>xp`Y4Q3`kVR|a>ro-6adM+I!I+uO zr-|ZWk1}(E>^fKg;(w${7N8%#u}wOe`RHqQE4=FM;e(LHlCeWhu;S+N%_n~qUf zngeH`{0ZCQUcxatT);}ExW~|wwOPk#vd->IJlANY+wK(gI$ubxzi!f4OBxr8VVS~^ zG*TL~=AY9aHga^-wHMlqvJ@~ps^YcN0f!{v!XcsSG&03G-a@B2I|kwPF><&5uJRjg z)tB;0Y#P{(Zj>3LdeEnCzR#)(NH+Hm22c?jK`nCc(#+X_(|a8cKl@2|__5d9FHU#Y z)bzgkw3s}$D}u#0@BnL|`h$GewocwvVrxPvLp5X56)r~PCeGbbW#NQI0d-;LNn|?s z({}0;D!hrH6NKd;JG2@tBF;Ndg`um96Y3F=;SVCahwujV8WAKf0jSy z4qD~<5fr%ja`y+(-KOEyOr?0o6THIaRUUe(J53B?*glf8;g38Z z0x?VauMlrxJ8+%8mzc!XoypsC)jSJXu|7u`zZ1*T`x|2?7rhl9T4et^iWPCJ_xmt6 zjzGO~N1#|DhWF{xy`7V=pA|p1Kim?RPHU{FwOstYL{)iI*B${@RTg>@giEN?!Z38) zS{(}y_gEF$mMGPt1~ckjaj@QHA^)gLvc1K&SN~P!p1e-=nx;|iQ(2>skC&mIXqFE= zh&eEeRF%aT#M40engTa7{pMFE0k`a)t#Eb^XAReI+1VO}h6@9YX+vwD0aQi{D1$IE ztY2s#@z2OAUF2P+6t^!D$_vtwcJMVYH)2pe%lULE%Qc`gz$cW9kD{3}vxS(Yn{cvp z{T~2=r*(P-AVqa#*5&ttB(tnqU=))rM;1w8g39@xpu|`>zfAYf!=1}B&dt>bBZ)Q^ zokm1Z^Y`^M)kHER?b2Sp00Lv(VN6M756=9}??fEkx~I4mv6A}_4FNV0!EWdaH;EhK ziT?q(eB}SF%?HV4fUddc4<>=M^T+v5YO=LdXbqgHmHOY;lVUN8RZICESthG|F-(l* z-P>E!QQb2~bc>5)0&rtzACx5$i32WTSy;UG*M>=fg!^#&XNmXxoOlc(CV7be>>tQZ z80hG7z}b6RT3U~ej#sO>KcDG3@{w8PV-Q3VUW$U^UjVw7KSnt=JYKId54W^LgdKA? z%~<{On>gB!_gjD}70D5eMKHuE>Q1<*<@^A0bz7{HMMk-ZeZm2hBQvUA~U2ig)lnO2lf-Q){6L28>hpPWOMK|hC;gh zPU3YmAhc5Egirb_%d|H7L1;jDZLZv zerSrMfpHAF-Jlt)-jXTZ3>h9mYLW40%v%F#c3O5$L#Wiq!yvI=r!m^Oiq1>KX7t7P z$tGfuJ?dCD7_!CZ4x#1F>=N3l3?c(2MKGZtU0()Py~P_cA-)}9_hf-UBsMWCb>9aP zW6L$@egP}dPzvjYz$X-eVj-EIgc|@DYRF${Qc#-lbzELxTirMVt8f8A$OQ(coVT6a zg%dLax8>XnF7NA1PH^8=drQNU=GPj};P!mZCC+^yU}HM#_mK8wO2DHrNIK}Gp(U#y z)c-8b%xj+KPmk|md}hiUu>#?}vKNxwXYLFTuB+1s5=X0nRI(p=>c3Yy#^~(C{Q84g z$nV!`+*Z6W$N6Jw%K5!P>X8k@lkCCi`D(7i1TYl*RjsC{+aKgUSa5R4W~}m0gZ(2{ zmCT9=m`cO~@*tc!F}O>DngV21Ta-UyWYhDt>O&iR?F^&v(SKL`U{THT&4P zZ#+enZHg|<^*$M|Y#xGF7)YMI260pf7#Ypzw)L+LLRYMV%tLm+JR!e_4ARwJ50PRt zOwCX&Z)k6jI+wEWw9Zs)xIH!gio!zx0Q zLvi^W$d(bh18%ME_ks6|i`)L@0hOU(25*p6`Bv+y;_P*9(TCy+B{1wmT@;pvpZoK1 zD@PAc1N}JaS2aLi)jgAxSd$?(kHoDO`gvDUQDr?8YHdC0zVv5kIhY~>|4I5<>lVd8|E~sqvayG;1#cG zspQ=cS)Wx~CEm8_mu>%IW}!(1*Xe>mp)i^GgZ04mpY_XMM0kf^Q4MIXU(TLV#9aB2 zb&o^~=d*N>a<7Cq86lVfR|`*J%en7L!$GV?e!H35!aD{q70qX)F@=lZ)mw2$Z2MFr zbI2QwQbVT?(}+n(0^O(PO!zXwF2T+*AZX#^6XngjIrdm$G{`9$phs;}r=dbcp z#J;Di(>4F-78S{4Eh+L5MaOLu07q%_BKSCX7Sae+ovtRHn)=L9uNKM4bN();m>|8# zG@XQml|KV3KP^#6zKV_p8Pg1BAiK8vugpivh;X6Xyf<}d3<5&(YPTL8Y@uT_i&mSo z7p_4Jw4KN=ulYja#iwPu6c`YLH6W`d>ik0MiZBRzu(ZTIZkM>bg+kn-2&c@XB_<>s za+8>3e&dbN(^$blaUoi2I9!ihW6(``UaNil`O%kQIA>}?!)JM(SHVi}?iV0*)bf1* zhj*ANscaL*llbM2y8^^}y5~B&J9*rzgB{^Gm}`Y-+^M~+#3d??Sn-ZMBc`Byh*NtEvzYnnI?sZTn>U-N+SYRi6+vQrk-Tu zihNZfThey=OXa*j{bFH*nv!}W`yh!a*ZcxI*E#@rU-SRmr5S7;0?MSnY!`;uP2KabualyY=e!c2m9{t|Om>B)|1$ z5XSYB&0Y`WnyJz?^ovyGWyEWf|9(`{6CmsZZISs00-tp0(ePz@_mbpzd)V_V^b;y* zwsvxi1(d(6dtUBJrf6(7(VG-snW;z-;c;lMRr~dI+{`hdAZMx#Q>tgeK{)$B7^omGr*=gh z%>cm)7bmfVdqO*JNI6I!0-=6{zHT5r{EDndDHDACS-bFdV^Lfq4Oz3&75kEboI!+8%qkp zMO^f14!Q*$_x`wJo05lXMP)Z=Uw4}f_H;PM>jmAQ?K-nCbvl`TrrBBHz+60WBk>-~ zWT9sHlEk8FJW-TMHeRKNgj~68en)mN!S~bf0~>F3&o=Y}(W42_%c_wUrEp9AA{?&X zDgw;*km<+QUt}ud!AKK3;r*-^HZ#9MSB%w5%KQlCHnJ3-;zZho^@KHxd4{D`m=H`B z*zeM%$!w?`7Cq#(Wq^cVJbL{-*-2U&821fqK~KdWnq@UuqV4kfmvUFEj$R23e-mOr znK3C$vHzH}11$7ZtrN;f4RQXB z^G}hxnEV@-kk}PUiq=btE!`N{Bwyi*3=A><{@unEwG^BFur`Y1a_H3^puU2iD1P~P zsK-pBP6i4hqdXpTmfMSAoZCPsD5#Ke?FA(STh(d{Yyt2>(&{aCFEvT<7*REM}f`8Aq3$R{3s(oHB_uEcuVx zd1UZjBew@w02j{cn>fi$?M%CK;WO}5bC^U~)PO8MRT3la$P8{1sfQbI*nFtsYh)yk zK$j~695R0}EuVRs-9~d|6;DP?%$z*zRnk%zv~QF9^tZuNWGufg2T7nLp=I)cPl6O4 zPnF)a6LhhBwnNDT=hXOm);f25)gw~Oi(HDqGM{$@xAJ9J#T>-=y6e1NX6uWJN)FUJ zXz7w{IqDLEE?38sF5QFL{usGqrFYb@!va)@Y%hPj`VA5TZibwM;*~irB(2HnqLK^b z`m@A^K2HH8duI+BP0GSrn4WMv3sRW{2X~7%C~r0Tk@&F9GWNQBGFsl(fpQr<9jj@T z(`SMYHUsw`58=bN%oYz51_SuKwh>$(i6<|I&?6b(Wn>pXMuD_^Qc$PuJwDpf|Kb1B z)~Y(Cp!tg&DZ3*>T7o}%2qKc$yNMb4<|3?t=;g&6JqD@pTaqi-3Hnk{%}e{X4RvUO5cs{RLv z+$vv(u?xux{Y*Tc+)4cBXF$9%FRl)~Qr)P(Qz7z^Df{opRvUm?hIW|EIyfVMgt2QZ ze9`Y6&Gy(^(uIY}RK|;w#E~<4UaRwLhxD0&8q-ytnW=_KUITB8g;7r%n?U$A-Dewa zI|woQR+ZStQWbJf?)&Osxv@diB*E+Sv|{t#o5DBu@VsFDxWm7f{{bGCDA_jZ-1X9o zt(fZ5LaK0Z*Ia!Io|ET(YLdFXZU@;9PUo84;RtvN3@~wRj*{oSb90Ti?U zJ_}$=$pAPiR^u_vw^?ea#FJZx0vR&FpT0G02A=1OYq-vjTbbm zPb@N>&RMh9g|5oa3o3u%Rr?h4sRMvmRN(nQWFPU0K)ilh2Bblf|CUvb1aVz(Fz=_+ zc#I^lu@FK0fLBXa z9;JxY$z4B&%a>Aa<%W6P=$_$(1KA#3^S3i1~k*!2*&dTWZybfa>*66-*ANO>2nwXbo-*T=)^Nw z2dQf>g}FRgQodGdYRh7Qeh)(6LLgA=s$#>1>r)%WAH%s0j8`OHiwJ3cicx8$G9-mB zS`l17y789WMb)O?8a5EdT9nqjRLE^ovntC!bmqc>?(AZZ`m@MxXYHX^^v(A@?ORE9 z%3}=3cs&+|i+=Jq%1jE(@##*-@kyrCdJ)2Ek)bX3Jo)=9y7tNxf9Rz7`AOx5)O_5! z(_20b<`J3DBU;491JwRnx+*JtY_wrW-CIS6%y=C=ty5^FB@_05m94kHglRRBGfw6~ z0kC^jC;2f;yn!C9iNkJ0wE`+LlJ2%EK=Jxdp_E^3a$7nVLPr(rMyjlD#=B3Fy9dvB zZJ9`dkCgIjxYpl4mV86HFXh>IG03O)OLf@O>HRUebN6ljCZ1Q2lt)7)8UqJOA|(+U{I)#T`}>(Oedj_ctLVm#vI zyyW!;yR)K%V|%uoN^Lh!3gXtIe^rQdoz7z`MpR8~lsIl*yT|iAw=EU67}W>oEj9?fFW*<#^$?W4TX8&LcBs0EI1xu0L$ z7yC7RXQL81XIw2E2M+%X7LsSR z93Ngc_1Q-V1|du4<`1OG1ij;`Js{-wOdC%}pdH0&gr3*PX2Ve_u8D$nRIe69;w97> z?FUSoPlHZ%8;rM(zbMsAxod00jND!@x(|=0n%w+KtLEfGU)+&R7ITPk@0BSE{#c*( zP_H2G!Fkh91%~;ry3ns31;QL!jPPDQjzfh;hZZi>Y3kr9!^3x^;$i3kp9pMeM7-EYL3CH_zzvWS`vhefJ@E3}$nc)Yw8JE__ww8C!i2*d&(Vp?_Xsl38Sj2 zE!<60<2A;jYy}KYYB~Zz$;r91His;*@a)3^A;hK%yOPn>V_O=@A>s>4#9o#*!tVG_ z{Lca%o|5SZ6bmCCE;P%q zd1V`iuB1`sXWw5$}O=1Gl3V{J~7;jcn zr5sU0wB}>YJ_EZzhy14%@;QdSYMRKnX-L9F7(!#Z)c!LI@y>VL&HC~ z(5vf03j$LPX0i6ES=^04nd$%HiLV^RgbJiZsE@C zkvNH<#c}a24YTQ6jZ>WO34edqOaZsJm+k5`oV0S~B8?XM&V~LB>~?-0lf8U7~8)6vF<9$_IEpVq$p1xDIdA;DB$s2AmH1 zo}1(jXO_L!V@{kt4xOGnfe|-uq&76wy_uR#+1@Vs>lE$a#fe_s7Bw@vYxm)aJ zLA0h5VTz@y7@>;7Gu^8D>GezYV#6+%T`jim;-{u#*@)dZaiFGAgfQ)dD{wFhVT#ww zA%D|cLQNOTqTDJ3JJSH{`(0`8NImqOAErhsAuG>gd;%L-DGuIk$Ezq-k%{_M1gh*y z%i$rHtFs!l+`XG}85y_9%uNDG)2(km=>2Hhe`uSTG`OAbB|8__1%#W{TFizD@6M`9 zN((#U{>u1Q+N+(uVL8fi#6f4Co;0Y7TnBji5!ucPH;S5vb2ooGyd@I0S1zWlO-uGV z)}1t49nEk2k?wg#XMcWH80rBK_+giwVwNhjbS48=_3j2F&Sb2@23?+kyjEG4mexeC z>sq^A5-51k0G>G=Z0WnnOEG`>h4IJ=(js1a#zv5a!Q5u!0ctlGVBaDB$aJpQnAT`p z??$n}CB7|{VN zJTMu4u86@2{s|@Iv74$X{FM%rA0|V^pFu&&l+I^4?xDV4hp143Zck27tv4SA9$Lx1 zu69|{d|c<02ArCysA(5ocZ8n!r#6pW>7XEav67OVCSS?>N${k zY3?a3JU3OHF3LGD$rur4Qttb!Dp#VNTc`SgxTe109uNHP;i8k-*B!|~XsxT_+;k!` z;(^ragF-sNCH|QBQs?>OoF#!W?d14uG&Un%e9|`NA|~!%DZhy>QW=S!7_DNUIfJjm z4!LVC_Cnw(DX5~nUlcdbE?DAHNcXj&Se4ep)g}j`!3$sG^tz|%d!WQGuA{FOPrW{- z`ObEWx?UPE_&7Yz6$v*LBs?GI^uFsVu{de7lHoLMT<_;F?U(Kqn6pHGknEe3T;y)1 zSNHgp&F>e^k9Kd(y*`P-|3H7Vd~cVq+$AoRV!J_9qQR*7d%oi#o^tol|I zXuIj{60@LwQmcF(V;k2%4l7YBEP6O!GJD5yru5F$O{D)7{|`Y0v&K7LI^_dq&EGW! zg!QL5y_%m$(PsHD{W4!yhl;PWvHC3>!dO)Pw_#^w!&dYlkkR?K4*Ja{4i{f~pTl<{ zrh2L%f$$`onU~Vx5@vAxa)0YuL6B^bP zJ^Y1X`*Ln_?^dtH*9^`_7b5_>G(_;V{MG74R6&?YmAmuErv$nXFn}~pgi*u?ULRGh ztT>Czp@q>%F!svAHmn<_f*McaIJkMRJQ+jf`xZ1O%*yHMZS! zveUGn2U&RW4F$|zUGOw5!FB%aWZ0h7$JI})9d+v7G5l1SLU$ghqfR``<$-BN`=^kz)6hHJgxV=uN@~cejp|GOWOxYbK8&>?brpR6a=q) zs|UM7$rU^8WQP3-%YD@O9XKsC@-2WCIr@UalfMk&vTob>O5;#3qFV@QI*-G~#7?re zP;loF6t1X4ik-xMYks0Ekas_^;yO^E4w&`8W!j z*d^fuI+<*#Lx+|3d-|u%I}!2r6*On)Q!w1CB{9% z#>Z>9sxqk1%_sH{clNW)#0%xXD_A|_ za_OYj0pMl$inyx+2x9G_bm61{yW026NIm^K0__zmVYZSV32q^nqp1t%)&baJaIDyi zo}F&Indxn!u-O{!$6f!W*_=m{D)y)#6c8Kaw#dZhD%pqC(g>7%wdMWh-*4~lvOMYx zYBortq!5W40KN&Ct16ovw-hcHk#VlLuvHu`Rcu+xbbmw%+4K=h6Ta{Hmp41#I-z6k_g&qER@F^A7)isS z3;-_25b9AK@rDCq?@E7_<0*(#&-iAbx?MsFv@Dy@dngmAN)6)k|>{?B)p<# z-2lWFek*{8s37QCd%NMh%I8EWk7HxkoVDi)C67f{(Y`k_hV)m)*+0zNbeBkePECKq zshXzZh=_?3I|^Y!&>E087AKzfj%Fa+Z&fr9esq8FT#BA(S4zx!`OOZxnBs(~EZCtJ z>=bT0FJ7Y&Ixyhu|H9sT%J=vb|9!%OD5(8Ay+*g8%6Vaomk?C#`Fvw^5i^C#Ly}Zb z(ox@=c&84YdprFebV~HiQ7>$or8rS|nqMjrm03H>AJ%mEgpgvumArsJk3O2dRS};; zjwJnCU%tDepL3yTLW27~6q#U}wnGAteM!S~08|+R^Qc=uz;m-^woIt!jni$a{S(hG zygfWlSc}R$z4=>rl6B9!$9NUZ5}S@=BT+;+iye{DRug0Sg0<#MUj8cF6XKUM(@ zr-amPU@R~WgFdBVGnV9o0ybSU9lshRKz0BbKkvnl!zFsaN7BQYs0dVL<%mUq-ltOY zP$k;5oi6UfNy3GuF;2$XS@VU|%VolNescPgcRHKpyrYCwwVfx<@R$%;6LbX&E;wyd z_GH(4*WF|4LOm)go0zlXSXF-KphEHoRd4Uxm}wML5IgY1CdE`EJp>bkpaq~`Rn41s zWCbhH*9SNZoxid77|#~Z`J__d;;0(6+-0BEzAa=hZ4Ki@AIanJw5q-)d^m<1Bb7T* zv)BpUANcCW&rDT&`mAQsjjsIpD4<~7GRKfY<;qiy3rZ;GxD z44=n)}sk+0F>Unu{PDaL1jp+J&a$=q&*FVt~a(Vb&h!2XL?J17zqE6W>Ub9 zxM89ij0zSEqH@2pSUS&NBdcNZg1!F%3gj71VcjlB!bND6u|sVcSG9vFpT($drnY96O_RhN z7?MYhG=3Hq!T@u<{ZBzSbeU&OWWZ4Ig5;7CFKD>cc;=wJlLNtM@F3mAbNbwjXch1@(sf0U#%FTCf1s&tn7|dGKk=f_*nZ6s z19W@p>9D(h8vk^Tdfw^&uF2L=wTgJ%);qSp@J|KAvPo`tp;y*|%iwMyP2J&m;wfbe zbKm8y{4rlIlS1e^)i88`tM}BsxL;j$v?$MyF`VDjyw58rM_5ULv1Bl3WGA+EgU{!g|4E4dRjn$p7dDE* ze#2-(NG!HVh89;?X`ufDe@4qItM;vfIh~z{`+3?WYzARQlZ0uXIBDtdpu%(w(KMeJ zVJcKe{9njXizojmDCQ^X(YFSG1$zo@A0Id%^`{hF@6pP=;RPN{&Mkmt`BpI5SPWTrzmHa6Hd%)I4ARahPe+mF zn%jMx$F;KwQPuk@XXt5(6lT1LW7>e!q#7*}g!VJ6WmkmPO`qC%wge?_vqMS z;|~hGe-5s^J5UJGPeg~WSmyzq7w!(}F$ZSH=+FM56?AekJAEsZRPpbwz4#vi$1{6c z-ltE&S>up>>QR#X<`|amWg}aKD~=_dQa9%d6i-)`%$9yLun@rl3@8>k;i1sAqwa}u z{I)C#Wuj;(q_3{BTDaUI4~kVG@^K&SO)oMp~drB@u@=>Fug-;>a)iOQKM?~bMNxbFf9uiJ2>rt{3oN;h1w{@+pxH=)9WxpXIj>PJjQax2$X7BBV*p8yRdCbOo z^LA4*)zS{ytn38W9gy4qjMUvS>j`t%Rhexv(pHnq)Zkn;%-_^wj92a^{UIWB`9FY; zOo9Xg#7wbzaIZx6GIh<_icDTE7^5urK1J+zZS20HI+Z&Wi9hh{*#NkbP43^9h#Ah4Txok7t>y+!JC*!dW+5IP*wJTaIgX*$w-SPx}@A@ile<5IQN z48`rD=wX;T_P>rPo3LBe2wtho0&P~Zy{#uIX_oPRroU3=Kw3e~T zxtmhvHwDRr(4*`3E_wH>kE1*wo3wzexyz-iqmB}m1w(2B$+>3Y>QwfOh0HnWs9p)I z{q9S=RTj=tJ?f)WnArVZyWsff`;UCTxM{ts-uX%XYU;!o3*Xg^!>cM6Ut>*Q>L+5BcGmIBfxCJB)?rCmj{&J)zKxWNH}|;ftLaT~VL-P+LR6bylGuG0!Ci9h7h>pi z-M62CCf*`Q!F4`5eu5kWn*%vf(@}cK zZA=aGg#dwf1MY*RuTA(A_$j;-{Lf$DjUrJ!$_t8b#W{Dxw5|$=&ZIByRNFy6Q&l}b(p^>varIF1`>$FZ5a;Y zSy17z=&>QKFkA)BBO6iybe}$IW(pXbMSTVyZr~9w<|z_lz(kqz|06K_WgdO4^7$y% z#_Zx}Z8(Cb)$E~FzI_+@@$DzV;SLwbax%IjI{jVmf|0S_z6>pdSuT;nYG6KLzWd?TNahiqVlZz zRNYuq23W*s2T39;$vJ`>4aiEpYB>5cSNB&tzjgz;VyH@19BAcUbs$ zG2`%pF!^~K2i2i@kD9O`JPE=q&cw=mH+b@(!G=2BZL78h9ft3~!_id`s{9~Y2$^IJ zB`x5frE~VF#FIz3M+XHeP&h3r_=pJJ*9cr4nP8>JeY|1vmZOiY{;=zh5Hec3b|w3A zE+5;tLK#B6Pr8+UJ$l0VLt8c|*W{l;4~Enf^ydc=vr2?|*QK5*n}4CQ2#yn{g>sqq zwZdTCH9YlF`xk5jrxorucjjMJ9W%Oij;B90oUONWYePLMfO`t7%=8XRqr8?l4zGDa zUZ*CdIiWd?pQ+C-1twId+l`7^(yB$?)9SLa>U1=I?tKz?&#LOXQ^GRI4AGsAZrQ$p zaD@sv@lAxrD?ELE6xZSBkQy^as+6y+oBAjWR&@P<=Jvx~YRFMXtzCG*V?M}{CoFkf z1>OhmEtZ~h_E!5v{&i?7PDrpZ6XrGiAve`sXB2rO#(SF)YOrgxMnA1))#?gU?V+ zJzzN{7X|k}eRa2aQ}MUkcM}S_ISB!X*UCQGl@++KZeHjON(y=BLgPuPhA+I21FNUa zDwdp-fX~~twvN*?OM8S#Q6z-seyH@>;Mpz2L8y!1=rUFqw=@CyE8(y5bmo(bcjDbA z8bIrYJhQi7m-7oW&saw>Kn)!*F6Iq^d5>%Fh5NLK46{Y$)rw;~ zaq6&{>Il-`itSKYXd~?6=xS;rK(j>CcU2*Oh*JXk3|+IA zrYwX$`RmG?YYZQJ5sktC6IzA1kH&1<+821f-Kpw#?v4xqiKaBVI5}OYV;b&1@toSi zFin4|hu#vE`_yM(@?l0bu)L-^d%14o}zgOK&1xJ^$(V6<=f6_Rq+(M#i(5Ms10xoFQ#1x z#n}p@Rq0Zva+r1B5E;Mywx~8q?9yxdH^T??GY9$~4T`*4S$lqWCLOb=gl4C4`MmZ> zoj(>Q<2#rx|igQH@@ z^S;#|lDMtqh1cEprj6T=D~qfNzXKgcAe4d{xXh4j1qp|YI3N$bIcp}D?Tl&N))Dcg z-7BQgK=DWd_9N|B_zL)t_>gC+jU{i_~E2d4D@YrQRCMB;o=jI8T%4LDbH=ZhuJ|Nr{B?j&rq?^4z}R@P%r7 zWx>EM3g3Qbq7a=O0)wF49oVIj541*o?F(B^O>R&4tJ0S4sUU>lYUMs!9MO_rylX>; z?$*ch(r}>0bAZ_3bv-!6OxjLf2TyxpJBev!v`s%-`I68ckg@_x-%1vUqh}qT3&cB#n25a?)WpF+|>B>WER_Cd?aUcgs=L`Mr6(*c;DRT?FdM0BPQkQO{DtlXK!R?e15#gA~`V+17@r^ML z>xE|eGvu#qmYe}XNgr(xXO%0@?e^*#dV*u}BRj&>Cbl;cP~y2(GVMNOMZjvj1WvMHt6**)Q0p^ACP`^Lp%t zfg)#kWz*7=LjuY?8K5bwz?ZS8f8`CH+hIJHm7~zxS)#Bgo_PN*^Kg~4EM9Dw zN$=}0Q9e*ot|%X)JV0~k`Y0h4#gb0_}pk$4{*wuG5nee?^M=1ld)BaVX)1v(__PO;fH0Ri{<#D&|+~X*uwNYFy9cGpnH>Rlp{d z2TMwut1N>3Xd5>8m7@9Jd}8H4z~ar^sDCF0KT5SZ>b&*`A1>nAZAN-;HC?lMjl8RI zn+XDf&V{$Au#~tWL1o&gqho{4lvIgh))pT#M6c2*2gBbk$uH`50qF2Ym2>2u@h0~}u*mv7PnLoH}+P=RaPD~Jp0MhS> zcl-z7hA(aD(xS(N@q&C!F9%pJ6g1pVqW0bU`zo4}yCJ(k1#NzUf=c(CdAH41T4DAU z>Lm0j$a>!u(1qBV8B-E+sEB3kncNIJR;bmz*6rz`4qn?1fbP!jwtflZEuU!S7h%7F zZ2jw$(V5}AmE_u4|5KHl-R!nK+fb)?9*40@y3PgLvGjJedw^?8U~l?>@VImJa#QT< zn3BD(Q7a~hB$<$90+*(?{1n1*kv+6iloKQ+ig$_*8Ht@~3NL@#o_XtM^2nWSkvy`7 zy2`!G9?&~#jL%kZTq$0KlT;4HeHe1Rp^6}Zq~HVp1L{B(zjNu>{RKl)6^D;;|A!MRefw|qRmY=EM4H*18nN;C)DeP|9>&J5e}JJu}K za_flAFXgKhDMgGkE1@_`R_HJ^i{|y>@2G$Zm2rqTkgY|eG?f%oTIWb`g0+PajC_e5 zv5vrYz^`OZ1zk?Jk~k>TuE+}sbSWw+$vweI#!f*49>De|vBwoHT*Vav!ya!}3tB-b zPB|c)@IWMZ&hRvpJ@hl{$gsT;{wzD>_sVKa&ZK9jE zK7sTH;*SXGZPQs(Hd7xvw@MuYSM4Y)^*gP04MA`@91W|XNaIC!tpvuCerF45=64Cgz#s!-kz2D zg3^?)*)4pMr722Ml%*+3Qk11BN>Y@ieybh~FG>C+Hf_M94=-$V3KS5cq@Il{X--ag z$N*$^$s)fhdi^83F7FC#yGykZ9CM|Sw^|vVpg;=g(^j+)IsIYVJag)6$iEalNwDd@ zC(_H@P319Xh~gQV-A*MY1B|hPDYB)(2uLoSMC?*k>0YJyYs*)8nU0}bS-0Xi&6WVA z2%jU5;-Nx2k`W>|+iv7^7o{}=E;PtXfkY)F;P5!-JoX%Cu^yoFSAO4iZD~pB1a=@1 zkGTEZ`yS-|b+MrLMhlT6DVd#1a3FwGI)Gjd2n7D9IPa0~SsP{C7FL8dB&57R;H%L6 z-n@+c{rgu*$$eA6@ytHe`+P##&k>f@Exy}{m8B{O)Cn4hownQGV@o%5>iLV8$+vX{ zD^f$yU^#1@1{r>!7zFe4rk{6Gl?f4(r>5r{hSQ@pZ zbSSxyqsc1953jEyIT-Y*@zf1CjHSRq3g8?JgZ(Gp0=iOj6|0RErR3}+d4xgJzT8jm zo%gO}?p3&Dz>-zX^S=s5%C1mhr|arMp(V%DMEn3BVdmq(u8 zP*5u4r6hx%bA#OEdhy3^G+dLYo?=GB`vocb|d{oMI}eda0B|;Q6wbf9D$y5jMvIsSK<#aD>GdSM-C%n zZl^wtaK*~?Etde$vPy~aNeVzfN=%3nW7_#+r?xlNE%=t{Lxdquw5C%bNg$st#EFkM z)wk9g16iLFTuix1;rEr)m5_$kNm`UrQm+31TpV%tROT+=%!1*Fxd03jRUyS82>KG7 zp2w5H7~|`O+D~3gvs`}Ahf?rP2T46h0YDt{&!#(f=QUN`welTp`6^m|()Lx5Mn*v( z0ow#))41*d6}U6krd4Lji|?_CrD`s=w60Ww+BQn}2T@D}z~M^L36qt($RRf>VA{01 zRW^Wyq;4FZ>4O?}GE`)cAb@K7wY>JrdKGMu($`bZho0&yL#c7qDMc!9l1h58k%8(y z=oxn{7px_+ee4B1?s0i^9<+~B(EGVEY$Gq24PfiF($0v{gtUK1b zFT8P@LTxIyY6VC^4KV?3y&+0J`HZD8k@`}2=?4IVjw*GlHwN?3A^10`El6owj1s2uRo-$54N=AENadY;Iay35C=xe_E?zF$QwBMSI32c^z!SB^`u_o7P+q~;H})F61T-1anWmQV*5%(<;}y%{TnE96s4uKp>I6#B%zeJ;(!T( zgu2=N7qJ^jcGvo){Vl2Ya4Gdi(03c0)*REJlOaBIhT{gFGL)uReWslbvZOw9fr{tT zc=zj3ZY{!7w>c#&spr(w9%|(&j<*z`(jSr(_mtQ@4Wzk%gr@)i0Ll*>{w%cy{{U;d zr0bQQ1)vE}w8NJ5y3&@rGRgEv zBxIjcUs8HSk*x6CC7e?G4Q|c%+GXZ#)uGoJVWg1K+$}omW!D1K=}(z#t;Mt)SOHb@ zDNrO6^c+;q>ACl-joYM8Txjk%q^YpsEhP$8Gm<)h9Cjz# zvrBJw&%&mt(C{tyy;zuKKO*_JZQce$b0zQ=!)O?R|isFuA*ls(8++oX38kg~0 zy?TpPHl@Gdy=v-PeYLc*+m3jON*sa;l9ZIrDV(XFZLr%bECI_q#xB%x2Y)AG_O3qr zz*~o#cT26T5v>iSu%#oCl1{3}jX}7#WR#*6$;Q-{(iFI|Ttb3?10bZN_T=&~azPlW z?T3GNH&3$Lw%uu{B4DQqMbcl&MnXVJohbXl`iYPg^{^F?Wp z@d;B-EufWjvHHtOLcsMN-KvzE;lFqxLtK>T+q~8imlPa?!re-c+R9YmD(M{|K=bX$ z^{t`2SEbk1>BHZ}a@*Qgmfd3!Rhw!nRE;WX^dbthk^u)`Bmj5U@9cV<)ynUl!SUp_ z`&!#u4BWoRZ6PHkDq${aND33bDwzgFUrjIZ7x7=B6i%PrYvKbSbS3c;UIIWP{(6e` zag1R4ebtuxeeuWfQ>9YMH8eXGyYjk1jp5b6E+l#@@?=JFkUc&9^ID3TxBAZA2;3)W z%dBXGttLaU)Rw+QDJ%K2oTW%oNFx~snxq!XtuLWTQWiZFk}!V|D*=vtKKwrCwp!18Nm3lTw!g zA&aCbB`|pinBak3Jm{Z~ym#Aadh==RuoaZ~qfgAZ%;WiLPunFk&l$lWbAksZt|k8f z$3DP(K%9l3!<&tynA0&AsP5M0H2sE6y6+Y%o=dUrGbPV@WJqdM)P=hcw(5ElgOaM{ z0bOY$IT`2I>*?F@_WhNxUxys$A@7CNq)2`<^R#UEaa6-?Jn4DKuoRTG%fPhZJtqlT zN>8Zcjunl374#>kt>Rrr$eC*M0L=Mk4#aUx9NXgemw0;@4ca(0s>>1lpqG3$)U`Ju zWexqCLW~-r zJY!#N=-nyy-8S@eSfKSsgAx;~RCKi8mY>rg@TV3+l&+w#3W!Mw#bnRIf5Zlg*ggBY zU-i}Xt1h3pS=S0o$qpg7=~IuvaQTW_k_QZ{)Zh&EB-R3WgnifdUsf&oPfC1TZsLlW2xSgip`oQqDM?5I5~TqHeWdp{AL2dXDM@YBZqA%aSIG*Tz~dx; zr{^BW>r)2)Azl)kAEMq3F_h(9E4W9=^#dVYxz2Neir}5c-fB_N%I#~@D?-vd5AJSx70 zEgQk4kNr!RHc~U~^7?VX&+j6#4@GPJH@SLd->dX}#k;z<`4^V1-MP$rW>;89V1}Ua1|aqk&z|4CAK^V zC%Dtnr${ocPcrSoYtrXWn;C}6CS%0%QnH1$rM?n# zJBwoxyR&ug9o!<*!(HWwB>s)7_Z_!l;G~Zp&|O-nDVLo=ujxuxvunXrrD_Y}EAbQ7 zo&`6jVWUUVmd$szI{yGn>bV-{O}gN=Y8G9y-mHlMLmtlKV=5``#NRkO%x50nzqf$Q20_j%Eaqkfp%6%4Q;Vm|jTWB~?&^iKjp{DgEB|1{BqJM)QI78h3 z02Ih2AV||YMx3ccC=^|_@f{#=qm^@>0Q>8gYUscCJvyaYUtV~Xl9S8&duu=$!jF@5 z*2*~hg8BXYRwLpsFrzATZ#ZTbNlSQP%da?6lA;@PHQ;P60N?65Wd8tEdAn9qp*VY( zuT`?H9@2F=a&e>~JW7+7ad!vUG)e6PeWt%b9lpP`%M7;Tfg~jylk+P*vy5XW9k4Kc z>n1GMrj~?dc*x_Pee?bY?kAemHrDSfE1Ru}63Q0ZQVvv}m2h}Go>PqadUveN(_51r z)S+fj4FHjnp~xe?N&Z%ApLCC{*=sal)^6J3OgNP#DM}Rbd}M^_1i0BUf+7x}36AqM z-1>*pJWAU2^u21}T27@O4M0gh)iLIteFW_t^L@LUN=WJ>JaSJb{xkSvu&-k^wI?A! z1K5w&8M z4o@769y5<$Z~1D|>f4h_VPPu?;XtS>vb~R?89aS>;=V7sltzu_CBw>9o_&YDGm-d@ zPg=#DVY+ec(p_oE8D+pytf@aL#&haBlY^6ypL*rpOX`LNm9W=d#c$cPWTd3JNHzATpJ#b){iLD4 zp9>1fNIt0qV3V92_WQfzb<=Xs+Jc18UGoZ#RFG79vz39{Bzj)N8vbG&? zc~2$6g{0)CsUxGPm2e5h2+nyuhd4*9uFk=E)Sofv$y%0|P@)#vRDM;SoMjvWGI_xy zd;F2=-^3N~=R%mvx2VLd9e60AH%svuaXuP~2I)#tw4W=05~2w01KU{#A~SPp+jo}1 zo1l=8OGzFe?GivF9f~yworIBDJ8|79bDk|3IVlPQp?|BKfJQJ$9)q0c>rp7x^P)1$ z&ptp;NXnE?O4Wgo2`7Sk;ACd7NNQ(7lH*epvF0Rl=5@y$DpfhFR!KBXTK{d=F;s${99hPKv+^?C3_JN2_zkX z5+)BpzUk^M6K;!JR$OQ-rS!&y1ZfgCQ$@IsLfpk?3no%$;V; zmzQq1%38a^r72Ta3>6{u_(*9=T8Q%kl)^&7fD$8m$k}x34HnJ1;$C9mGM3iD^ynJ= zK!6VrrAdMb=eGH6a@*Y>sqMkI?llVLrv7<0xi3D1p-KH4jR)Q7StJ!6q~LMs#bK_k z*xhuA_f%>9Nfu;w3!Ycybs?ADAh=fBXks&gpGe$ z!hlE{6}Dc?ct#(6h$kOq7OXPm%8`hrPPW@E{*g*e!orj#0utB(DJp~)*w7!q&xXJhgQi#f>2+&YKqLT4N&!yI8ao37h)~uD2 z!g2|&*_pGRSHklJwpzw;oOIgRGB26LE$}<5tQ%Wa?k&dEb#4?C<4q+{pA5CBK_NwF zO>q3Vg68Y2D&rl&Zfsn{F*Ec>T2?5-Sc5G)aMD|b98g<+Go(t2(&0=J0$|o8TXzcP zEs|tOxfQ;=BzcaI=;_FB001%1D)boVwk4LW)YdR(t=oQ~yF@ukfQ@mK%Xv;pctffB z_D>_yuXd&In>yV-1PdmvwOtW%ZTDD8-zixPp$csTHykIa&a{Mpc|2#xmjP)Qz&_+;5!_<0Y;#9T?%G?QmGFkqeuX;Qc!Nl@9ug(B zBsF;3KqU%gVL->#92%vGVBJ({Ht2T;##~K;lGCnQmEG%kdRM7yWls`ZOUcs`AzMfU z3X09+8-FyC(CvqEwHzlIQb14Jl=?wQ1Cby)2*^Fb2cG=aNNBH%=ZvivC4{-JT)*4e z28^KGOlPBuD@ zQW8NVQc|EoR1`MVZt?8>g*>G>bB|%FZF)*wP1U*r zlLVz`c?m%kBwp0|9xQ>MoLV+0v zlaQ6;K7-I?5#PVQPip?8tfb>7>OBWOzjyd>D~aR30ACC^)yTVNAu9rTE}_x+4;sa7 zTMi*;Qcr_%b#%xDKoW$ult+{#i4~i)>o#_`WLvv-&DPtwacI(=BS`YgLA~I?QS(mx zn%}33#&*)hwKolGr01XNn^i;o9wNxCD@sB}6O`q?PoiJs<#0YE~i&-T6R02rlC|1UH2d$Dsnd z2SxCYqGjB`i>3IhXEEaVV{+R_`aPx-aI8IxWcX}bwh)Gy2>~b~H}MupHB+hgs7@!A znzL`hX<2&=zcAv~U3(F2H*PxSw_IA5r6>|qprZgl2^HGchL4D+hRc4;?d^|L?oFQA zeY9zNHM(?Jve-lIE)B9ATrDbJP|0an$n>m+Z{jQA9pI9!-qT1@dXxVEKgYoZTodyX z#yfp`gIq7`G_E_gbw+W>qD{IQmX@7x4E>#se^__wDhIcGaa3Sc!Pi!Z@@6b0Jw*A= zgMdC`!tem(=hND}Rf+!qMn_3=E+K!9jOm;(-^2&PlZ#7f z-W@7O1u9j>KRrYo9(^;6@t?mHUg%F1+Z(!CqArin8urJj^#sWBnp4(JKFcX}hVEJmX~!Ce5QQmi zkf4)VZg%*V%$NpQ$1d&gY%!ccyEWTb+=aTaZ+&~F+S=qcPKS-FuM3jWp)2r|g%Am@ zk;y6~lahHP*YcFFeA?RESle6KS+LEmrK1em+S=Q;hFG??w{0yj!wohPhM8rgCAQl_ zLJ)+dDF6T~<@@iybod>xm+MfRCz7C9q`Pk?KoMG)2zA^ z(Cce$N=egqf**}$)^AC(?dOR`?Hf6{(mu}u91 z@>Gipi@;-0GW>}4wBI*w^R2k+Xe&!Z1%&RP6bT1UBnUGSMrNo44mRQx7KNacsU#^Rf(?3A3XLlkJYw%2;`n~q z8~*R@-iEer6S^_xCu*A&#!R@XiKP^mTc4;SE*q(N@nw`@J8+DK)?2i_KIW$zYS`i) z%6g|Yj+C+M%_FL`Hl~U8{Wq-i_;?m{&3!?L2KW6%0w|$rGonq?Q=G2m(7TxxoQ%*X< zlv#PyFLdelc*eSQn=q`^f!#T6bzu>cRLpp~->P=6huS;8H9&N`=-R4g}}LTRP(o=(q*e+*@t{6jFWHpL0HB#&Aa$dz)RwaQowK>ag@Wgfd$Xu=TlHEFq@c zC|E5EbS>u^i^>R{ctvp_=QG{{{T`tRf4dE5`cQSAxGvo;D0}0YdJK& zpk>CJ32+bsK|Ndy75|$_Vgm&wqm^RLryFVq~oj=W91nfjCSB-BN^|X znkRGNE-YZgzO^kW$?8cU9>qf>4?*~2j`j23;pgH9F5`GrwjSmn+jo}DG~L55xR#vp zi~<{JWhyCA9f1ilI}(0|bzi1fn7Vo8+bp5wxLZ^bq)c+$8+nCPTl?L z;Xu``E2Ay~mE#I0p4{ix9sd9y!o8_so3Q(|C2d@`w8D2v(n$Kd5F#Uk_Kl{zv54fG zyJf3WYD^OWNFeR=>>!cb(gzor$%z>uWRFlgejcQKG52IuCwRFfEvz`PkPlBd&mE8L zC)fGvK;`|lwn|;dC?o(+7~~JHBR{-%t0S&8$cD>`TjdK#QAP+kAo31Rus^h7u=%e$ z;n*G;{pH=mZL-^CYEqVvG^H}2Nr5}<{7I_$dnjDq-nP;jQBhKOO1Uw-MEZlk;$9h?rO$xj^M_G+qHqB{K<634PCx{6oM*Q` zE50B;A*{oxdnw}VUM*`Qs7~TF;iQ<2g}L0Psct>2-`N&dIoPMw*6UQEy_pUFN`ulwr&Eu`yF>Z#^3xR5V8i*t~2HsLg z*eH(s#@*rmoU?Ce>$aBdF5y5eAxH?<^*|biut5WnAp04N$768iMs7q$BqRhhpj9Eo zAt2y_l2MXA{AW1l2B{eP(`b&vHkk!1HiUUkIHd(8%qWoJ3dtmrKs~tQ(ySHhT_H?5 z;97pr_$pZ#$tuSf;B@d0U{7u;$-RAUWd8syPM)PJ9+(3sfsCI20MEz0TIOE0<9Cg+ zj0yh$6_h3K8BR2%kE1C_hRGx?0O$e06)M~r2ByP0d1CN+SC-0bevN;lItpD%N_E5# zudJ;yHrV@CZAgDbYYB>d^JtL_up4zu%#w+zS5Z=2Q!S|t0#(wILh5>`Dnf0}JnG9}DP(aOyx=8DS;)dc z!6!MeJGrQIKZl*18nM^3rnBA4q;;E(ibdLcGBi@0LQqyxTY5ANLmzindUXciO?E}rV%{X5E3LyN z?5RimvP;2I96vDxxZ;j``qd|)Z%*l(hvnG-I<+*kM`^-vl2D=MEbvDQBw&$|jC~(6L!_SAB<4Mz$$ZE>NCB zj=K6A$OWfg@>AB}2{>#XYuVYu9KiBE7iD2{gyUD17^91}x;Q2j_9OIW)Dl+h;b7}+ z4k&Gy4IHkSKh`hrTmu#57#2K~m9!WZU2Hqq(834e9wv|9C zkShpY+{@JaxJvE|YC6^mnl2wDAtb1BM^GI=1D~hXzBLBp-bAt>+}>xQK=s3t>trvh z(MVDW9k6@n(>0JW{3+?n<&hVSJ+BxKDjAr#v)02-1oV|85YsLmNWkQr=i05>d@F0) zlm_m0nKrwFPP6CrHv7swSn5+C1_1LCFi1b?yVbppZRvxql^gq8AE1$D>sXDGwjmFw zlAUJdge65mrIf2ugeGLxO7jKkv9)yv4#aOhpZ+?RZ7xOGHt7fjC`(SSNW-yi)|23+%1Tvx0 znJI%WgpuneBgFM~IXM~Z4P({I!?y3+ZN_r+W$Hv=$b7vTI^058&O?eNNdYGv$82LB z*5ZDI)z-LePoH49#EBsxNRa$>C8ei~DJst(!_DR#H%mc!)YbC}fut39ib4TMQ3tpM zP)xzC#clrp$4kWim_&4Hn>L--MZS>hmuU~cTZsSyNVY{_0(m43Gy6tqXwQ$eo#xqd zU2RlrddVJobo80iLPj!rKHD*pteoI0o(DavmvuDjYw0N|Q2zi-lfnEz{{R9jz=K^GI9-fD`_VIn6 zr{AzxO_JuRNpd7%bGW%V`;=xAA+)%Y8DlC)Nl65Q&qs70g#Q*S8I+XeBsn45_Bj(RoR2vWJqrC~YV!Qj|$N?W5rV;#+Tg zKW;9(dvNs|HX9}S*%CWarZ|S9sn%slcLhsQa&n{$4Dv@Fwb8pD5zh%K2vYZJP9-i5 z#(Z!_M}OBKAHV)y88n=9Ic~n8Xu^|kk0lQUC@BR_sElC$0Mz$6^_X<^HSUxmPV=SX($pf&!mG^{qj)rmy)fm)q?7CX zpI(1%J?nwgiS4%Il4Z_s^hsM?FS&(v91Q&V$FUvHaaHEZ=-bxq?*7~Fwt(Y>FZ6p$7Z**w_I)orqd0W7U*rb`;NY}MTZ@h5VxIm)g=K= zIJK21&8yg5F6pjW=@v)8@)lFKQ&?6DZc?j zEviMbWaxzw7k?^6B}h?9(o&?DDNrDT=t106X+i-69>ie%#+0i&+bcXu%I^-uEttKq zzqPoszO%Y%4_jT?-88l?ZLS$`DYsaCmlo5lEhP>%wIM*HkyjR%S2j0R*Va$FYk6mI z&DEvVyNYT1Ynyix^AED>2wQHl>JZ~@B!wYqN|HeU*E}9CJ}n#n0O8+ub@t3FXEba( zmXEx-5aKLp87mFPoXzi9y#1Gfd1A_$ zv@Tv-Da%XMC+O`mv^4w7IO9kva3u~UB!rZySW8M$y^~xnl%*+3Qk11BN>Y@iDN0h6 z-1fxlYjkb3+eT$AYj!?|(t)xS!(}!reiBDe*j$Z|M23V*daY>XT1}ZH8r1f}TM)Sm-=&5znaG*zTu6?xgrVGX$if3x6wUQu3!A zuheWWoc{nI9Gu}p9Q$_0PBfcf-1$zq6@CQoErz2R!$|`Em5Gm+ZaNzMU^# z8Hm61J94MfX5)^nq^3|1CP51T6%8g5qxDbqy~UaOS%=~YxOCgwm6Vv!kXNJ~lBrLa znI&Y2>^2+k5+?QA*-U0#mXy33O4I{`uAKvu$$Q74Nq+wK(sW zfZ!w*Em$0${C50!Bvz0=jTJv3L8r%@l{S-?2?4M%-z4Jz9C66*D(q?9%6;bT;Zs!F zQ6VZLuL&eLeKXtsT!WhE7;nY*Qm`edc%~4Ctya&1*h{VlS_<&WU0C|7DMA!yPnkzx z008hzbD-9Rlr1S+Y75q}q#5$5MIZ?wDkMk9Zz8j6clSk4vn{Cal&z(LQWQVR@S;zA z=NRW4`toZ7bvEO^!-iW@RfH^uQ`A6KPIKS4eD>}*_MUDAs(Hn!!2VF{AqoKGb-I+6 z!N|`k&OL=+&1OK zHwg05pcE40ML?;Qocya9KP#mrNzQVfc=WW_okrd5FTTl0ag`}cU=kha1ob?}IbR?W z7N7x8;~la74y3tRZ&DhPqU0A=QsYWIc_e_P05S6*aFdby>)TOEL?pX#hddz;guNBT zFQo_NC2iybl9isE5J4$9;B&Uv^wq33`KH@*Si4J3*xaQGaSGH@Rc_{h2wFmkQH?3dbO4lX=<`jr5gg;gcGM}Z9oyY5<%t+gUwfmT1$%Ib=!k#VF+4MwRE6`B;YVY zQU_lIAF59$2Bh~5*yvA|+RKg;fO@&dIp7i4;AHkbnXF{JZagVbUP#}dQ=^zk5P${TiU17GLq;YK}w2~!S02*MB00JbzJ8$*3ir55A zY+Rb$l{@96s1>D0^^QlWCmy)Rrzg^?w{`J*`q{V1Q60ewIpYB5xWUgO9Cz#nN_LK2XylBJP= z2RRjxyob^1nZRKKI6LwZ`4(o&(ONthr(iR0R^jOOgRi^kc0<&mmFRHdXOK-DDn z0w;fY?OR6L$?$JdFOhFH*#7{V*66QZH5J-OP(*udG^9yFRt^&K(C<Zr`g}A*U~vf|Th=P_=EY?kH8f)^cF#i?ZNb z*3*{>vI-KV93=%yBdJOmAt6L(lzkkf5c|L&2V8K<%Pnb!+eU$_QkAR( zf%8K;W|53u-(4=o%@?<;*Yt+GNw#E@`ZOX!O~aG=N7t=E5E2k(sw*t3#3o;A)g|Uk z_7@%0w}lrQaPo|;B%rIL5Tq25+#f^c&EHhmUnpv2#wvokic9W*a&y4yBa(BB0CSQ% zRc*JCnUQpf_L7j=2VZRR3Ul0JoRiPueeb;OKViEx>ZJx#(%O=h^rSi1VL%K51~HTC z#%jYD%$9ggBCAWBF&9l-p$=R+mJqd*J{x+xr^P4$k2dKXaa#^n>AmJ-%fFUz{B2lu z-LuPT*@Zf`+gebRIO>CmaVlh~O4~?|2q z^(Yl}5>?L!6@BzqitSNjv;I$V+xc02WUf1oI<}OB6)9^{!h@iae^n$W2R?w-Dc|El zjXp{v6saw`q=EjJ0zfAi@ATt5inzOVagnE=H%O5hFR7Bx>Do((S{72GKyfZ7Bc}l4 zwtYCNS&u2@Oj8Hfah7av6nr;YWkEJedDMVVhprkybRRK3C`#6nNf1djhs$}hI@n`B zL2EYRHtnFe%dV9h^s?DfP~mBp^(G}L@`a%)2>{Fp*anR8zo%sB`F0E5kg`HtdvuQ} z`B9Lf`-*d|Ofnqn3y`w46!j!zC0Qe}kgvrK$!h8Ft=G-EjU*_RI>Z)Zo}#n!10F+N zK=OWU{ll=sNfsDW{QHPZmexy;NJ1KKp|vSwsY&M`5`LY>7{(|S+M*ZTLoYhph;uEK zgrlnim31VO-yOX>cCKS*(O#PytLwWPY-0<#d1c~kaeMYnt<#7pacOZ5hYYBNY19)F zqyVC8qFi+os919rZ9Mi7iL#=UX}rU29ZlN}=q;gbl<7$*8cK?F9i~VIwo`7ucqr6& zxGY_)c55;^f@UgI$!#GkQn?Ov&OI0hoB};?4sn`ZAM{^?oh`iDD&?bf9-fP+zbQvl zSPV~j*JSk|tqeBPuQ-=dqtbA(f-u`498JsiLNb<+@13ZmWYj|;IG$ELEr&}9-m6YW`2jd<{fnY8`SQgT3TD- zH@-d={y3|f8|0;>JQ#gfdWtRHNLvocQADEk5>(_SZZxFD; zlW%owd_=OR+CxC=xI&5?WT2!MQ!r#zn%igN+*wW{-636%l=Fqf6P^w_M|@Rt;ulb}tM^A&T_B;xE#~z# z5MwQ-nkZ#1l!8##%hcgXa1x`!yYq^(7N?gQbW(DjnF(-|k_f;cbdiogInTefdJ_z3 z+q|o1Z1NDp{{WI&Z$ek&8+CmVEui?iQXN}D6jqcaDp)#1L9Pvcq*&&wi-wDiurV^? zmb3=^S-J~YC>p#ZD3YB8N>Wp@i83o__TL2ie|mb_+W0J z9Vsg4Kh0MmkB4rSpEXhc06Fg|VYq61Ipjg|kP@_~sYe_PtDZai`s!X*uGV(PpNAIx zc4?A8Peivz=+uyKg_ZRu8O8xlJq9yTR;|t4OGE2iEmwSL$t)^5%I*>t6w;IwQbN|S zk<^j;pd9-5t{;EX4zt`h;^CcWO8~N`Y8mMlGo$%_}XO1!Q$?U!LaBZ;rj%{ovbOercmhrru%pTuZFH%c%=#)}2989BITUD{aInNm^7Cl0gQxJPR*pOuua7dCxeroUSgM zLzWke;mN~(io;B{+pI05y!VKux)`XX>-LD&7B|E8%|WMb58J1(dXijW#j>D@aAbsrr(32sA_Vy5ai+$R z9mTY+hCV*JTT<(7Vc>({X5*ic_anw+g3>>&?iT%w)7K&9EEI0$n_=?JCRg~B}P5B^w?Hx{4Bm9 zJJsU))jvyazRlkqnWZ7A68G~ z)BgZrHheUguQ?_^m}RK-B~oOmN>NB4b$WUl=906DSCr$qT!Yb$hKs8zB^~qBuA&BV zFgX3SgSE1BlOQ#bn8irUIg2q}VmA0YH&;1=^~<#<@@d4&OSemamQ?rCoEIQ1ddp17 z1snTYt68aemE0Kv2f?s=YZq~6E!x6u+hnilslvk3aHJ992L6hI#Gx8PuD~Hwt>JtW zZ*AE%hW$M1${TjYpb)Ga6$H-JxdV~O>cHeNJnZkxQ(Cqnz5Ty&tTp0u)N}(B-26&J?ygBv)dsf%*l*@M?5WPBO z(Ji@im{Ott0Mw2?N9_aUU0-PJ!b1v<-I>KSvZSAxXN7UrRC<Ty`|)RTpzbpHS- zgO5|l_p9Y$+=*s0Ih0&=Z>eiNU#gN)u9agStAmVZC+S}jZnCa+$v2MOWxPh}(UmMG zPl)g7fFZ>f5U(myASO&HMts0m*xbp}tUnedN@=Bz%5+QG)Sc9&b_oy?CJ8%@gl|_! zZ!Bixt>>nNMs1|wYFHf$Rzi@TeST06zus!@X^rHy&6vw#dJ=^pYWbEvZ;+e}6YKB$ z&lRP6R_BP;hEVLDuQf?ro#kKC|Ks&XmxPp(5*x7Lkd~H)4XM#11j*5z(jXu`S{gS( zQevcv#3%u!QyK&Wr9Xi3z4p6%{R4aK#@>6q-sha>IS{bLFbpgNBT3T>mpFE9pAIYJ zWt=MK!P3x$1X~Pq36kysf`(R0G=))p5kjFgxzn!6$*c`W?$%0TMvc5)uUE)WE~nW) ziFm0YG52zSiDz@N-)iMEgV#NDYmig%q*c#BVqzS%#It@U^Z5R6`K zG&|>Bj2z+_o&MV4#eCYPr|^Uo2s8qWuOE~wYv~191D&H3CwaDtoTVNo*RK{V_Y+MKV(ksrU(DRukT{80M{r&ZXzLo|G8AqXwG3=E4T=mwvCwF6pzbOiq93pMf^ z*MyE1524Ai+?+5Ff#u1JH9zW*5TWgu8BT-JVO>Q0GsM1O&;Yz|Nn~ls1?Bj!17U>0 zEul_B2xdyh!Y3!EV#a@gUNXhnbKPFn3ey!^a#%fkIA>+IL*5W|CNewI$9P)i3Pt-D z%wz)c;W=DO%Op*99vuvEm<(o8S~{OiB0zTavF2t7x4p^g(luo1bebD(VbovG!(Hj{ zjufj4w$rM`>?o5)F^d%$7axs@W-x4HWH4m`aZfIoi4Iga?O+PRvFQjYPl|Fi8$#=SDNv&-308|e)&lDm(i{~nPG73M44Gf_4>?2ikKP{L=S{2OsZq4J zjv7(0O_QaFUdDwVA~S*%-N*c8PGiFb%P#)-W@U&+ygS}1n)zb++bu4WYf)JN9WMm7 z1&C8^3!^n7ZGVxwg?Iln2*6k@EfNayIH#6FQy72^0o(*vB)?>~&dRBW_!~y%mX}Bc z5+fZ$0Db3TM+$gC2i-kZZL7BXaxEkfAo0pAPR7F$S84-dZD5TxSbJpqB+B*EkMZ@S z7CDvsyb^S?^%to%NoHOxj7hAwkB7f3JZIsAR)I9Lc&pHv6Jj&oltZPoUAe1avEi%X za6=9a7j62p+jkk0vBe zeT#R|$qSWa!qkc9#J0q={hcjo9upSnznWX&>ZhkM+1RY%5y9Hsbu{eD9$MFK`X&&+gY&HFv3{qL zynBv3LK)n8j1SbPXynew8Bc5I<}v4~qT^$T&fm<;XURnn%&**=qHvuH)rgl&lQakw zfVa5vdSGi1Y&Zgro?_<3x`{3Nxb?!*sY$A%;L4C8zaO8H9f~TvIR)nFn!6$vq2)OW zmFOun8;rDh`paxYMf=+iwRI|grN86RTh%S7zP+TnUwJl! zA~^h8(u`5#xEfp687UIFDi%m+sas7eW+yu)LS5Q7N>-K!Kn~lsKX7iB?W(kUxw~C6 zk@&TRtLm+ifd|fjVM%me)tfP+C^N^N8kmwq`$5AO23J*4aJZy+CZu>9=C|A)I$HnK z3s=Z&%1K*0sc#gGf@vSZLx5*du)PJJ5;Bj1@ik@ z7p6_k-dg?7Q&@XgR>k7`*yS^DBb||M!@ho5M$YF=(kA983^9?RxWX_wLh~Yh?%(k2 zWhLwDh8BiP1Y5XgKs_cBN+lBDAD==p9V3=`(XpgIVA|fY%KN-j_BQ5aOg30gxL(Li zTLw;S&=MtXRvis!8|Bma9j}`S|GBJ3d`iWJp}s zt(%Wm^QJ2NjcG+xMG+%MFI(@OuC=b&mV^gzRPD?^{_W=1L-&YQ<@SL9oRyU=XWGI1 zw@n+>Y=b=@*3R5i(~YufoIQ=JVcy3MD&?MHk$CRJ4xjwFtgM3peX0m$6t8Gj@*I*a z`;B|(t>aNQ0`EfCL2-D7<|5(G)z=>>jvG`zrTH)4AAlYoi*nRKuA%atmEONh&&%;7 zT?GhE8g2Jf-aZahe{kO-h=+ZDNa=za|NUwSfAqoB80l~I!fvj{=Lr}$b-=oCvrlr4 zf*@kh3+7YCZi*Yn?eW$co)Q6cQL%v0GlK+fqpyM!Bk(t`YWhnmvQq4oicSJ0U#I>7 zy?3W6<#Q^;$@QHHHf%vf#H}Du3SumC^l5F;9^2NkA=(3`qvFxS<=hC?LUcu0{If0T zrK4^NJ~F6L8>a_VB~r&-T#@|HruI5HltVWxDyVdtNI!I zZ+e^;bN>b~yR`CX_v`bxoGAEn)cfZbFOPUJ_N&i^|Lar(32X$*-?Qpyi()t^c0utY zRp0O%GXk~w-E0&VmIvJFzAQ>NZx7zg3>NAHu@y!wO4FZdGZ zE>?%i-fVBD?EEyCU3+B}c=HRF%}X#Z)#bj;U^-YGw6%BsX>~iZ;OkD3!|a?q|Ews^ zE;LO>yCsx~XROx0zgO?hD~|Mz!?c$H-Qg|i_Bdx9FQt6zA;Z-l?PD!0>Z<0n!ZjEc zTu$vc&qiw#TA$zx)aADPM?S@Pq_3O&-gD^dAtLl)Y>u>CCrlwT#kth0LFxtZQAo7Q z^W^8(A2^*<{|*d>;{Esf3kKyPeh4QelTY1=Jo#ZaHkhv+0#lBIwqnE)1+Xa?pY756y+6y- zcu5nD8D#W6eWl#r+?SSfl~V_dSegF;%)oBc)Wg7Yzvw98GWyp}Lzb5${VC%d#Oori zot{eAnQ4by`{v)v0?5|YI?3zwJF7#($KM&bLcsn~J7O`@P_UJ`-pcA|_fX2ORO@i0 z$4`%3#^jXTIff}j9LcxV08%U!MXl`6Jj={{Z)w0Wa;?Aer%UbGY2}Y|?gNheQqWMb z(VQZx^2im}+|Dgb9icd>yLR)p4GT{DrCf<_sw!<7D|qrgm;`?&b0IJn(S-Y%W(f{jWBb>NK{+W)R;fI8XSc9DD?eBj@Rn zB>(ACW=>ippH4=1a(LCx<-g-5SjkIF2P*mQP4TN%l`i;HXbmg=g1aQto-!n`b~?~4 z=f}b-p4Ti7)R71Fz^w}zMGw@szI+f%2-L16l#&NJO{}4QPtf+|F#WpvH$Ur+FRp8d zaAgE(;N)@#te-dppn6-(3sVx7)dY?pN6<_$i$U|-+vKo>0MTL1J)=_Nj&L}6|D1k- z_12pLp6Je7MKByu2XWmk#7iZA*=Y$1+O$4N6~FwbSN&OkruOz#zn|w>!1$VuqLta;!$Tj7b_kB@YHt+6xkwA=5M2_BWg$$4C^ z$-IO0|9!c?;z8Bxv7z%)BM!G|HTD!xXP+X=KqxK`8Cr3BFnyQP6796@WY!YsjknR` zO(K=L=jrE9p+((!O*`#T zj$`*!Pp4)vFS_nHcVe-BHYIa^gy2;@qu_mQGC*R!uY@KsO*5K?|61`q?UL0yd#s(0 zt7xCpf>j9Hr*!3K_JSFD^sjs@%*vlSZFzc#=Lm%tMdjDkIMmF`C<^jlgodUe{7S2& zs;WaLBz(<7#K){%`(dilXFllAW<5IMghv53_0PH(x3%Bg!{GX9t~uZ%q#&R_=l=l0 zf(pJ`jZ)WZ_)wpGQQ_>Di=n&6BbPBhjG{fI4KOSWalG=6!On?bfRIpVM2a6BylHR= zLE}*`Og+QJ9;H7Ac2}9Mv1Dqcik6={@((-%&z9?bHS*0yyvlC8wsNE%8NT)n3v=$M zOGy40h@V@g&d)e>;Zl|n&6*sotG2xq{Ol7ujv`U_l;5@RStJ9i(7ihNf|SvoP*hE$ zQ2cbz1wBqi1M8n__tylSW9qH$-6NU^+q%5kZ1_{s6W|5HewDkQ2rm0>`5QWu$`{e2 zzi(_@DihZWs4~9aEAc?x-AiOfP0k=2jQFOPfmNp+nx$eO%A$^WKIlbFcetEKOg%7H zY#dkZ-4sHFYzmnyyVz=bS^38ky&p(k3iXwLsaLd;SK>Qm zBBf2qPbtF5h*%#Yy-ok~TX=;_@AO9o`j0^{vwv*qR0NJrDYv6A;r0aT%yY^^R6(@gQwS6QKIy2zKJ4Z(Cp)NarBDkL$aor;W0 zxduy$Huraqk0$(dE}|#f-hQ@nfWKTHOM<={gHs-Wbci4%MQ72ac{6?`uEQ1q>bKT- zWAK4UZ}=eZw+gM?MG`te95XYcjAFCI@2^eV#qXG#Lbf~10-O*B1rTbJF}k=KJ8B6L zm?FLcAYVQI4(hv;1?4~Yh{rL~?XxdOslpOxn-b-JWsScG!rO04ts z-k&WL1&yZhGi#go57$kY0|u(!`z$^npZTuIZi7Id43DAulqbX+)uPvM&1?-#Z8*DrC%Wl@`p81qdyxHLI)Xj2d=ffv->|INv^^MxJZgLyF_!n6WW1|R+5Pt|2cCqf;TiNjP^Wfw zCGi{89HlK6$a;)2zu+-PQ{*VpFksMI+tj-x0X2A>F zvSY&c(&GEVodV&PAI1CLNnRtO6yJn*Qhb69wV#>bmuZTdJ!%bC0?3*nk`uZ1r-Pe2 zg4H1jUr~u)B`ZR8DiQB;KpH%_X4NDf)5deX6IzqV*s5Q)IWqV4-mS>F+SQr(1Og8ZpdG_e51oEi-2lD+_GD$ zN7PhQDkGn<@iL_gLpF#Au0ZVD@OQIyxyFdZ)Q0TS${A(&)$AV3J7XrW7veKm9l#I= zf&kA;R(5MY4$S6J%=0Lmp~acSW|x7twwaR+k<|Fxw!z#jU~5k0_GWfF>(U&Jd$oTY zI_KV#+G-(tEM-h5GC*~d);<@c zwh-Tj@7XyTXqAy$aWvD+gt7sM(r}xB96~%Ta>dS^&)TWqI{O8~d-;|MG(Z*~cTvsK z9xo_Z4wK<5VlwVpp{CT9I6M9`<*D@#o`CGo)(6o`8W?P5WdDTCF&Z#ra6}ogYlLJyvAFlWz@pg$)^8Mi z;IYg)h|+jRQ)~x8j?Qk+u$op_8-mp1O?r}E&9YMT>yCG5ozBLKSFKXE2I zlI)wr5ez$T8;hh6bmD-&fFOc#2|3aq45#9{mOVHJ&YpIomdWs(Ba9?MKG4Tl6`m(N zEM)gHM8imaU!5eiYrd2Me|bc$?OjzhO}ZW^_$a7q%{+zgjAT4|)_UFaAUwOAUSM;! ziUIqWEGDO3)Y$*m$LtJQCYI*iUlbDlyPK=hY?Z&3tXUdn1LxBvTg#i{n7c-;c^{c; zlc;{AcmF&ULFg*37-C=8DYHh8s@gctcHs=ZX%pGmsSGlzjGDrI(vc$`Qd?mArVo5V=n-axs^RuC@{mmWs)2b*AwxSn2U)QHlj%FYt|eSN(T z$zySAJ8R|4FC+ro!W|>Mr_-%jT!Upjr_j@(&s*aecbl3Wz~K5Jl8JxHH!Q{d7D-PJ zb!$@T(%hS2{T|QV8dV{e4Djo0{+=d2yZ=7nW55S~EDeJb* zsZMwB6l}fNnokwO=Fq;JVfGdqNLE%MmY`Q5b0JiYL!$3y;vY@%YEf{;lLY}vh>rDT z-hE^q!&b5No@oY;*)Frb)#J$b%3vdqC%fhCR+gGTW(*I5bHvNQt`GSc@ex*|olnI; z`dFRN{{hHwBH1`b8dN}sIS?dC{EH#{ zY*fzlGdd4jxxDefr3A{2sA=Fn8Q1e_Ro}a@Eg|~6pZ0vfSHuZl?7s2B%sv0t<> zl6rjYeqjJ)$1@*Z3YWxfxjfIv&4w#ah2uJLo?Ru2&iU(3WE;?BhYtFQY{6O@wrgMN zQ*j@>zUx9jd++X=wdeg!_1OnJpE5STO&iQrWEIJt33EaCi%QiFk!{VNwj|y~VR4w6 zU?deB-OgZuMvH1P6M(d>&pkJqO><3L5j1AE{kpu4Y|q=#Si(K=ys_>{Ra)7pI;hPzG*?GhTv9&2s^7#kL%Ik6CE=Mya)!d`^=-vZ&L=(;@I#laUL^QV%Q2v z7Hlsleo(e-CF{S_^($+me8{8EfLA*_;w6>~1{S@SY3Y{!=hxH~m0zJvbq_ZGWXXpv zgh9s^>d8Y#v|}x!ADMp>RQKkO^}4`v*(-x1?k+;EGI)Qg>tm41FTG?ca+i3wPMK_= zxXRq+b!fxoV#(;k$cFWsuD9@P1Qjx8I$kKlXv%NG zZ`>g$*7{_&+heoawvFZ1&A0@;y`ls3EfjKr@^p~nje06(shgI7+UIfEqiBq zt73Wv)Vo<8p+YKk(gaHv{FmzkWnu{l$h7JEeT$)khbdAP;k2@e7M9SQ7t0i%a_%7A zt=PbQ|KOWf`uD-q+BFhsUc(&?MqgPwBK56W2A_z&a-CcH^HQ&;P^Y$*=5;+MZMDu1 z_PO|dLv7QHMUa5oM>rzqoA9oQaAxENNn}Yo>e+EvOJtYk_PjOAsNm336*udNlll)` zkGv$?t}F;1a-aDoQu4A0H_~M-&{IFWl2)3QvAH>8Gk?D-m46d z6CF&&1DKA>Y61`eOL~oKZ>oO%+iVSKz3uP3eNcDQ9p4(d^snq9ylzu*bMSFf)mDv* zCGNK9DdOnU847KwwG4gWlI4-bm|gi?3z0$Luy*eSw|vXi6OEO~l%woVsKq7gsQ&ID zyVr?fX7vK1TRuPH?jr;tNYsP}QkfD_(!@Aa^9r2fJq*MU3^BO@>N7^dTXef3;< zmaONCM_pmMOVC^z+P566w;H))#L$Lmw`ew^jz9I6KXt_zIZZi;XtmPDJft&-4alrs z=H)ih(^gI$#yrEzP+d^*2i$mVbEaCCMQQHY?f?BBfa$MOD^{4wXzueJKdv~d<5$|- z$XA0=s0H~#N0S`0eGxUauGgkTvpxk0bZYcsy!s!#O#0wk(%AC3a_~HoPu$wM_+$S} z34?!ZcP#lhHJ&r8yb9k+IU%A3oeZ7#L&b}?Z$lj&XT@Q%3is|^EeQ@~5h9l@mQ1*2 z2F&oRWqH_q3QXG^D%KvVXA+~PX#0BVQIOw8U3^4F{)VnC@6yhjiyc|IP3-59SRwwe zhR8Pt{qfJlZ?gSmVjr5kLZeNg0PUj~0B6w1FkZqa_laKKa`rL^(4neK$4GC(wDzn{OXhs<_+%HF z@n=P4D0RFVX&k%35(ko-QEC-p<0E2#B(xdI|$z+Z*!R;;Gn$R@4bPgSZ%hX7!HL$xiP% zJ30rxY#Z|I2D`Uf+Bpse)C_`zpN;+s+fvG3Kg$R+G2*x3kbJlyB9}mXgFrvXVL^Kn z!8rdY%AXXmZ!Jt}i-xVwwzXC0{1Au-+lfHc`Yl6~9vOR=Jq1KH1efeR=r@hQ#>Ihn z;)V4Nz{9-BE{|9>ZoASxV`|O>;_a^fG(R1 zs|n!YrnTn`#eXlo-&Q7F4hBzA%yQo6isB?vz+V_13+&`i6=%8uJ930e&oSI8U!Fk{ zw?2g@NV%5ft{5EfD?qsctvXPGkJ`2rd!EeTHi>Ed|U%qZqz5G zc6hf_z`S4&4PXhCA%6#6W}QyE&gb(udZ2*DBVj7W@3eJ;5F99z^FizRl}Plm96oNG zu4CVwO{O?9u(G4gEu|*for}rH0evBbLgX?q$8_aiB!(qC!~7^L)0ewwkQl6bCYXbH z;J$piIZx`K^=f3tOR3d8-VfZuut_za;t+iHA|N4S>%9ombUe`v#dyg=tyJ(zrnlfVkci$l_1z#Mh(vZ;MYh+!DL?s9(}i%`o%SbbgXx&hFG$hE&16AEo!O$547nl& z&=n-_BMkD3S`1t>PfDn>8h|BZ)GQYdY?O2Qfwgb<98c}bK?Nuoz>wNgS(_6Zx+T=w z5P*=oYg=)xqtYn)k%65p)`A~cD@z&yyvH3pL0KOk%rrF@23*zId|;}<=zqCjxf(cH zfC3fBee`&$GywogH)S#+s0q)muR3@+$zxmn+@gUe%U>45U%y(@4n2~7!VH4JPS~dYLS6oR zQ857V=#}|AR-^mqvZZt4m>GCR-4O%jaNZYTBJKAqD;Ldr&q_YcZ!F*6c_vkY{p=c+ zGx~f{!uk2DXCTx+4myQ4@nN`kT>0x5b{Akog=R<{Xy$2EV~|PQ@FQpYn)$Tg)8!$< zb=#{uQ;DUT<;%`4Yi>)ZOjAO-zN#MlpIDmyGqv;akktl*@`Zv{%lZb6?DAc!hBs=1 zJHQJkaSG_`x%jkZMrLYqdf>dyn5=>$9=i1BuTEi9u7Oq@S&JP{*V%hRxT;YPDM-T= z3^!upOpv}HvM4@`=j|Pj&)11=vL@e-VH|Iv(pGZpg+O@0s>W~}dv#I^cZB6c&c&-i zstHkp(rgE!C`xS^lHcPh0U0oTwueJ&DOZ#25f|tDBO?cnTO~uIepe+Wdw-QaO=y~{ zS>J;lf|Z<&g!c~)wlHz0fMY$`hRSrJra0n7y<$FU$*2rU!C=4u31{zlgV5$$fSZqo zBizD!q5_79`;xllk&c;Jnhq=WL@$3AwNmcY9)^re(A!Pgr>)+#nJ#Q0rCyAlZcHShg>^&uVPjx7QISu@|7fGDX?j*A(hO~vCE8D|2E$WSmk_4ejfy&OJ?{@c zm2QSTKFrC<=4*Zr!~Lt#$309HD(77|G$8q^O~(t2XNhPcNLy*bteooYCNrlXxgLj_ zfu7AKnH@N_3|FUO$B|L3`(no2=z`;RJ*g1m)V1vJaaMHNe~)@h`PsgZ3&%@21_f3< zQZ;%io%WCBd|&$?AE9Rzhept)>M&@7RGVdk*iG*zi=5PY=UN+*b-%ttS1ir4!;q?0 zhSW+PJj3}VepC#<4pMpBR?L&QE#q_K_TXBJD+j>R9P|Ba75<`|t6KZuX)4#3gE$9N zT@E0x5O@Jx|NEJTGHa@P7;`@29OkT63qsg&@xco{gX zqC)A@%QRO+L4aysl`#*@PFd_D~*5RsK)1`1AFyOdA<8HVL?Wh^j9%JLqK;uc^`j7@%zB#cA z&L1O#rU%CN{2X-Kmw*C;j)^&Z%WiEOiC;C~${rwIUO3$E4D1L$P^jLMspl>3Ky4Wey7x z{yNiZhxf3qf}O5!cl~9iBZSe}I5j&@+0hwlLrm5~ulo8tH>HYB34ixCu+a6&O)3`%F)!M|BY4H;y{bzlOj1W5q=!0>ZSQ=TW1+dmNs(sm30SzoPjgGF|`#kk48S_9ST@gx~fCPtk z>i6^-C15So)GmU}yhB^&uW-7yxLn-0^~y@uY}v!uXJRyj`nB>z@P_$t&Czf{sk=#P+#KY zGQtWK5f=}T4sgzPJF;75Y(*Nt;yew!`$4d;K5rQM-n+6!te^y0R(zsoXNL&$?lyImO$pn^v|kK&>I)QE}+8$>;z7U|lY z>8@hN<~-o!b|h9u1FtIrXF0uCSQE`amRvm`VNr|RM4B)F_DMbZKlg2KIMeJ^=!tdn zU$BCnWsl~6I^VrPWA4XvU;EmR0=wHQw{;grD;^`9}kd-`6HC+264@!HZ{VU98|LNxE`7VLV5!2F^fra%0PiRw+ z{{%*Mz@}Y`L3lged;>2y0%;69M2rYHf{RIe$y%~ao#9|6R%hhTwLa^ESzAP6agZ44z(nhJ%tR(Yd9*L0oS z))Q%53Ch)Dt_V)hjCnPLRlk)n=U|nwFauHs6(+<}e|%A-q5ACPLj98if-EX6!=7~b z%FN5)@tK1Z0kx$a!RLqo*_zGM{w{o2^(aWgr*wk-@wV*>SO}xOU}{3s`FsEWTkGlA z`)~Tcl+=$lV2HYy{!@JGj?Z4ZDX zfO)@t5i=yWpZF%N(z0F7hd7KNyvXRC&Q%Ot|24-&wElSUM;{Au*?_W zda2&t+Er=&%Wy|8%L7@Eqs4Yy=7huq;D3O;Z>C_=e=nI8tTi5;iBaCa-{5?-j|G07i9+*Yp>Qj z;kbI_nm<0?P2~gQAgS^bS10*Ao1x@ zDnp^2U1ot?g}jO)*vgp#GC*}az|t)5_oVxc!_`H7;QrxQfttQ##p|(-hc?7FKGEqf z)c>uAyKXOe9ll-O;7PRWl4%}RBgnj0I$$pg=V&Nb5i+A9Z zgTI!c0C1ihmUht?9tPKq_*AVt%!uYC-NM5CbEFQQsvlDt@fm;-@(L>xny<5JD z)Ofr;dO`b`FwTLi;_#%ruVf$HDi70r4kAE_Vr3)w zrxmx*b#PCd!kVRq5YNswfD2roN;>Rt_K*5|g+iLfm9$;L7$!<%u#zc6wc z452MizM{UahVL&Nl4t@jxYOhUh@tA)oQkjw;P+ulR7BC7tQ%XpLcCqxOsmkV!M1pk zOtg3D4j_DZlI3Sl(sYpkfiR&-Hdb2R^Y21@1lhXs*}s(LH3DZizuD_-l%gS@IeIr_ z1U61O&EPkX|IHWnJX3-FbJtnm5fVMRGY9=d^Yj6yQP90-AcT1cND)Dz(f7?`r$$4N zE9>T6w0WEN>Vvm5DeH8&I!sK)R<0GQq2clRyzd^M3;}?U&KI>fn~1sL zSa-R6uy`11n`%ZylzB>PSLF?a|h(S11E*i|!*lh}!3AoyFbt+qGIB zn(pTjckYKY-MrKagxOWRwi>IYW#(ZZLZYciGFv{!b5ElROaAGv6bEtTo=eXv8y^lV zRvVqL-WNnv%yx0kTAkvY`Zd9%$QH?Mx2hZ4{d{FvDAOU5PSffT} zxrq@RJB4a+@D$jO}Sv9-YC|I)yfr`cE?gP(9ca%|gO z-Kn{+gx7fuA?2>tSVFGl<-UG9dwlY{TTJ8*c!jhnE+lts2(0XaM+?lIrK=kUi5PLB zU`A!kgn~QxwYx6tXOne6&2hBBAT5Vo&5p>a}tKiTp2ab?sZYTbLGW< zS+~v5=M3|eFwXR@QG{%#_?Oxpr7xxrofKH_=yXj+px$h0`wX(?tLbHn4;!8?dewk5 zpzkP2i!#VzeC2E@&F^+U2v(I_^WLi~`Ba)!%VS4qR>JGa`&rM8w~D#1C0f$`4^ZSo zg`lD~y&=z{P?nRT-eN&wCPjFN7@?QBLoEB3~Nv(4Q66es^BS3n^k(0y!qdYMDN?$ zS`C?)shQF27>rO1qMB=~m2I4yOhPQ&p{q?>AJKrpbU(ge3#c#dT8(u99%b!@|uLVI>>i~hjFHRYZ8V~w3J2#^8tuIIEn_p|=azn#ys zgy}n--xrP5H6eL&Jqc+<{O5W>1UdpBQMy#dre4~e zF8_3{exS1faebs>zHn+L~SK?S*Yf+S_XK=Y{00A5%4HwGE1*z zs6=u7QH+Hjom!d|GkAv-vdG7TsmaEnmyDlAm6N_#b#iUQ?MEFp@NK?;vaagzQFHic z{I!cxD}MG4ak9&~O@ep3eSm5Utna}$p}HTB%%iA;UHPA*4+^wkMpac&*_e2t?V|f{ zlAB$FB>xT@6{WNz?O;B-U^+0(jvTZ%eK$$;P_W_69FZ zr#}5SQx5T2cIYCEh_~XZ5IQa>R~SqY&c=RcH{^$B@^KNrw0?M-Hz6?l1hOGL_t5w6 z+Lqafw$Wf*8DpzB>(&IRB>G3if%Ibuq7j+Z=7NDUnqdA^RKR!dAMHgBq%{LgEmW2@ za)>Eyy&X8eHqT4^pCh`KWe=`O$&2NYaMOOd@WK05K9LTAKexmS9nb59eg?!6wraE> zu#p$!A3{kj%)n3;JF+2+0-Fa9F;-?X1|b^ucOd9o;ml0U;y@T*UFcov{qcFmqyAon zeN(k^9RtAMkHAOSCH+m@neui@bR``lq5BmcOCO2FF}#b7R>)hhn&SZLSBiYH{tPV} zErg~#=W&P80*)YQhB4^k1d|U!uXW8OXA^yQPu4I!j9#8^T{(QF$@Fo?aX%ReMO;p{ zt^LP-G|%l0y1njs^|g2`CuI3=9{u`O=rp51K3gX9x($fpVKk;>gv_n-S^px|t-rKR z%QH=v5+R7sJQ)Cdu%gz&7I%?}B;qHRx)4c;-w@ZoaEX7Tbpa*^J0V^AXg+yXiRMzD z{QA~|?`+MRGt*Q?NywR812x+kR~tsTY!ft!9%4Znq91oQl>N!?W^q(8q7?bVd$!91 zX+7#u*cl+?^CzB|dX#|$2v#$QD$f2uteUv;^I@m`ewvi)(H5?b@H#)J9rv{EJtNT# zRzrt0uE2Dk<&a#VbL!ofgqf;crIo-!&g(RprnoB3Q{(c<2sgJ^@Ev>OZa#iorJ)L)~bl_v;a@waFl? zcoL-%HN+9-sElj&%P33Pt0nc-R{UU75AP9!N?2IUIh7A|tyE@gdEx$;`#h06U}Rmc z_9iQz$?k}c!UzeIzqgh;$0&@>x^2jfep6V(=wK0fQYb#gV#qWjv(g8Sxz1!e+yb5{ zB;M*DkHAGvGd8*g8qWi!(Q!!fN$9gt&j*1sWS?Q`g%N8s>D-Tl(aT|h^PZm?q9q2D zQln0?9YCU$o~-CM?4oT74%y-Z%l|qPj3`Vs-b7so%A1`|X8LCEc~o|`)PwVwm&&ry z#BuxPgj~V_AUl5{gT9(vBltJlm)}jC@I9IVB-CnIgerEUXK}TC(U8Paq>?#aS&R^y zp28$@6gRJh;#MACx%eDQ*S4|+jun5hgoOeRUtMx0i*x3hQgfvXNHmC^>5z&1U&B%i z%B^kraqds)FlZNT#dr5N0~{U=i;N010iT$ire<8c?KC{9zw-^Dz^KGulFPc%*@WYg zhlmSaW!{A#I9TRvCwwQM2mSi-ii0zv4T;f>QpF`3WzsviJomuuVpt8&XTXn*<*HTa z-QPCU#u@b#^rnlU9_=F=nV%{<_$a=(7_YlaqxQA`k#;vE4=QPuS3I=T_n#J zel`Dfd!B9i_M>ZZjCx>+?izBUb*Otdm8X78x2;w&UJqtnKGJ-;pvk>+F>1_MN|poi z1p6{nP=Z~Q#j#gh8B{>VjG&29$M{`Nj|5MS7u!X$16FHt_rU?$yzYw?nmN#mxc{Q^ zH97*Ej4jpj0hQb7b7%5#Db-j1KEqqXN|qBN#&=1^B;dZ0x4>6-gp+q8xubpO374w{ z>7@|YKY79F%v;DePn1Q(_xag7lc!uSZa72trS0r?`;uSG1Noc}xCVwOE+01PzlB9!`CY01SPti_KgJ-B;=?~$4PpZ5vAu_;;Y4` z;Cx(L>eW@FD~fcqy$;vQE)O0tr8GQN+LT?NhvIMSqM#@rQ`A)D8%k;~m^YhInmS80 zb6nh4V!d7_3u7mmr&Bd>V^w8x69zvowLA01NHe4yjJL#7z; zlXz*9gCv;)vq!ae#){!5QIt{%UAKN_)Rt(dBb*2hfe;`Ps$o<5m*FkxsuBp{%leQ? zAWO*D8EciLzUrR@I;Pnuae8H*v|t54^Zx;=8xf#{5+kp))>=y14x{w*A%0mwe$Icw zb=xBO8w+ajd+hWS#0IS$Wd==vxSxEl%Hq&G+Da`*ob8Z#Ydg{+OeZNuvi3Ri2~U^@ z_lJG67{U!wz4YNiiwIB=h@`5ys)$lDnMy|%2sJ( zBgIs8yk(WFBo+rE&;6+b_ra#{>h!^zitaf>_?-BntGeIRT+0TE5xyIs}&%|Gala_ZjUdql$THz}xM#iKU+^w8P=p_qgb zff<=iECVH9*$TOJSGVSA)TFy;$KIQ}12$WEszC@uGbKAhuQz`Q_h7wZ%ziobQNv#? z*XkcH>saUOZeKZ6fo-tfgzgM>cs7TRcH0U!BDCi#bBux5jA)B*5V^_Sp5Nz{288oO z{#oO2G1Ru>G#4i9M}dVXCc;&HCL}CfPR1Xp2Z#S*YAKW;HhC+sWiCQpT7UE=0N?O> zcc*$NWUf$C;aAiYwSzx~lrZk1%rsuVXJOSypUc=zqTe}&8v4r8*94e+xRtpOYWf7~ zXtHbN_Ylu|Q`Fr2>2INX9tWid28o+$PWu}^PuE5opQ7Ks;KEj(ue%u z_-N;zMkALVXb|dZ+krcZO44y5QsP783l1Q@=G-^BU0u8##2f9SmBu(~=Cu_7zS#t6 zzo@Q?FyK2qoO|}W>vVR;%5bs_tW-p&sEw!i<}mA5?J2t0*8gGgXhQO*w)NDyW$;I_ zDN&>W$b}XQQ%gcH#+K}HngIAmwpqCa*GBywwPPX^j(_J%b?Y`68x;C0yRNq7_LtCv zJuuF$-apCNg9nh2y38@wXrJNiK1FP9^AA0xi?Qhns5mHm(!r>P5{AQjNc&xlne(X6 za|EdF$g-=m|4`6~`Uy(at{?>h>+In5Iw}12bbWV4-zTvHqzc{9z45Ic{+dzz0nJ_jK z#PYhO=VMcc#$auu_?a6W?;IT6TJ1^wocrm~0c3meI5Zu(s!@}as$rZ_Mv}%XmBJJg z%Iq7ezm|rt5#A{XjX2?(_kJZW!dwcyXi{A?gGYfN2Jm#AJ)Xe75bf8yk`0yL!C3ko zUV7>BD=@F%CHtMsN)@Z=p|6$2jt-IwnHD^Ph=wR!0cW$MA8B?O;v!~?6qd0coTi1; zK8c&$`LD!}W?`~lkIM2K9+~CkV}AhS-v2Yp3o75-=I!& zABwv&cNy{!vGByLNn6&37z#22wy3qK;Z0mR&O8jh@toX>5De?B>d~&mmG%jHE>?Og zErS>xm4j=Xl_9ewilRCid@0qiAxBg9US1#M0=S3!1f#ga{Gs@TS3E%$9?jXiIXw@u z;RFj)`m>5FL%4YPDcX5XN)$UU1+yzo=8vAN+0`awd9NGohpAPIGWUBPczozGMdZT} z4#4W)^S(d))l7+!3k8n39b-Kgbs!$pa5NgRxx~#kLIj(0hIXjF%?MCDl%oE zy$lJ2rF+$)q6(c1LllOA3-bQ}=0F+00OSFTbn)Lf_vbkyDRw)5G(L!!+Zyjmvww)n z7P#n=)4{b%LKI`I;)j_bExv}AfsjgYprn!xA0NNWST-K3tSb(CFu!C1zJq9{Sh>0o zb{-;wWo7|L)P#ab`l1figxlqOUE&e^D0b= zA8Y(PZaj7z+=uP|0Fvxct&<$LNSs|@T+H~7Mr|=VQ}dMh=x~Bk3P|MeD-tjE`=z4o zA|=hWwKcbu=Ots3yx^0^sRy?|PU5o%d%Q*JJ=L8wGd$X$u<1;Db-SiKDDjJdC~YmF z2*Y3;Cm^feu&hjz9oH^__Y^LHl{D+I-cpa8Jj@1))6%jQM@qQ_1CVoBOpx|yx%y5u zn;64vF0ZZFLNJRoIO^;gV7TK9IezMp-NS_4FU4>&(iGygASDR^>G|^vz%apUgyqZY zoK>q=6#Mv}irBV#;^Z`!{0n3xxRf+4f)Yt9DFmn;LLrlJPunIj9sOVip#b-|GN?ODZP>5Jn0xcqh{}?q0eU6Bvm` zG8|IS&y=o&xUd4&6VZUB0Fa~K1D?XO?iDDvrB;C<#FsUhDsC02DB2LMei=%Rp#Y5_ z!L4Z;QWjPpl9C%K^66ZoN|ov(#7Rhk2}uOO2fS|pJ|?ZcD!ec4#^I{y+}&|&v_#R{ zrwwmMUBVXN>A1|>?=z=Ij+8eMtyqTR>nl?UN|M#4{{Rj;)?~+$hVw?aiTG%E)4upX z-YGM69I5g)TU#*>*}1xTh^@UbKVeMNST${trAIF=?WSLyDN-RvksMxZ^Cv=eFHCOK z>^{`Aez4N}fu-y^+eKe(x|>Prdu(gPhW%)eiB|iax>^F+ksajo?%T|`ls_5tJ043- zNNL8=&oN6uYj)~Uq_m}LK|v}?HI05AqLhMC4$(4*GcpI1bM8}rEn?0%(=L8U#2j(_ zJ>rN$H4>+6ml6==x{8w8NhQmF4Zuo^iA70LBsGo}B-#UE8fGkqgv)ik z$2BvtTWxk1ZWk7|&fY$T84|UgPt`(4)>_YGq;5|qU;$A8V-5MD5~nA3Mjw=D~_(OBAzFu%06t7Ms) zc2u`DmDbk8W}A+v)Af9-yja&?KH%-@(`&R&ME?NE#@lYT>jrCb3>l{jAVrg&N$kq= z?mK^nYK~O7s?J6KvrMu) z0G85*!nY*AOsL2br5PuQCyw>(TMG+3Ck(U0Z3E?tD@0qQtR*F72mwy0B#;*B^*Z1J zHLY8by<+7lO4+Mcr722Ml%*+3Qk11BN>Y@ibTi=!;bPYtJGb|gwH8dMRw#BWf(uNg z*5}v}`5t+Bme#*1POcJVwio63v7|9247AhDp*-_eYWOzsF2={|X*zpVyC7%{Q!$q% z%4jLi&8xzY)Kh&in{97Ng(5Ri;@#S0nFx;-EV#|S+O4w8Om`*SVaR4%fexd$WMlt9=Z??%N5N|2xkA~eV`_9Qyp z)v8{!RlBHpq&UlshTLh^+)7m2h*~8IN{~{dsU<;4G6@74%Y6^E+EGrj)7BedY=Ay! zt>mQO5I$bxx8u%DWp$5aHr>|P4(Ur1>LDdPaGO>#0p}_EhI#yZoJ`U>+o>!pNRBdq zK`T&LI2p(QjFNaAvVQ@V+AmeD##(jxY2YV>k&d5C5&;JQV;^w!uMc1548wq8rtbdd z5p|-lg`qchK}k-a6jG4jNg#f)*nmeB#s+ST#xAX#%hamW^^(Z=U$t#l#|ucVm3}s(4_bM{C#TX&bzfC zK4K(;o^TI2Jb-?u+dsag!r+yo=2;mBkaOFP`6G{h4{UQxAbn6y`aP+{+iOCfbz@*cf`lEm5@I>yefc$P)w8>FRHpv`G^|Qc zAQAQ!h{EztGBd}os62gY4o1c8a*>7Ttv(ctGNk)MA-@ARvf#J1gaMMvgJ zfXO_NeX-xSuf9e(sOb(oQVLM6k=T*kbL;)V$#y!5|sjWGXxP5K4Z6~Sy8gvOw!5@gwvpb&-mQNamOIw_Nh(7 zWOkKJ6(d9G&`@)fz-K)BjAQZsd~_v8Q`>-3 zup)T=6^(Lsr)=WOZBJ<(I2ew6#IrUs5gkD)2g}MyQbPTDNIVh zU&3y8SkFYbvcut4`%>b#+&fWh6?G;%5K@4mP@qo|=FL!}OyU2{U5}UiJ zb@=bJk~&;%Y6v(f={e|6?Ztgo_yJZIi{0GfSeH_;rtvF#hegg&#T|e0#mGVv^h?$* zZ*G%n?YdUugdvukbq;DO(pGDR{6gH`3=NiSvc@HPml{KD<5oqLT3M*BA7bL&hSzqI z<7r6LuML$ABT*>RSHr-chD|HGTa_vOcYKQ;2AENv#g%Fvb?FT$ryE#GkTV^`FA6Ae z2lR=@t#m}j)ojEVG2)FEr%D)QmK$+USy>^r!V{G!l@(z;l1@oE&2m?ucOz46t=*}J zdgo`k#%*lJxG+j&w3R6;EV`EhnNun$1xivDvC@!}jEc*9SC5_Qwk?!Ml+FGVVI(|< zR6>w|6Z2sW9@fxu8u82}peH^hfPI=9s<-*1lP{O(bLTiG2baZ7z+;AI=OgqqfR8PyJyWZb%Ne93}b+Tzb$soU&9Rw=|y%n%tJ zq4e1uJP*mt5;5Eij`_(S@ru$WoA{GRX>E(;l}LQ49ZN!$j3i+|{+xY3o7%D#!|@)` zSHCTZX>tGnC2uKd2}uBa$Eo#k-#z&sc&{E~-weHe!rV%doWXR%Y5gOX_{GJ-Hqr{) z4czKdRCPoosPV)R>MK5_^h*>hs>>J4=~BR!u56jNSmR|S+jI~|d4M>cYh+GsZqh!V zFz9VK7L$XR41l**NXVJ@8Ml z;MT0Jmi0dgQ8<;(Z)h^1Ef;Z917kYW?CBek1F)^a#}w(^%sFiCTe@s+kkWAFNF^jj z(tGebLG4#!KGkic$HS`8x_zdJ=>S1%q=V>5PI&p5RthrgN>elZj^%#I76({{T3=Lgh;UfE1l2(sgeforyab zjF-`Hl8vzZGC&)PJ6}=A4gK795mT2oYj0m+YC2NRg}4GpR3^HVNbHf)c{t88J?mTk zF#IQ4tX8@Ei=yDS38KQzpn}6#@}h+N??y=q9%53{U;?D4B=$A6wsm_Fn2h@lMd>KR zp~6CvqDceg7|9sP@99!~Qx?svEN|Lmxo>&NXiyQ*ryRpfsR%gwk{lz@S?X#dWnNO;B_|1Uq!N_^q3hAk*K0otxBWF@v`C4iBi}8yi0YGVxkq4JQkK$` zxbw&bZGu*zkd>d-Nx|l|TfY7VH7q$d9Z{jDI}L}6ue|}xhETFfx+Cdwl>t>&}@wD11#|_%+54)VrJt z=Lz~lj9b{=uFB=Ri=;FhX7Qyi9|;YV%2WxRZK<@Vr1xBJ;5B~70vdH@p-(PcGuVwE>osF}bew*!0LBh8*z?D8fv+7U@d?m(2tk{ici3J5=?X_zQ5=)iAe>;F z`xD!}R!w-f+gsCXLl>r-Q<*4Ao(TGo-vW_$=>LnSTNU|YY?~1_UUEN z1I=CETsBdtYK<2PkYY%XkOT>xx^^9zI%9~X3$w&>!=-Rr1vX76?j#htgqiIT>)U$R zhSX}*?A^v|dMZ7>Eu!S`e(7RJbrxw)gf!8a_GTSvG45fOn`IXU&|NJdrlclBcGAGl z9V5QmbE_MU*u67jxxV6A{F!}Qa9l5_E2S(dAsetSDFC`Zc5akPo_>e$JG1h7^>C1#{Bf&{9;D zr7J>`v@Ib>T1ZkLTNU*)3(&-CN~qHrp0ecuhzlG<8pb@k9;##GXqPPNo%D`~>Aq%1da0sf-m zlA`fiyuOyYytE++^i7|{4ZDeH7uiP;wYYIIP{tt5(W*K%vMPkXd4ONo+~_3kt7dT=L9IbLBn!Bbt-wes-nOUK8c#)A{{SlM z4NYOv_R^x-CtvQ+Z84p6B(KO}+0U?-p6kZ1($(oAB{& zH=Fp&OV`)<3U0{-uu5Ha!D5({OS{WGnU#5B6}g3{%=-n|m4G$rsu`7dMBA;>`!aRG z(BqFvnJIH+zPB7(&Mi;MF1q5}dDk6Umb292*0M^Lr5tR(CB_{OH6VFtQ7ZFPM2A4n z%A_YK?nmm7cpm1i2Uc+mdsA3lqgYc@rAu*6T->Ts3?tJq^Ah;B}|56?#skYJ3P_>@N<_D*cNIs+v{ABZr zqc2NMwWlAL;Y?&9#W?pJm;^9gvDUy|Wi6(d&qp{i}&@0Or8*L?R*xR9mHXA@irMqo5(gIS1pdkSbp|k)1 z0U;?!hyYeQYZmOz8p^K{8`B3|%}$WgZ>{|;YnYi+E=T43vvpxFvIJEjT`pQ8CDY`sN$Hy?eW?LV zF%9=IR=r)*+#>r9!W_HA{8V0Ii)r(2^o85owAo7tS`{iot1EHW5J)HsLKKuODouEM ztQm(2#&Z)^7#U&QHw09@Y zmYcm=bq$(Iq1|kk7&B0E)XH*8|E!-icts`|MB_o>FdUKnXkn_%0&FN&^qXNb4uP!ZwYPhl_ zvwv>cSCuY3WbMOEDN>ZUw5dRVLF($srmRj!Wrf?Jtvx>>Q$v*C6sb7x)sDn+Ir>%I z(mQi)n(IM}Q~;oPL1+*V4u8^~NGjvs+;Lb7;j6@cmfwvn9!zNovug_?SZ`73a`#j5 z7O;lI=qhvPnPrr;tL(BsE=O50Wu*_LDtBVKD(Hw|nJ9h4l&AugbdNA3f^o_3obX3( zM-}onshDPH%PYk2+ojyuVwTRj)0WpBF$r_)A!sRcLyWxIlIu@4RMUtlS{9@bqxWYl z=3Eaq;8=EH%h$GemJ^BFrsB@_*(p*M50yx98WQ6#vf@+0z#Qb`oS$F7@mXP^^^49r%WcYKEtI3Lp+t^5cL$z3 zFDF7op3_?BHREwtTTAtV?e0+o)yX^0y{?kiQz_Lz%|EVNM2-0DJL zla!I z@Ouxtu=PKUdlOMI>@>1vfS@yyQ^_E3FgyEz2kbR0RmCCV1HEbavP!+a?0+!FsUEsE zH`E)9M6%mz#n7ZMfVF;6x@aZPB~XyFDbz?Cg8)G?YmVBd6k*3TKJLm<(+h%;1H5iW ztd45jzq7l1j49nMV_5_Oe#F^1R~&r2@^W%Mqk?J`TPL=Uf>Wzt=`4PFR<(K?0r$((t30MRXA_oz^{-DiSXSO?PT2kI~Lh03#qMZIu zWbl$P{E$AQ9sRw$9ocQ9a1N~vrvkB#ZR~;1VUQ0bagSW{$9uga%~#5#k~7u&JC5U= zXSQ;3b5f_6>B+$XMDRxgIM1$l^uhG~wOY*ikd%{{vAtUlE-ibo+pvF_9DjXOWrh_R znr3}c+8~X&8e%~9JWXT$-)6SPA6la5+ao9E1x-P1v(k7VkUvz6f(h;E=~(-|cFnrc zn@y_VV$Y?mbL|%?cPTb?2-D_HgAwUASke$wmdkBT!c+&$65{cbfmz3PwkmXIV}%4I z*zUF#N$W{dNMqw0NDh>j?(3S4!^ z6xx|>hFkJl3kgXpQ7J*rabCgrM!jn>bso;wAL?DMR=8;luFZLo``e~*LS9NPENz`# z+cwNyEtM!CS1xE!QjnCW6$7sI74=%=`@f=P%iO>MR$aBi&9gTQAT*+jqLra;IEGzH zRHWOsw*rzB2EdI?YWpk=Ypjtj&vZzQa=#|sa$J?<+x*$cj;Zeu93hv>jyi*t@Yy8; z03MFKEqGShjjh|OLu-p^)^qO`_*Xf%X-u^sIJs=0^4mEGPnO!8P(dgPDaTe%1DLn8 z{ZK7#kzvaXpqXn!kkf30X|RYo^{EYBQavQhI`BRuHN0W8??DyxwvxA z#9!&qEUrRQuN69VI*?rjB_Sayis?>%+-%xGS~O;xN&|o^Fd4#C)Bq~L;AEWRKEsNc zTehod9%)K%Md_6iGL<3&DnSGupfa4~j=+w7>d=;(_={`UK`7MLbiAIVsl}yBC1W`! z1;h;FliSd6Dq>Nl@UBNZcaT5>+r9@sUjXCZ3VnUT*(tWV#xXM5 zr^K?ypLw;X%g6Z2Ou$i(L%JATaL-lej(w`mzd@uIZSFnG)sd-8oV+wT5+ zRtZ~tc5T(?$W!-p#BuE9mmNS-3V^}=b$q8h4Cm-7xnsm`fw{Wck*T5dEhteA$;T%I zgOBvtz`^?Bv^P3A)$B1qVs2*ZND`!n@kbD%f+tB2Y2%U!1nvo~(#IX?rT&#!-sOvg zkdmO-!=`+sNK!#ch#+n^BeYFldHZv=G96TQbY7cMi6E^ITF4p3F@OQadwn|tikL*& zEviIisqGu5+*oaYKiHbeFp_b`20oZ4j^4FvncZC}F)gayc!40hfCF!p9;Etbf^(7w zJ;y%vP^Vs7kl)WG0VM@QCCrjA3bB=T_v9WpHAdqu=XM%HAC&Ve8cyk5<@z338g&%{ z@K8vTs(zSVM>$;B*SbH zta5M(z{xm2w-rLn+T9FaEHkC-q^Km7A|ePNj#3E&8029;@<)wm)U*Q$X?S(vAq)9} zQgCts={N*qztUz=0Qg~lTP)ImZvJX6Gr`-4Ttd1XJoUIDK%GkTkd5CW3 z5xUgb^64rHbd>-^N0ukNR#~fT7^G>#@W}qD7IwhtF&hSe<8C+hspY?CbSIRSB#08Wrpt*no$R!CSIX^MSZhP~L=A~y*M|oacx2?!Y>nc*5aUcZt z1Eh11dHUn9;;~KU9-!cOs&CoE@r!%AN6~W9(R*}!=}=f(Z51ghf{+xA(oVz>YYmBD zxHcrs#g)bphTL0SGNvAE+1tX~aWJ7Fu%r?=Qb-`~41h;6yQQ{wJprxmdQS5V=jO*M zM07_&e$Kmlc}zEywRvj_RAPm@(l7}ZifNXp9IN0F<(+n^`N$W#)@0f`er%_1UTddLCSa_^rVcF!N4Ho z3gwo;x*Nl{a~PH--Pm#?gQUFb!a+iY5z-C-9X%m9z!@09=OuK{#4mF##kOy8k#S`z z9bRhJlhS$pQjXr<{`JIhkAkkD;gQG1-kP0qkk^E+i2eVdBE}(q&qA*rp8D z+k^Z;^v>l;Z!l%Ivi;~O3q{5f)6Jmm=1~jd_*-&`>;ly@?#2Fi)wiP3Uik?WN}# z{JCsgWdagQYd%^6j|3IJlfdUZcgF&+>wY+P!t>>kF;G@OB)qoM)zyNso|ja4J+sH$ zL9R!P^cSsIP4kGmw#_SPK$88YBX@9wsW1Yen~gKaohg&N`ktqTXKs_@AUkP^<+2t& ztIS*5EhK>wmzqR-&mH%y+Fp*(dVb{&^QSE~+YQ>-`d_!)?4D(|=>|eUNPZOfYi3+m zf>gAJSbYltSn9&CGfP1DnYXnX1*cyn^t>DL&ZUZkz8wix66Ma?!%2CyA-o01w~1?g z%o7E#ox`SfJV5AM>xKBbiW(A}5SbE6R;-Nh6Vy2)k8$WKxwmfyo4T?Yk9CGiK`HAG zk`{4*l;uQ{NGaz7KGm>f}KBP^A5U1f|OlbT&$g8Km^@c zyKj2h3fpes4Y=r5z0{79bYHC4Gk8X3!?2r|SSnT-jo_D7xPCi@xZ(;6mN)jvUx?Cz zrP~|UZ!p)KOK(49>#Q=i`UJ7+O9rNk_T4{tw_YO-khg8}nUm!e6@;bqHlpJTQEhlb z$Zcn#SWgD0RnJ__#6(w^U8;M{NlM#WawEv{U1ekemYr>BS{2D1Nm5if0~s||MBmNL zF=V#R=xZiamWvIe^S=6|w$D#VOJQ@A7*cvbZN>r}Xbd*Aj{cy+J|S*bYTB{fn{dU) zl`D4*!~=~uNdUMluC1GP*%CxHZf=YMqUPXNg>w(|abEo?PUQ}$*j!jRr75dy!GSu( z-LrB68*=*%zPMubaVk^Eu*1{BLj!eI`CM_fn_I4}4K&ge)L?sA9 zK~j{W5|n~T09Gw;7mQt!+31&MX6htr%~Na$Z5l&Klw7B~G1B4@9%aZWHcMJ#DYZFr z6D|UDIO8qb<;rp_IXY_8+Q(OGbgi9Ft~C|;fZ$nb@u+UMW_-kir49sKWk^p`X>9;2 zr{(BC2MJCw*I3)p9rs&qVxJ|jQ`OQVW29uMXaQkBk$?xG`U=dRwYiA7^jtFtO^b>K zU%qvBN=P6AQp+r|(pH^8K}&tqY;`oCYbBSzquU6xAA!+waE2X3|l2`h@ z94z_{I6kMJTE3DWc`Ff=<8Qgtpp_(()zn53f^nXZLV4sLe_!R^9FmrtQ>iNQPf}ay z>q2m!bmJ))9A}>AHCdd`i|ZdqyIPfU6{&tR$koP`CA6Ilr1nY@goptlFQUu3dWK;Y7Iet|*dm-d2->%i+eY zY_^cq;_umNSvkoDQN-85jn&_u`i2YkyzDv)T+*Gh(r@%;iQbsYtrZl_o@( z?kSg?0LP3Vq_pd7$Z7RR3rd?Ts5RjB1 zDMC__=suQVxTV{t9>4M1wPA(Z*?6UKcI%)e$C0d+Ev6evoAExX+;pWSN^QW8IQ8Ef zz^pAA%D7m!a}l|HtGrAyhQhG>b^u!z?v}s}++8|S?}IwqX`OB%w7ki33Y4XH3i9=I zo8iuFi(Kuti`A_|h_iIl*4HGVEQBSAOtP5Q7LwU{@{}=gVnZnkebs<~6yk^`vaZ#m zY;s;?w`_s>Om!>E807W@jORX_dx2UA@LS@l7Sc^t-s|ZrkD+f96#cUJLyo6QTxA@| zZeuN>ZmP+0bRpLsXeiq!b;(;xi!x-0o9xin2vWp%qR#tdIU%RwOJUaDj_h}kmsx$6 zn^K!`rw~%6+(A+nl_h8@1RD8|*4&+s z&6sfw+%lpTl_f3&7M*EseZc5HRWL57WLzfl(&p7z?iXQWVVSVP8cpSnCesON+l8|l z&5iqvxpiT3olQF0mdRMUwz_4Uq|jSSX?;vPF3$zVkOQsZgcE{#eqd9KoaBMVJ9^e; z=v}SSZn)D@EEcsPT{tU<)=GHE#yj!I>;+;rkk*Q}C5WNY=UGa8)et~P0~p7tBOHu( zHI{lyTI4jO$c_|Jf|3@XutE050pR-a=mEui&FW+M$#*Y0`NdUo+rrkE!SIsd?GGxn5fQ(XlK7P_gzF&mGAediEUt@y*OP6e+WnkV-Sh-;Q&i z*bf<~c+#7B#T6f$s~x~6w_pwreDnR>X;&Gi94h|+7sPQ^P>uJ*uM|l;?tEY)e$fPa zb9=7sL2G!adSi3z+{WfMB#zzot6wK#_PJ?FSE6+K!p|XA_D+A5p4lC}dwbNpY`)VV ztp`GB(Sh@y$;^Z3IO9B=eGV%0$#K#F2q7app33y}2M6|j{*@(ll`kZvKww1ONTJqMD zl$O_r6VgBhBaZcwt}s-W0!Pi$(lguNsQ2!8_WU^*4dZje?&NPK-MbrX^?bcCs9>ep zEutHN4ctCb0}e09eZUS%hd@cnQ_eG8Po=o^)&6tK*nU5rE^sV1;}Kz}v5aEt61>Cg zbwNYdOfvrfinarq0^pgMq4WDSOfus zR+r)F;crInZmPNJjemBF{zRN<`1)bOl{+zBV{HYc>5?`Ukwq*w$d^qnTSF^$!kr8pLpxS~;r*EWX{( zj8LnG83=me_8YrlN@;H;Yr1GCIG;YFUI(+ruxux)EaQ2{DB^73mY3>MaC_}R%Wn!i zEkL%_y~lv+b)}J|g#t#UB`L15T-zO)Wt29p6{iBR(uEi-9FPV+Vopvt=RA8FmPOl5 zpngiwG;WwHLx-sb172YC$N+Ty{N$gyx%stt{@#mg=o8##`4O@dA8FN=+(#p=B`IW( zN4OdE9<_aR*TkdAakF`{skg$xbx2aMKpFLX9{&K-j`j1)6!=B!z8KT0V;jB3B`PUi zADAwdbpt!L^X<5qOzH|qAyL}B!-I9Rp?InYTlUy%ssQN3@MJJTj+5naEV$Ca^$teU zBv$Ms?M~I9AxP00Y^5pS3ws&q9+@M!{BhWxPGyd5ZqhshznI!>C)EB=WD-vVp2P4T z(OTrO{6=&|`6)!cyKo8R)vXCgIOjQeTw{O$2Pc!wTw7O*eY(HLC7njX2uS5mB_I+( z$-=S$&-i1WX8!;}`rU;S`B53N)V8FBEvw8y^uoVbvv8y;L`M5=NUhrs&0Q+QQiEfO zWp~P>OeV!DDcXFlAqGA8_nOMvzTIt09YH!bOpr;!e!|enBN*Uh6Z^frYMqC)`)4KZ zIo%tl(Av)k5L)R_?o;pl-%N_NHjV4h*B1IwsI9KLycG3GQCU}hoE(lYPB1^YRzTc4 zT%e~}mmO_jDJx6413Y`<@5ddv$*Kn_=d3ZO7;_QK?6?M%y8Xr=*-#_^5S6?DGbEkV zH1^)Mrmk{wkP&HVfjmeOv@u}dBW>j7KPQr7S5x83{y0bsVeEV1thR`RD0g4{Pgk-9j3Y*?nm_%J?bg z7&$7(e0uZT_NZm**N~OD(4yH(2pn2k#;u8h8sx~_N&Umh*|fSf_ly4kn--vy zj>(4C%$)wI9XR#p7(KgHOCx3W@QZs4oPNa5w{upT4N$7n$bP<+5*a zL?8%hNWZ!W0Wd;JgNh0;B`2MrMN_)6!xW_H!*Jz;xSKm;>B>t)Z}QlCL9IJ@$MCHK zL$xzs=?IC7XSlc*eTN+%G(+YEHmK<#K!rTwP_Cr_6Ou4%1$N@uXGX8nv+nU^HE&JA z6VA3!*_S<*pGwr0T3Y;tqX3sW)Sr;3j&LiaopoliYu!Bko=Jfv$t<`8cFT?r(jx^b zO86K6^y4@@`U6^H-wy+N!%jUrQRs>9vjx((#08ZC+VWN8#VKjU4xE58q@TaFduyP* zY_-Pn!O3~+F*$`}NlUgCSe5Z`D(2PJ{1#hr>C|3j8dBB5PK%sF2DL4rZCoMG6>i$Hu?x4jB}HsBQlvPgtOcQ77~F1&)*ckKRk|Ez zqUh%Cw)j!eHrXwPOz6F2wu2=YT2iGsln{lebtOPm(_W3)8Bx;o9UrCSG`7>_I@Ad2 zVPJERpgM`c&!=I{a(N;K^Jv**$W7^mv~X3*I>Jf)WdIbQIU^+CPI;2G2gJQA!#p&{o%E=i}jhTsYA|nrL3%ydw{2aC*QUPNEyXtcA5Bs?fuFE z8n`z54gf#_NcjmEQkB&C;1A~X=D8+i_;Krg2Bu1z%ooi!2rIJ1-?)@QK_|kvNl=hT zO2UW|t&Ym-SWi;=KaU2@rXhi}s2`!cv1-x8je$w=7)pGx9QH7G-(YWT#?v_%x}6=S zTUx>S#1@A>sm2Hx&mVuV6>~O^ah}=#05;F2*ofwnQ}4`^rb2{!dStv3U6XHpEj;C} zQ-uJ!S!u|!6s0*PBhLoiYg&Cf*|yvc$7~8;V=G;#>Htyl@1h zrA4u+k8sF4c49HxAJIPCV7Ymqrq#c`kwhC-n$X9k|aYy?xpD zpI6zyZpEy14TdV#S`Aw@1=85qZBCa~l(z<3Dd{dPZV}UkII_RjQ76=yW8wodHEfnM zW>jwV_EHkOM+~@D9JiDP+VJa!E*mbZbvUMOm~1Ezi*$kld?lTKqq8q!H*eo`=cte1 z){eQUtP?Te$;KOGEXhk4)s^L)!dYu%wAIcVc>0n-cJQerub`Tu^`7HsO}v2R$6M;# z7MYCPXJw>aZVWLlOAM_IxTqHXrCcs6OOCeCW&~+3E!P`u4XJBNPK|XI_imK%Bc0UN z%8aJlY&4Bys2U1N78cocwx^*FEh_0zdEmEHLQ2-6l;a+CHNWiF|6EnO)}Qjn!!1cZzMfw-Q$TU@22*=75NTz^PH@9sv1)S{3zHEhb%-~~`e zjMNd$YPdJ_b7BlSn~dAkyCm5*txL7b*ei}*T{bsTTfW4v8fwjCeODESUnXQ0_TM`^ zPkc!3E?e)~H^a@X*2D3fg1yhXCQM1KHzB@GHRl8hB^0+y{Ny(5%*FVnuBSStj} zI4NmP1RLx^__c`QxTB4sAU}C{;s(-57PhV0*)+Ent9w>;s2qQhqb*`10XCF%lz?QZ zlmz7=Il%{%jPsQC_03vs*F~#KZGN-3Nn#vHUy`NUZOU<^W%-wETVnm4Ar39qQmF;> z4w96ir8@CL4Lss9e*Gpro;%WRauXgKO)baeJmU;Rj>}2vQ_j5dn{AW?la;ujkUv$x{ z{XZaD;@O)Nw*_yUKGxx@%iEH-7TQ~FX;`-lQ7Q3mN_-@?2FNRP4GoYrUaDd_dnV<} zwfHv4#jy)(iySV@G|C=z=i2bi=Y{}qF=Q4UanlLb8)ILH@+TarN>{1t!jz>cN>Y@i zDN0h5r722Ml%*+3Rr1Y1bB~Jl9-m({pY^7Woga z21MIWjE=vOwjvXIzvlIOr~J!xJ79~5CL_c&Z#YzR^V~R z6v<(e))L%y!ZNL1(zHy&(;lDZyMN*>z^uC6<-Da01L_xu8w7rIsX7ZjdFXF`BqQcSmkxUs>%Q> zI%0$Z2$t+Q9DlVZYUm+f!XxoIT=P@1TMmVLrLDp5=x}wJ9De-sQOW)hmx?d|R>`)KgkR!(kWR;J8OLwYZXX~08rOpV0EAKEd1XiVkl10LK-S>#p8fTqJf3s+ z*FIFJ4}#BywDHlrvO9mSF|+&eUuq7u^-VjB=ZTO0mt+3`6s`XNJNzM^6K}~)xTBx?wQAC@-`}5XX!4bR zY4CsW*I?pavgtMfXz~bB9k;v38g(RaQsOaCuy*Wfx_ar=q=E)^#Ng~fXAe*#(1lr< z9^6*NHy7d&GjdY*xP{|mEB1WYS$aic>th{)+bNiOO%;zN{6Q` zHk7QWh|pDYEFdW72@+?%tPf16ob!`ba+TS#_D0J2R@7NwSaX(ki2*6Ma}vdGnm`-` zClJ262m{eET9z}Ob53A!FJkzZyLl!<-$27{-X!-^*0yXozx%Z+uBhF+>Q)vYTR{gT zbjcu}u3sni5nhYk+tZDy3boq$lyl~fwxwWtfzv4|!T$izbHJ-jO4v<>nIQcgutx+) zKPdzfJ&*Aqd8-!?L1FrcoW5d8cD=muqzf%oU@$*Unsq`V^yaHzRv%bI`+4h1X_J84pg z{hRGP3as8ej5Vji#4a1tI!kH@B6-~@2lm!n+I(g1eVvBbjTX>)F_#wQ)U9B8D-*(S zM+X2Otxo>{tFw88rX;p)Erv0MQ}*)OuyQ(zrKx{T6g}$-=~y@N9!8i<5$=En5l5{lcCSVPKPPwnncL8U>+}Ejy|7K)&Bq=KAp~c{rS~} zD;)1bdPw?IhjJze;#c+3rtSz=&-`O~@9V+lt_NSb#yHQ|(D@4?_x}KjZ}!&PU--iB z?5K)|D?#m)hE_1IHH}P39Ow0Kz#QcAMOw?Z#EWomf$?(Mt*ww`D5*+QI-DsajC{D% z01tja=C#X8Vf_v1HWZ~V49OT(f|Q`OD$&d42~Z$~OLuABK;xeEP5!)fcIl-tbDl9r z+E^)5ZncN4DhH53vNJvFBDF7wyS}A;_ubKYO5Uf`R74SY0R;M^&$+xt2LmMVdCxhZ zPUqcv%F}JOrzbrl?R_Km$S3#Jv!!%fJR=UUWfu0f!KP4p^Ha2Pyt9Fz>-zQgj?>+acaf;$eGMmTJd6ch7 zkpLZ|fCXMeZ-%pMq@mW)(fYkFOc>`m$J#N+rBs&R6PurFKi1bF(m#;5Wfb~%{WfvO zxIbFQdR42~77cKchw!@>l6gy(%`9_2)=BTZUfX7T#b~hQt~H3;tPpiIbm|O_KB|EI zoNZZi55|V>LrPMIZR-h8Pzy6iI2ggik)Hnm&FC>xwD{}Z6#oF`8#2B6zbKshkNNk2 zeQ}TGtShBqUC22tR6A=RKMhCG_Bz(b| z%`|xD-K03Bn07UVcS4QOqtnyv{{T;Z{L~bm8rz*MMNita>hi9xlAD$R#yI}I&JRAm zwTCpRM^Ca+RjoO13`(FUQduc5INU5p%x~&yWp!7mibz89ZY;EdNJ`ypVoXUPO&Q>G zJMYbA1Aa7jC4@R7*)*te!U_zAuMV`|DbMJdyT($oke~^4VM+?*4wGdy-t}%i;-tsa znuYaSiCGqlauaQ~6sgs^lsz6dO6H}mu!fBHWi1IpPg!uS#oDS?LdPj(tTy0XSzwrZ z7a)R{!wFlcAV4jx&1!7NC9;xvB$HU&OFri;UeQ)Kj%3AdEY=EKwfah#VbqcdOU_;aDU z%$Uz4D1hrOp@vx+aYhV36&*_lzzx9)xN8r*al1>EBm|{*R!S?gxmpr5p>MF_ z&?F#$NIPk@{9iZ1)aY@ox7z1`QCCB#)x9LEImt$&K_i|ytD9x`=J`fvu=bO zfb)9X_Utl^R+EAE`___aT(=VVW9T+7tMZpkFe|-672-LGiT?n_-=lwiYkYg+H?6Ov zrS73)E+s+;Ea97vvcy*YW5slCDK$Q&Lum>JJF#xv=PiD!I$ zcQ~8j+qQ11D2k#+YZXPSMp2?@5u4hptr;Y-soC19s9n_FB@`KmQM>jI60tWiY6YQR z-uL(Y@x9;i9bb+k&vE4WBYB=Y_kG>heO~8voFyk|<^k8K za883!O;xB^#0{s3kr_D5qK`O|)53(CedLc3v~=5SLpTji5-*C%^sA45x+2yVAfkCmG20{m!39Z=k zy&#Q8tdO|H7^L3{miY@-i%L|7OVynbI_QryGMbcgtb?23BEUN5`N@Uo@Ef%wBqX+L zK|Pr5I;SmHZzh-0i=XRTX-4L;thjsh-Rl*U=dtM=<&cK4#SPfemY8H|fFv;>X|1rR zGoQI0W3#T}B=NZPC0S89f_q$bP%Q zrQ@!0r~H%fEhUwkCY}3};Caex>ysvAdE_gA#i%ABXKN9IL!SLDT0$6>9Db;mp2wR&&2%fke@Bn#8M(T@UEA)I%HR zZg@#;xx2gHO&O%p`NcK)@%Qg;h0y)Xs(>Rw4Uqkf=4W6*4$T&i-UdZhV~dFr8P<6U zDuR)A%A-B>t=mAtx2Bg)!aSd(*>d+}ZUvr}jl?d(h9dp`@`iY>t0#2aP3N*TQa5ST z-JuB|UiO~{Q2Izkwe=(K z?^?*K!JE$1Ad{HJ%oHew<>RLX6!ilZ@Mz_oofmR(^BAkKmuu8C6$hNcF7j**W~wC| z<+9r?cP$Aud5^*6_nK2|g@6z~lRxr+{2x=@_X#<$w*`w@P1l77$IcYUxuiD#F36l+Ly^dVM?;7l%I#|!pH5rP{B@CVXcPDmqVlh$Jld9rdsS#?ayOJ4e!&n2TJs8 zO4)X!Jn_#fU~hQpC2JhH7@O8nU!N5oO6lkbY3B|a{NNOmA9PoBIrHvV05DnZ0Y}$X zgF=MWxt?hkoc?;n9pC5M+Wq1xA{5w-9??#DnU8XRb7wIsr0CIn;){@=1ml9LtoQV+ zCv5Z`)Pm=i(UQp0;QKmvpEfAV3PWx)eEWvY7!SYSMd!RO0|@5wGRO#BtU{lqTl!cc zuqW}x`Yo@@epUDhOaE0PsUBh~1&y9~(iME{FQy9A;r$Grc( zQw&$3`$Y$Aq}jEqbXG8Z|B}AFrt3K&vU|bcem1uk>FOQRynI^UAk&@`nShD7B3&!` z@CHf`I=CxGlv@Obu3wP9lD+>vP6(LM3C+|G`kCOmsCKTO>wmk~Q9;bdp30@z*5U2& z6~2i+foq(j6gyW)H{Onr+@RN}Ha6gO;@ZZi$}$2k5Bsl8B8Bg4n3#W!P1^a|lDZd3 zS61u7@%10YTYl*|rzvnlT294pEhM-rgX+_*G3L@w6lZ7XDNK0_cK0R2$uQ(1eH?6zIr?RgXp7Fixg_QI5>UKL65&HEbci>|sb0AG~*) zwy>T4diUMDb*Xtj!;#Gvy|}UgVjH3hrjuvzK)ZOrZeu^Kt}tAvujF>fSwjQlcpstjRYR`wQ z)K4s^9VkP%KfB#jXZq7GXWnDP1<%E@fk6qD2`s6_BCC>Z68R z;A16rMWHr!@)5|89L1)OOfGs1)D0`5R8rCn`$Wz;DEI``$Zv}Oe$ksL$B9h9CXG-J z+??FHALyCQnDyvm5Tz-CPI^N6$?AH%iR$D$C8Maw`l4tb(B%J^o?U5+^^WOiy{ND@ zKEJbZ3bUMg|GkE$`as?GYviLYw-ns6vi`lES3(~&yKGHv8_C^kds+b*=m*?yuc+2w zQIP=!I}PA&PkpMQIuv>MjcSjUx?pMMvHhaCa@AxLEWXGSG1Zu553I(O7rf>5S^Ffcby%qt6(H-W z!*t_Cq|Q@~* zhqna}QW(@{5`AF%X5_jvFH8`~J`GFw#z4X8CwWp*wvM4ZT$=Lt?zs4G^h9lA+VYEw z1D_z&^{Mu=q=|}Tf17Hip(9~kpZU3k1Tr^ysn%@oGhSn*cH$eW$NS+facvb2+=8&SrLTU@udD@#uP+}{^-K}MUfPh# zUH?(En5|W&2#~_9sTKdMV@$b(|C5w_-_m)j(&gR}c0e-~AAH6*>3cNWaL0HrnxrEP zIOUbPZ)+2Qx`jITI5|3A%;mhi+Z~Pirt59~A(67X7t`{GE3SqI#ew!S`c!6Uu=jqi zG32(NTyU-kD$7QPm8g>JoQ91+;KA!wiKhzAF6TCJ!q@Hws`0EeE#N;COqQbJ*s!HZA>Z=S`hU9qLPx3#vWzcD9>{L?a`i4K! z^zDm)YcIHg{{<12{xKg53QGwyBgAa!s0n$mW`|y;KV(dNj${3E|AAp<(JK?XFQdBU zp2sV7zRP__03&q^BC)rq!*Ys*p>DH(tE=NbtED&y=xzh@B?S<64XeEMq| z57w)IFjis$)I%#T1q-hw>*<<6y3#?@|zydmu|EeQ${?%UqR}#@vyqZY8tM~i~V+>@C$ZF2m-t0hG}D8Zd!Yk zOx?~_3K`vr$%U7y+%235q5NzR*K+hNc>m&SHm&~n#3^`mV0^lY6{G8P-tK{Kh_c+fX;W^^^%)bwuW6yNn9**UU{yZFIisAT*& ztncO@Qw=c&R~OcDs%RREIYD+eO7E2p%0nuq@}(@`;A!%exTTZ($*$!w&NRWqE`n~$ z;m=i?p@t$mbtFzzhU&6`+vg-9`}wX_x3^f6V*Uf>J&G$gbszZsJ>{K%6}fZ`PW|nr z9cj#2YFWY{UPW6A*y3fc4|dRW9sDYX&+cz}N01?ucgvoYy4_=>78I%Jw6p`74F{_m zK&8UD-}8xSE^(1uUz=ta^1c7=ciEIuLl)VU2&Yr#4i%R<+BHmwT_i+7>Cq~zY@BBA z4!A=o0YZQJpkFvf5X-9dyt$ulGjHc70#3bygFFns0@yOqG>?G~P%lz@xL-ItNTgVf zR@{#dHZTbXcFZi3V7}`0eMEzfzlQexN%Xv7p-(@q)~04D4q~XN-~;#jsC2GKSAKgT zcY{fB3fyU*AK2H2e=>eZdN>95i2N;7+q5U(I zoL_uZq@qIn?-cK;KHo*L$xhjA-VEenkqai(JN3>ZZ0~f8hWBsE5BNgy(RWvB*ntMT zSNM6mmNA*ZQFDtZZL39Nl<^59oh*=kBFbsC-0C(VX!c3~_=-yH>9?EXMS{kX4rIBq z|3hMnTvSB0F7OcLIVA$@eSL7&jO^C+vn<=PjGno3qHIB9RbItg*drGy7q;T_LVNy$ zYdS8)WwokaxYV2sl5Jay#oap5POpjDhmbD&8?PRzf9^WEOSK3G05?#VcEXEX7wG}_ zgXic10Dn20>0WvSP?9VI07&NGsMzHhYg*Z~*X9U?%uy)zxX%~@UVB@)^ph9BHhZ9p zFdBC31Llb@d!@&T9!KI{seh0N0UA7ZyZ&1@=~r>mdN5b~+`2Om4(oyFwCNC=h=$)rJfbc(x`-~EYI*U5QGE&Yd&)?&QruC}l#GXrW`-_(CmV|#4f}TO|l@2b)junan zau9OG*zb4k@3eB0x*z2c&o_Yj1^@Yz(C19Qh63?fMyBE&WTDqK*n^Z zHLO8GvfHaldo$&Qe$oA&G|;e*);%tDvbP-U-FG~>P!`TDqW)Rmpyz6!&eCpa>&vS_ zy=(d@-?I4wgvhW(=)gc0T!BXlbB@fI<#khdB9|L0IuTr3cRn)JvSIn!IS3qSFl7ti zRHh+!98*>63BCL-&fLFVQ}Fy3p@*nWW=Xc(hSd=>$XkV5E5S=w?X#mv&6tz=a1vB{PN%`a9m~L`+E65glCU#DBh(C@g@`6q6--nW-wnjfLK5~ zYJqhzbkPnpbMdO$m{2%!7VHF(g$(#N?Of(!{DS6u8aGWQO<`tm?4kGE*5sNdLJ9fE zlJMLFiJ*x={D5z#^y{-SP$a&iJ-q78RQ|*WS3S#*FV3SR78w1s=V{v&)`WtYo*Bjgws zgt7pq+HaZ%#DL?CO+;v4Zn*?mHh{h(rEFz8Or@E5d%pHz({j0^uhSFMcoOav3F`f4 zLsOK68%soRI!*Bo`2qw6{2zI?$Bvk?w9& z{jUGpjAtG4(H~E~l(M(6!o_o^$yHIo$98+~|4{_UlM~cGUKJwy=6j697^2#+4`F0N z?qlJJDpaCSMq1vu45?#?u87O5Z3k^Iiuq_sdev^kIzIXKsN36akU*!U@!2sfMH3CU z`5e+Pljq2PHH@?=tF}#+yDt)Q3Ma?Hqi?=9))G~?%1GnPssFx`UzQG zXPE0aZ}bcmKrH88JPx(ATOnO!KNRY#*fATOE0h`vcxRd`wBiAG zfgtNQR(w$$7^j_X(Gi`IyJITmdIky_M;iwFS|?@{LJ8qJ6P&AaYSc{-W$-*-cPc)}i+ z_nNQ#qo9vxZs6>x&wxVVspH_w(rC`5q9f5$r*zq8W?v=*i>;$SA~#~>lMJ2T+h?*K zub4xUJUd=Un|xs)KnLQGcuVOemU)l;8+!(MJLsZ1|BuBd*PIXkQe!i%wg(G+%+25( zPpJ$ib*D?rJdbM;CKnw6##SgAbd-2=o=BRT;AFmeN0Giska|ERnofpHh!uhTy_>CI$Nl zi9s_s&q7b^QMn`B(vaG0=#I#-;e*~Azh}jLrS{?*f@A`e*Mn)~qy;q05rb$4eT{J6 zNfv5Ex$5*gkmYlq31jn!+7fK)@4oCeHl5UcG5uBYbtMpvs6~S`72c zz)7{bjWg4T`KEAuKhf!_{knCd-o!bU3c`};DT<;rxTwr}h>`6f>~U<1~*vL5d$ zT&#E{Ka6LDT6D~89Z1EtLY>>TjnvpZ+(#1`Ju&5$`Q!FmCRa}Gf7HLDd-s9U-8`BJFm@udlA79%CXM_%2y zx>UHxn+F#X;#9fd;I_~7<-oCa327IODBiq!r`zV%8c~8f&OiE`91i|>9x${US=FBT zv6+uO33gm!gFp_0XiW1Y0Q=!?&fdG&-cH)J61^b!Mm@yTas*3lrIx^dqxTe~*|pk< zs);BOFD`spQ#=-VUXH}(*rNb$$Vl_29bh{|#;y?oQy$L*t!K(o&96G4k@kZKCl<6P zZoi#@)u=dFN3%|v{GD-^6Vd}eP2kmDN;oK4+CQ1N2E^(_L&H?R1mDi4Jr3@|tXO&% zLnfizI4|tsV9GdK!$RHJYuM-riUwS4IVt5|8CJc~_klh1iWK@Mn5~Ai%)U*$%YY-) zkxGvNzg0hFZ4qOjE*DW#=+v<4%^2;lW{3#(v|d_X)nU}AN#=e1RK+NZR8Z<@#^B~m znUAHq!{;l7rVs3<89w=`c=A?egfTw4-a|l1gg)EiLVam-GF;xLsWFRbhnzwxTb-$Q zlE)iR4o6MrP{ivsq|l(l@GYwy&Z?A0%-=_4{iJbX#`SL88ufo-p)up}GOM|- z(gf9RUx&Ao{d}(Im$Dg-%xWd!lJtLuKd|W&fnK9x77Ci&qC(HzOc53ofC_;YMDfPRGU?S4zqS4hXIyg z{}i+|-ueRqh|p+Ksg~x%T*ePK{53zh-uHbh#>wLTvoPB_c%MPulX91T6bFLVcmGjr z_szFo68#tcQGDCdPbs`FL=iV}mufx;aI-$K$P@RELOf-k?y|r64q!L3nd_7mF(7|7 zB%ot0CPh{Gbi0oOv*eJvZTO;YP#vl1$GD?v4tu&eWnLkoS%xun(@IW(W1|&s@EKgc zzxw#qgD6rk7)W;RAcvk5c>KLNDlYm<`hbTnIkECR7&McPW9l{d9~+$WDG zrL7@i=)4l`LWDIErv;jF41YIv@NhP^%Fv35b8p*Z&%iAV1-bM48#m)%`)p1d0tz(p z^1V&;A%+vb?nuFK)^r8Roehp)&Q|YFXnYKA4JE}^0j{N=YzgjD%a2|6zpbR{H&j>MFZ4F`|Llg%ltIA zXW?yNh^qAZBB^q&J%}rDQgLz~g&u(SeMZ)+-}N<8 zOkt=_{Fd7Zo&Imjay{n(hhXH3B?x)&JH@PEuEo_ z8k(96t%$>884q z1m<}TejC^KtW40VeFK{Ea~~+0fwo@O#i8$x5=_^W{X{F8tDg>d`$6a`&10Sm+Lj9R zU(qfoRG-;z#mxl#q@c_yw(+=zn)?f5CK)L(U|SYiy>U&T;uGe%<{-9{yl@$$T)lH7 zGh<7#0p!DBtXyevI)V5FY|bbh1prm^%xAGiS2Ucy{~hV4aQnhPXRrEb_*FSQ^wwkX^jyCJ5tr z=S{4W@v^;*Vl0F5KpDi{(tx|Pr2kHcN&@4$&3=>O0ctm=cDu3z5aL$qjTAZWGD;Ue zTo$|QgT9F!8c}a7;@@(|ZRsPvc(HrGyBDpE1!ojPlGDobT0fQ)Qn9%!mwCPdO=U># ze>v%NXy=MTU!wK_6#+Vq@;C=$t$OT?h1`rY|X9dnNI^h_J$Uq-Ql~htpeQQ-{{t{?jv~RWwaI)Mw3XYRPhX1Z_X@WzxPn5Vq2#8HsIlY-%6( zfcJT@CD4kNx3mpQowxrg5qENo2PcdQ60u%gb}}KcJ-ak zRNfZ{Pil9OowIO~yh@CG+wO;Y(<4b(8KXpH?XMX_P*c>=@cR90183-~JjW-6*Gg3szxiY}`9sfzjo5@2{1S27lpHrWU)x2#JppL=ex{G(=h3Oq5FIpcixTMIgs==X_};mhB}acfJS9A8Y?!LNt_TI$BG%T zrxa+nlllXkZ?gdrV}8!+M&`#iJJ9zcVe8u{XUW7kqr{ z-LBb|9=LeO*${lxd{WN>2KY}LAZRlYxLoI!UITdZ_fclU=hPO6QWyqfV^Z*90Qo>g z!(HXqc^_fRh9ojgx*uSmAQB{t;K=R9qE#c#MNc+1cJqn>g57xUU0h}(9o9jxsGIH? z%v-?lDJhvwvRLdJ)=P|vjs>8=;Z*_kkA@w^%{zgFGRq_}KgVUebv4bB5q(XY{*8Tj zED%#uRHieO^>9MaNk7>L7|olJE+nM){1?#sL@o#JHiVMA%1y=IGdHEW{nMz$$UI`p zkrzis7!_vDEd;TEuE_9<$q^Mbvthz0A^*7C#a?@4;q~#fio7~XHpiV$&9(NCdfIzr z_g8k!$leygpF7S=t>}h=_R8S^&X=zpO9GmVYh{l|DxMHh4kcxb2AXlsjpZsWDX+Y@ z#x<0YF_}PMB8jSoRUc@SYv<~ue~Yn_E{}r#+)421y+Db}i>+8P4?mN-lq5=wna!ay ztA+W%@@Fiy@_&3M%Y#6Uw+uU%rx4dD_1TT5jyR&dafdgUal+wl?*l0}H@I@e+F~@O zo4JCRo7uBjAgNs@v(N{AkzPJY)~(;NqsdaTQv{ucN#U3%b&#foMC{MWYMh(vm%nyb z_`$6mWw(JEzBl%-6MaDOTmXilh@mf*waChI@_wi*C}C8#%D1v2f)oyF${mv$=s4Hf zQDS@;#fZ2ZmbV{fMmDT`^a*@{H2U;QIVB=%|IK9d(1P?jf#-d8x?J!Az!{Zf2XB9J zubAz~umF4nL@CWj=%UzFkVjcNONDr^8xPNe;t}P2s@g)gWk+x2-$Mla{zQipKykU! z2xO~Wn_-~axj4!di+K}+*ir4~@<;BRK3ZVtsR~eXiHm6j!qFTth0a!GqjJVJb!x~K z@h9tFXIP_U)*hmT?))MA(QpoW5)qPa0q8ndXeAf4X)F)1G}Tj0hGhGf_BwXW<3{rM z$y`ln#xGgoFf0Ux@0cDofU={SCQeh>!bd|Kc3>A+27RJQBX|d89ss1|YP&jl{Ts!; z%>q18o8sz3Z3nrX?@EsV!rpqXy7jU==g!)#TkT(S>GF6yyn@~YcD)I#k>B)8I(s;k zJ2b_3k6S6;wQ3Q7tbX1e+*Z|&*&{t@d=;RCkgA?FksCoUoCwCs6+bbHRC$A=Djg{w z-9WNWq%NR*ji!CF}xax3B2}Zti{w6N?lr z#Wy`~U^n-{2h*CLLaT8o@#$i0Sd&4C9S9@lI@L7IgSU*dZ&6ON8J~;+|D({f6h*$Z zxNq5D>p#w<=q=eJG`gCt6vg#e{qy;UFkVnL@iy^Gqt(dsROF zC0qj+QFR6k<#R#{>t;h^A}4#t~^7;cfQ+mbW}ltg&h+t11yNM zip|Fc1ZpY&kV+{qFKJbxxGHhfMF-C_`(Tv4y^6ncbgN!1<*MxqpPSihs$KTO#E>a# zCUiPn;W2_^*92-5K5pdyrg#EH(DTQ*d&$>L6Bs38FKLU%wPpMoOm?seT3^87#uOc; zsgZfV6KJVNEW$b%`QH>l`0qWmnYkY#bBhd-HPQ~h^Y|l9SIvb7Akp!c^f&P&1+Qid zfn%tv_zaRdyz?e$B%60ffywi77F)@}uD!B#;<w{DbMkH~a&8pQL8TN{u(U zd5-Q{g8#C)#iSsjy-&tgfTpabByPH>wJ47M{nmzT4o~g%tyt>nX09D0Jk>Q-5QnPw z*j>ptwV@87(k7ctCKDMhcGeEnrJ^jXFlJ?ctJPEKcWEFY5#Dcxog1QZ*VC|OznRrQl2Fkyu$wjaoG@0Ny@ zl-%OhqSYEqqD^!!dyZXUudT*{+7Y>V@vxk|+PUu8^|XAQFU!Ncz2pN@Ko$aPVt0kTau<99$4`gBpiu;+YIG_1c1VlOspmc_O z^CRu&kwg>6%?0OCaRklgn-rvFHstjvzJ#-0-M#zf%EO|Du@lPF&e!qJR%hH$wo;43 zWbrZ8MzOH9uPxVF?r|*6WRWdy7;M^7yMtNKAkv2*YtkH+qmNW|33M@Y{tH;H+Vmkq zUu0bN5K3rC1_)}A$w$ot$6R9{32t6XP6G~O+0O$kHVE=g$f*iuF-G)>ef8A#@CWSn zv9o-YmzM>*-OmY^kg>Zk49=7Pe&o$NQmioy!Xm11V_>PUt7_wUg@wNbKHmB@bw6(& z%Ht|uSMi}p&``+izwjdCgSMR0^+u_G6u1M2U_71nL}>Zxf)44>ianTcTHCS{Spw42 z;aLR)dLEI4X*^)?oT!%DBk;=V30bF?O(_%cz40YSG}!+Bw1i4aFrAtcUi3LsHaQ^L zt)mR2O4e2|3??xL-{s`@5p#VB1ar1v{0`hmfDHno#z`#-bE1K)s9ndm)j9cM$^#R- zU7O|Q{wlMzYsvpCo~fYEfiSh2fzh8MIPH}Ox67BbmeyLuLh(J*9MX%>-%@yD*Th-3 z@}@cd%S9F3esm5X?m0t!XhMS(k=FV>g6&-o7Q!>RXDVEm648AWnDHPC?VcciXkH3n z&*odhUQ%oM78Bn%HZ@Jt)J|$E)w2L3>6VMjt;s%Fp zj6(JV3vpzp#s$LM({C(8MDi zI3~g>oS=wxt&Y%=M+*H@V?1Ie5YEJJg#5R{9mTM{BKXgd;P#}p!9Cx>eJIMGvDWNw z(3BHcEpuf|7h)?oX_6>m6kThcomDQ@vh?yC+`IJ9RS~5KmvjxoukN~`a7BZk88i+G zAUYkZBLUY)T)WH_(}24TRf`;p?LiVQ-!WNJld}9rxm?ID%$-+X1dbjNz6YKgQ#U!fO*Ts@dKl_NljGf62e$0F6}~|H|J(dtPn> zGVZrDE@Lc})U(PM^;7D0gS?JKcl3E(8ZPN7*PHX0+fWB%`3R~+{T&m2q7Bh+Mq;8S z@aOM~N%Q&=k;#y)v~t4`t2yx2@s?fB2BWCHp@Iaj@`rh=dL6GGx8#yy4L1e96)QPA zwXxvV$UTq??qoK;#Ea`lzH?FyC5v3@iFbvXZsL_XJD!r8AIytx&X6K<9^PCZ6E>mC zqbySz*Nt5OKGpQT-V?{$UFgyIhC&ak$jIfh&3JK?7<^0TO3;mCRF~IQhx8}02S!5- zevCR`DFp7K6$iHJ_eGZ#t~ixkNvP&-5O`x&(^HQ^mY^w;7aTSr9SQdo8}RtR z^=g^nIec>r8o(vug*fmMts41u!nml%rP4cb9_Z_=dT-=rF}&7Tep(^Bt)&R zOM6er!iwU0LY!aDKMGMraz>7`90iFTP?R;XJkC5xV2O2;z{8AK&LRVM0zMj zLqoLMFVB^T+$SswbUbOYTxfaY+!t5B|GbX5se3ZPPHjeOa#RbnWw|+0$WYs@iNNNe#@;%)1q5t8QzK81MSJ+V6PTFj4j11K3}xtq$eqC)*<^UBWjgnot(`# zE#(5Jjj~#R4Nmt@9;cRg*-)}@a1?{t42(BvUfZ~#hd<1*x*#vQE}6`1sul&Rqf!yl zOhI(s1aS+s-pn}I-~FqcW~x8GP3wO;;=bJ=FTl*OOZ$N>vpr@*oam+58aXc+Ec<(llY zL%r#f3|U)|_opI=!I}+u^<1#=q=`}H`7=8|YakKfp@p{UE?j&DCU7Sj$P81($FdKv zTSc#1m{}GwM1086hWXuvYPgq_Kx=w z{2N`g=jsu_akDZ)Q@#3_ahZ9b{g0jSVJtt#&Twg(iBWyxw(QT13X2cs?g*8fo}T)b zTelmYyRA6?HfpRU1jo_c!o~jN<>yQv;S`V(GjLM>x$%fLr*_9aBhxP{90q5k#g3N= zQoF&KB(A20ttjX2F4z(-3&Iy;+L<@yA&C-k#Zz*rbGV&W#C_*+KX%t6se(rHg- zJ&@@-Zy7Ib&*r_V-2l)mZB#V>>5EQWFN5iq(@U#n_Ct7N*Z*y8=61W8!~j*6IA&^bxhndEC2~LsT|b&qHo4 zz88jiTZ74*B~yE_=m@(}EDkL2VFY6gMJ4#y>AbGqsAKMPf=CO#4wHLGpGZnmwwt`C zpLkU=NAP;#E{`iE`=^f=)d=p5^lx&0n!$iHzaA}`J?Xu?1=Ks_P*9MFmH4g#TM=YQ zQ*HG8HOffJ-kI9T8rhC!x9eIwPMI-pn$yKD=tOzBP&4D397e?p+ry>xwEUbU?F0M+ zn33He5^wwd$vmJVv-$B$F*pe@_}LGo7+(&@=Yx-$ge33zr{weHORmb7ynWf3WMl$i zGXQRKJUQq|6T0^&VW-lp2G=3b-uD~Oe(vP`;b7lupKo!iga2}e|4TdL&LURx#qk z;mq#5V_D9OS>Lwl7I&}#g3xWInp7exU5@Lmak_A@flMVl8oN0f^LEQ$O)9175hf;M zbw-ro2CMpQy_8P@zoPi1c2PC}g(D7b0tJ{Sa19a|-?z>5`vUAdpR_csitla<3n zE@k{9=dm6C`h@ za2WMG$9;bGJyvk5rg(ZTE+V3dAkc~G2yfTN-syvsOf*n?RZu3TNZ-=mh*d5)gdg!m zTE}fZ_=3lB>}vM=Z2x72EP(37@1B`H415sqxDo2hLCl8k)KBg1^9BgIM6TKo%}QGz z6#V5J-Z5Sg>yaH(+MxRbNC1=#DF((9xU$Ln1EC<}l$9W@bAjbGh3Vt(@cx@)kH@pP zzjLeJ{CLY_W5WPix_k2-cyRyk(&V{w3``Kg7@+5y9}N#;g$Jycd_3`9csD)38dTSH zZW+;8CV8*ha{{Grl_?w*A<>-Umf|FgRVt9Aot9l0W_zW$UT#8S4Pv;wyJP0#*ccF? zab%Rd&s17rlv#`Q0DF?;?9;}yt<+VF#jG81gyBqRjYx;wF?Dlw!#ay7a&^S-smQN6 z8dOD-&w`ER?pjwtK%)rJ)^83zxpneYIzmB+S$*nQOD_yF1XwWRYNYG&Xzo~ow@545Ffl;u9dG5+w z6ib-8b5Kwr_7AQKpl;rM^c$3=Uho|cLx75@wiFZ4vZ`+zHrdq%HTqK&io%xY_G}Ne z;+3{^*n?ycjlA^*jbgr@bsgshMUTz(#Rz!GH3L}W#wUELn(?`qcoA~tLN{nCiVH=ku()JoPOjdu zTl$?A&%tz~*fTA9j@DIVr=e~5FV1^-qTKDUX2;zOR9vK;UVLKcjj=uC>oytfvjf-6 z7@1dmC)V{r+pK?EFPtwt;;CvK_lL2AD%omg+&IKk$(^rnb|K1Q;!K#u4?A5IqvarK zZCZx&PP!s0%8hpr zmnuE`8F3-(N)rX1ai5bg$=n~sO+h?GmFhEjT%|=i7yTf>2Hm!tZa;&6Sa)N`<>r)X zyVwV45PxuJUV}et$&pmjh{4G<(;d%Sh}4F9c7?c6DQBy-4a--hM{f%2Z{nYcum$Uw zLDE2Ro^i!_sb{#Y1Nx|Q);%$z8L@T@TMKS{5}@gIbCZ}n;?2sU(Y{xKwQw}P<70-( z?3Zs$ut7SN$0cYjs=V+J!}_~R^1h&|dnKHBP_uv%-SK_XJnOl;XZBKO=83UPYI+Sz zBSv85RiN9P_rOmdc8!fJRTpzIUn0_PY|_6dd$CoX0erCX#+4bbD~q_KvDtmTua7gr zdUfGa5V-!1xlJIFW{KjF2SSq7>q~PcpuJZW%=Xwf8aXJVO z0h|9ppE8}oXP0>H3zLN?wCzBqptDtb6G3C6{5z|0Gw0dEy<^_ImFq`y=H^H28J1CO z!96ZAUXUYC4Z-TV_IESs>GT-AGtVy@oMOK-fA5B6A&1#w7OR#pZ~j26mT@UklZbN@ zmp89t_xkuXdE9e~q}dM2vDNC|I<%C75gY7MX)$iXsJ15U_j-_;jX%s;Vs4lB78f=> z-)I%JFsHG@ZJX&=IP_ew)5iEm@bEGof)FO*Nu?@x%0&gdkk5HTsXB)UG=>>?5KDIq zo5@lD&X__C8@_Ll?^niJ$2Td*OfQ|2jK8FS?*dr@-aVVqYs*RAI z;w{Y*y*PgtfjB1<3dVk+>-(tS?Te%ReQu(lK-*vkXpVK3@uZFAOA94iv=pjj>oGm7 zMAxxYdMgTUvg<>2=B}5YUTYnQenph27d8r@ zP~biD@z5vZ#qVr7mHo7EZT~1>u!VS9Gh;&yr^&UtFo}T@Q`xTE|3%q*hqLv#f8*L} zb)hI)irR10@Kz%&MU~p4R8bMy+965pO-rq+y=kaDLyaW1s$HwZ%rt5QL2A$b@_ByG zKhO27@AF6gJLkGi&UMbY@B4MX2D&UR0imbj5(}UkM8`>&+p&9{w=3*Aiel7y1 ze;MvRoPL2sC;J6UTC+H=8pRFgPXm;1FTFV^A%WAzxgw9QsepK=htnNFq?`5U8TDWl z)~Jk=l=H?s*Q)MwgWj3N?MQ8@8v>jm=lE4!6BfYU%{m7~BCU;`75NuMC3P@$6ZeKwA@k4k_M(KRb2<-AO+>WvKOD1=$)@{V z|7lsU9rWC!y6>b9wOx*&gU2)AJi<+oCFq{M#Atmdo}33z4CdjH{_aJT#Z)=>q=26% zOl)-;J1A91Q3MVEmL}a8MVnsoXYgCT_}6&lcC4i_M*{BOY|eaQIY!T&!LM6XaQavM z@Fzd!UB(-f7eBR`lMMp-1K;(Kq~pXtQI5iwq!f93)s={^Yb`h6MF&P5qx(!=5jeFZ zfm6nvPTY`az}R4~+qcYr8q;Q;K27HHbE)GxEByAj+; zHReqyz&WlHg8wqiSalqzZpE~PaBki@iq~+e2yerd?(QyFZc43fT*}jLM1wg>sS+YHhtw4x}1gt3NKX5hNP3qv-^Wn)s&8|S zfebS~yU~8#Kn~k)kh8JTZzJ9dkTatW7#`T>o*(~XBO$F2Aq)xl!8PEbXH>54;&;u; zn;s!;%$yloa7o2F5}w{pe9CYS|C(u2U}TwZjdve7t32E57bpm`i4KW?e@4ETKToMw z&^ui)c1FB8tnmmh6LVI2G!7(6-{t~K}XO-G5&}j%IF{Jnn97&MZ7}ac(8G^lnk>xp-Zd(c9LPZffA7mH28!3`no+Zg)mU zdWAZmk8sL^%C=(2FiHoV6y&IO7@jrFkfVy))TE!9pdHI4Kb9r$4FOv9e=0N1-e&%H z0G9ak*BHAYyG*{ohw3BAHaeYe{rJUShQ_$Y(hc?KdtyQ?-#Zw31<+yz^@Y7kKI>1i z@IG5f=(V}^Ng8Z;0feD5>I2n9bY@(Fj^Fa32$v?+e(8*+MY)m|z=k8T@RvjWIx&Dz z199KQ#yv_B+u?t{RGL4?=L9Yd(NWo}+^Pa?iY?lbKQ=$^iR;sg&kR!E%B#3K5;m?o zW>O9Pnae`uAH51IcQDJwDl`YDPvBxq2ygZLm5cA(#211o(sOtIGUWHSk@xs&1vb2C=N%vIB*W{@GJdpuKOMzoNo00(Rp7{F$~+UP$&oc;UqlqBCO=ZLON z6}Lp1fkAPhO{!acxqRqm;o=WSV8vGmbBT_aXL5(2$Q5io6sNZN{9jWCCY7%o%6}Qo zm!Fz%DQtz$hrI^M2DY*1JlGYF!WWO#Z{s4JF_K34{AJBeLru_$W|7-Eqj=}wZ>!e^ z|9KFR;INMws9kLw6@3yDNcVbE6-dv&EW#H0@a{g-^c3hQWKON_xV;{PYE?Zy63xFs zma+`ytM$fA_v0!|odBQ^f6lgDH|!oSf^7H%*Q({x)Hie__Q3^2-NDO`c}zL!uTHB~ z6dwIfdlDqwlBqT-qBxr%u*MqsW1OFrNmZMV#fwStEVyMiaHcH5K{Nk#Y*aXNGfkyO z*+==S7O&Rh+f0j%$3A7G+=;9bomL^k)bXY;1MRdii&SAF5=_Cdur~ZK%0V4Xk@@0R zSiP(VlhD>Zdwp)H>FDgL)a)AZWpj=Uw5b5Jda46+fq55uJeS6$ZTo&COnNC)#f|sO z6U6%7uxGtBEe0i|b(TW(s6}sBAEvqufW1Q)>Bz36(dXP_b1M8yaaUfAybU>4-3fmt zU=h^bFa=b(>r>}fxH-JnpWGaP5YmVx64J>*rvc>%zkS4{c?qB2vQ@JLOmTj^sDb`N zLhc!hB!eUl90GZyND2zgAm3UVYb)30brPGWT=q{XP^v%V!W7X~*eGQ@vVwG;Rt?rJMC*SPZ2BkR z#<|dZ#-q#0BmVDWf9qd~Z>l-s{@y&dxo|<#W&Gq{6~PxkTVLKJ4{8AMp!Yyn;t)AqySh?Y*43R^kXm=Ny6pLEBu_^-|3{_^8a!XLu$bMAo-|2q;d$E9{*l`m z%M~AE^hwjR`S~==k*4F_Cc{|Jr&-eMiYAS;Db}NlL}kTwL;l2e@u!y0J$MmUsEA5p z45x@c$Ch%i1EXX()?}#0|2~o7?IG@+1g`1J%oEb@{3~yLn*4|IcG%I3T?JhP7ym zqQRIFr{r_Z3DPYf{}j5ZZ6msAL1#u}TAIE~Oqyid;PIMVz83j==X>0$)GufqJ|$8x zPdxYMYW9J+&2kq_^e=;l(O-s_d{>O${kW-B#L;&cPqvWRI@uDP3+o=~!UwhR`bHC! z56~U(=kON3@<|>g3#Zbf1v3}En#uGl>={$$P#a8B8|P}!=2INXu%Lksw`QA0R(q#| z0a4qg&z*Kw=t(-lsl7H%LemlGjG}+8+_gS1V?_jhDkg#3ZeEUJ_%>wzK4isgujQ+8Z* z&GZhnZOA3j4Qm#_f_K(izjc&tInsgVBbgZG#j%jSmzG|3>@3ZSt_*7qzyg0NBYB>@cY{2T>oVH0j&38i4fSr#46;Z zbx7TG1tFxLk-WG0+*Bt{l`e++-zv?j5Wwbn-r0S+gq~^N`Gnn>NIEH)576($*IVxm zY+vvc3W_!Z5E1241>0lZpJFk!!2@so8fT&_M3|1+hHka~d+*_rR7#miLc$Y7ztT}w zv@HJ*$)eVcgAcyX=uAuo=F}~{5+C@V_-LZ2`{)=QK?{Df;cC>sj>FiPLSC(%Y$FPU zp*!tD{ce%KVwQ@elEBTn2kD~dwaaw6RL2&rXs+P<{wL%tA^s*P^~A!d^~K!YTK;`t z?jSV;8B8n}*-#*oGf`)T_#w#(*r-2ML9((PJs8GM$gMrRmX;~qV^6(mlK1s##8$q} zLNW!jUAVzo82obtsy4rOTt_D%C=b%|<~og0GRN>wND{FX-KzV+`vZJJQQd5Vr6Olg zvO9miyZDg57wKug_pEr|8Nol}{j8Yw;AX_vp{shzjGJ1q@$Z4%SB7L-&g5K=$#Y>{ z2F-@w>(;K|?v2v9dgFblT4R>srMzERdNwmCFEk2t=yQ7 z?7MhRl%ePMjk`Bq_vWo@)!bx^f2z##mmyx4Al+5cWz-3@fbL-rf)0Fq?XnA9K{f%+ zWO@2RD&WwBWod&K6Xg{1psQhoBu0?-mN2;;?(I%N;vx3Mrk4PUKm&@>(~XGzl5&%L z#>+`T{UWE!$K{+&(jQg3hiO}+ah^@JwbM&WTfT2d<^{@)Jm()E$?2|B4zp5&XXRrx zn|CcXHVCeU*A$AzFm$qwmwX!^vWSS(!-!7pN&Nel;iBU!QV!ACJpKmm+zgO)zy!_1 zm+fEh%Gztl)X@WTMt7v-O*6p3viA4hO^UJ6iYq-UcaC5k6Lt` z_+gaF?hPZ}8xIrrPS(Cke^%|`Q1iv(sTEs@IU6VpZ$2k$Uu;o=yESeMn9M#a&n=W2 zM+oH4s$XVwa*|x*+AN>Oy7oh=`hdZ;Iuxwb z9Fh`fNRdp7ODy#s*k<*38*Jn-(R5T`GGep*g?gJ8UUbN%_xVxlzg||UN_4JPN+SSc zp(U46>ZC6MJ8ybZxo$IgjU*QWB?&ewy;o^}tJqT)A ziD6#eRM!9b#wVlh5T(2!*Gw(ctZ1#33=v?fihyW5KqUvGQ5Sgc3O3^>0@lCI8H zril>Rc~tIrU6aj0A5;*DczjG(1Gf_w)x2QWVGh!`{-ebbcHa`7SeZ4YX(LciLV6V7 zrKpyh?DHz)SNfkjo|iBl6UnhtV~RR772mskO8(h&Kr*^5&~bc_LGs*>QoYSJw6`@k z&!d2-$DtN6Iw1M*9u?HfI3>5&xZmU*uGGm%gdPi-vk>okloV8Q zRgj9*0b`;A1Yui_y?W^9fWLR)XVW%T_x%&L{tNG}Bq$3n;vAGry#(GoPV7HBqk*0s zJoN1tC0z~-rgH6UcuKtEj-Cx=RQ?{JTLxAu;W-1EvhFIrbP|1&C+jqj~1LU^p85Hdj$rfnYV6lTO> z<)@aTM`C+AHl{k*JLZOm8r@5L2PZ*BC_JFi@-BtBCyH8&FWOY7&}^DW+ATc2-}9j< z*1ljTE7za1+T0)|-Rrx(v3i2kjRkimU1NzWj4W5g7nLJXSH#4>eKI%9y^|XS4*( z&qGBJ*5sylxe%uPJT}i(GGdJk;>|7ZD@I%i52?{{lJ~6t;f3WMhF<%;CjID__tA+~ z8&msDVhJt?ys!e>8`vaGS=i7CQEHSSH=#u#@Td&`coSTIwN<>tD<`LYB|99)E|n&- zW>yKT(8~B@;g8$F*H+!J|4;4ENGyDEw?w`BG zHlvnF>Sli2O0wLiz{;KE(MtdF3qTj>lCW8Y{m)*Bse+$qtNNF^j3Va|ePa5EW$1E_ z2HnyijsQI%Z&cOq3HXMDMb3h}*4qGks%|H*;9P4f<78MsNc|MzQHVFOaMRqypl3K- z-NzWZgu{tw$Dd!6x+CEq=_dc;hQwoxH zziVB^I`rf4^6~0Clz+`-=re)J)B<`FoLCHGf})-kSUB^p+o9)Ytt)Hs>6}Jdev4PN!dH)d>TX*lK&BygpP0|08e-{>eJ(SNw^ALbA21Q4}AylS9wOGPMLoTSYny9_HHNtrSQUYG9PqcN$^e?MIe>s;^aJAH9s$-iv0)w?z_Rb#QKQ=mRhc^o zOKv>7D|Z$RnPYz8>fdYmbAo4*IY~}0nL7d*?Df+mxcmIZ#G+YBGZ)#-pj%((R<sdlF9lCE}Jd9yAjX471T$w|3eWIS^|zS{L2N_0bh z>bdmup)qe}Ed`d@r`Wf{8}ad_UwhpL-Zi$v$0Ln2oGET4hNZw4?^LdhwMl|N6+8T7 zNIUJ(oCUOU0~LpNCEVf~Xqy%4uE0V5GT4qyIm-ZrRLGhwt(Z-S630`{$=Su~m&z_E z?Qli;CRmJw!DATmqkF)<%Gqg|N@wQ6USP$;GhupHd#Ndx&Uqs}W}KF?^ysup^Tc^p zUKy3=MDtD+yWZfLFlSbUOpLUb11y7sb|3r(QTIpcbl>PDSbTtDs%~DMexz&qi$Bn&*y9E$TH-UDN6(INHUWk%G<5&~kasfpx6dnNf{}d}F0AL z#17MyC`_lM_GkQMNMF>9+a6Gd)SH(6a#h=@ifI4%Xd(N@NL&zgYro>;{zg2lpu|&Q z5&{60!vr#wV)8>oNhPaOQf`7l$!?x;i3s3ODBZOe2N*5t(hMvSP@jR7lRp5vx!(i= zDDWHB?1ArAFY8t&z1=JT{gtF$NVct)Z0^KDUAr3pqfxF+}h!VMsu*8r9A+ z-tbLI(fML`&&xgi`fWGeCZ&}%UY+08hws+_nA{SsSi=ySjnpH7LL228t# z@T%Etwngh8>Wmq^35zSBp~?XKg8SlrUSRqr-}nHEw%E-{#ad!Pb)2b1r}l@2cy8?< z4!g<`+~ul3jHW@cZA4)2ee)OhG+g3^$4gVrJ7~)=hOyH@s*JGBC$>(E`W;lT?uq}I zs5NV_U@*^#=-_nF7&l;YG%LqDpAeLvn!m3*^KMjP%AbX>IL|hzm<^|+_~Y$aJaL1h ztW2p6za-d*o&&`xMr>R}Csrp=<_utc6RN`OW!?#z2N%k-6mC(J{uEepTTN5V4b%?L z_xiP;c{dmDUgvxKM?SnsGjmchv3y4pIaW!8nNf`Lr+;6r0eJ5dmaJ2muZ(fDTo4ZG z$efNx$luNd$G7iydxy*h^kTlt&JpL?^`)8JfuRP??s8RmwJ`A*=FvJBJuN*6n_v!a zpGF_+tPH?HeZWK^NUH$^Q?;?Gu=kyy1@-pZ*E=Pax@v^y2W&Gd}LU08@4RgIuL==*4@CA~t!?WYC>nfzlg58scmHW<-!MW-4ZXDWYXG z#CBq@b&LeiQXD;$ez((>+|u{wKCZk|mP&(;;Q5Y2W@fT}H`H0gXY%t0-zPCXw)B<$ z6@2MT%3sn-Lg^WQHw*TlX%vTaTx^g zAQ?dVEJy$@dqBAd0+nQI=7+=W+pK=CDH&$8kq~2-8x0Xfa?paO)AiUXXG22ucCHQX ztySZ_%m?5fR%P42+Z8-1(u3I#UQ`6rvv|QT#zgvpCM0@ugBFof2sd3&96-*wekU-Mja8~Zush~ouHcVv!cXDZEpH=hM7WZsj2|;7ongq2_ z*8dEI^Q@Pe_GdQfwh?N9S%Al!@^v}R5uSpLsycJWS;;U$A?Cs()uTxz{1}_~kAss}k@+0!{ z(|iqACQJyzTeVe{oi#MR+XmvDY!dkNMawa3d$YltOfG%8{%5tH(m2?3*>F{oi|zno zW8hK&UeF?LC>-pA)D>|P#hVJ;#6-53FdhHEHRKDggX$CUU8Wy(-Ao@&Q=1?m*S|pn zf|`yU=N90ovm~N2JcKYlTqlnN_|NZ`dB&wq;zINc39*AM5J|f}#as*Iv3eAD*|4SS zwk-(x1vFN)g{|j&@_=#u@(yG0;U@h{7gQJ}-VR^Ew>n6n6Beq0Q0kT!rYGatIVs(I67zz);> zv@ zbyb19wQ{G+CNL3zLMyPC+ov8cmE4nzSBk|gi`~4ml+MPb^v!pz=*3pg{L8|>3})WD z_?94)LcuV0jSfyp!z680_=AOxKhT{X*$&#-l+YPT3Z~1y5EwH>6nkFH)~t(PCy%SBRrs{spv1YoCjqHyfJc=*^7)pG}yT)bB-&Hpg&Xnj>X~xi_8Z z;gOE8hLs>6cXzB~^t)y-2<;5j@@(V{sv@f_i@y66VAGhg^=C_nX7_+Ep_L z1kAl~ze7M^@KHx=uhv}1OTn`%#a5F?i39RUoe+hg&M}RIm-%z!bw6ha*PM+kf~DyIp3l6;+`{+Nfv&0#v=@ymP&6K9 z%6Fw=2fDR#+A)a)R>{@vR{-Y6VjPH+Advg##k0JAR(9zi*|a7 zfO*UQ`R(57=-9I{pAs*wh)Ld$yUnmo!|#o_XINPM%PM?cz5Gzcvn4+CCIq%t?74`h z9IC9r=xp~!`7XkoX2WfO_<72cgq!9qiA-3`pPR`H-kf8TU4c&mg;qx1OC27Fk2j+Z zfn7^Ajcd+499Tp%s@)#pBXJ zwI0ZtWv%+AuHb5g7nKig&W?vm6h4hObIz^TB!}rdU@g>|0sTW;n6Diu4;F78nWv8) zORb5D)3JYQHi}0!uDd87ecw*@Bm^lv81dm!Hiy)bh4Ly;Wjfv$S4Y`QUO{_uKR1k= zCHvu1Y1a?LPF~jc;MZ)TdNPUC%p?&{mJC}1YcB^J$jhFD4GHIiTM5*?f@i53xy<+| zO9;{K1^pa=PFgI)Uax>l_i z)mJXeW{c$%DH6WP@M5TZI6Xva`-QHlNkZazza+!o$^GJ!ANxc<;jR&y4#)mucVXaE{OO30J&GV#%tu;qo@Thy_JdQV$-OFB|ZWi3a z+atMa*RRf3rIYZ{>A=q=TqP2W!^>MPP$7KYxdEPFMrVKKAvKzrrK#g z-Ru%5Pbf!I&g6?b+~YCmFF-xpGCOY5aa7S2lPIQwkh$xD=7IMPCZ|tBXW&1 zT>iX}Q&iJlmOmFha7+=qyzcUjj$Cd=r>4&p&8wO~PQQ;~VnKD;s3IY!*xvYJ4o;$Z zNxK*^tJ#@q^EzD7>auyH>qz+=OHh)=fhrUYUTo*GKwK5^Q_^nojXPs;j0~dZt`1F8 zW!ph>t%EQ|<>68MLtB?vVRSaMda87$t)`41UhVwyq+E|I+nQ8!?0uVq|o zKFYB5-N`Wd`uvKRHX`cMxp-^(AG~>07}{E>A6SvMImdE<-O65w@jH18Qw<0Iw)H|b znY+p>L`@nef8Ezo@5k0zOuRjH4C;buMvbmC%J{$CS4ML>fV++F)b(-S>}0=t+lOi4 z8jW!)dOmw@cb3~uLBRVjg9=97{_cvH4^Th&4a_^^WX64<_T5hGkZKw#Ee4D8-`;~h z#ipH5j!_fRo~Kohyszjd4_%b_`c(7G@Zo85$USx)@u_3gR8QiGfK8Nn*k1-l-k=4= z-uXzMkQm(haSsRaR3$H$Ywawm=_x+elB5DK#zeV^?$Oc|kJ zDsJNgc$*BVMZ0I&*jQ)k;}c2nuyski57uRr&MLW}qxGy@smbG&L74FR=lfd8k`D-< zwyJ3cTQc((zAMBYxCOrO%=r!SjKHee8DilPlQj>P6|fidfS%C>i&lc5QMZ;?@`efD ztYMZ97#l2Y%_+xwDJjC>%3qHEo(!bRq?CDHK5y8ghUhI6c%pQBKMNwO1Ru#eJ6fx7 z4Hj;{Z1k~hVpGqCSt6S{Zyw?kF#<N2K7z;Wk}ud2CD}lJYx5P8b^4#t=dUZYgxQ(Ee$7WZt<|Urk0^6>ZzI}`D}^Ja)BM^rqR6f&B1-gUP@DNou`It;NWDhF zK$}MKt%NC?v|~V1s-V)&fm5K>$>Wvyt2~&wP8uIR=vgD_CW?JSViZ?m@a+`0{dLY- zTJ_7=>X?&w^_!uR*oBmkXa@S$*LIBuFwaSnoa__BZaj9p@- z^D?kzj~={=J@j5KmTRk=0RHpqSy6Tp6f63$%?#M-=d~OKoh2Y>gtaVbx(?Y(K77-1 zHFL05SOci{drGuQv`NUcP^})H1u|QE){5dn?)4=K${&fbnbp#J6T5zqKRU^n3*U~z zujHz&tMU^m;9mMX#;VV~t@eiA2d;w3c!9P_!j?& zM)NNYByx912T^|+TvUYqGW_9QyVZ7HMZQJEmzxHi2ZG^elx7UCO*y)FEr0LmWG{vg z^vOvU)~a+Ep6dz$XAtML^{1xJ8}W|cCW>Rf*Y2<^%p$CPcseGz{3yt_&uj z`yUQQI?bmln2ozSGBN@zKWD49IslC0wX9dAVFMcQs4mCOn)=vo#Hhzh(xwM#l;;!w z*U5^occZZ@OE$rHm~-(YDeF_Fx3i^9kw&zz``zK*+}zSqvz5p2wd#-0{xW>84zFMe zLtZRj=a+#BYyM^6+Yg{C%Q_`u1=I<`Fj>o=)Y6jvG0UvwfT^GLM7cVgV57`k5Kqoj z_Unk^O87~j9%|%=^h;NxaC}JTh#CYT*`iQ-i`3{7#k0XtiaLX-y)i?$0L}INhst&v z$A_u{e26!qxzyNvG^bSigQw1OWt2U?61DwLhq%Y!p2}D6ozjtSR6Lf}`oC)(Gjze6ne>hSjqF2CJ6LHw|VN ztpD`uzXA0=E*fUM!k{yKm4i3yhrhY@i(uZ6OKX>kee8BwyLwIJ+t@a8vy-YSJF#wi zlU(_FPJT@hyj!Mg z633oWc;2*#oq>T}=lkvdI7R;o#-H$_wHy`9H8-ofrn-e%9;gwxrbG0p)>&a}r{AIJ z3&2M*Qt%-ZBzgq$aI5fHvOl&dM4dzr6*m@BGHTYZ z{_Ar1VtoAnH_!Yp+>iguAt1(sz(6I8%lwsweW+}K)Qx&W(n`iw6>nx!dY{p7y|cE@ zMQyG%&uanF8ZY->5EEHE&;vj`b@XiLk}E#^C!^VgIM*YAPWUV_czkd@2XT-A(E|Q> zhTVqAPaOEeG;u@Y5Wj!%k>}w{UKo9Y2aZdRAJTw+e;Lq^*JR!f`t6D|({1!)`!^WG zn9AbrU3>yP{z$bVjn#pAZ9afWTdd6#-06rZ?_%-(xq%Hbj5#)w*_rB5lAXBcSoF-n z=*8Z^P?Z`?>MQmS*x#UBeH5V)J=q*ywXAv-ayL{2#;kFhKz~5~Tgc)6@Syx(ZjA1K z;o#MhIcEvtmxKF(PxeQiapL#I7Tt*zO8yZ-;}bs!JqR3B7(TYyLqq8?uK6B|9V^gUc<4P>Rq2I z&nqHHIwD`_QII*mb4^^zZMYxU!CANb9^czgX!QAmuKv{&`Bc4@RK+Jn3?3*!8MfkX zC3a1*mq`_CroQFIje*ZvtuA9lxvf9rdN_&gVG}flm?oB$0fTC&6pq=vz2+H-Nq#qr zTyin7Um0M%!SiYgJVnca2(lWWo?^LymI>*$>dEh%fB-_)!SF`ajIYa9-Q39B3sid1 zs@@(}W3;Kl2QH?#aH|+!Qesg1ERFnSff?*Y-F`^`UWL{asoY2Jt^cDRp1+ikY5Fz) zebSYyS7lm6F33`pu70?i(zB#?$l42EFPp^vWmv3lrV9aG1uefEJpL7gsYS4k}5~~ua)rw(AIk9C5Cf8ey`3GjT zeF)!{jRqHSA(7UCx^$gY#^>X{KKO^J&g?GfFoP))bI`mAz68B#;sR)hDXdo@%2d`{ zSB;ym$$^cXn^%d}NA(f;t=(bHSgb9bD01E^rSSnub(ez5*fRD`)nIJ8!jO84Z}4Le zVVtGosm5w<8?%%7ch!$dtg-N@L2BV-Oq#;dXa!K#%||dhU|2KTZOl^dxoS2q`;1%3 zT!FF8h8aLr!PC()^Ag!3UFup?#=uqHaT4n-Yk)UvAWH1JFZ@8mu>C*|Pc`urncmyH z3TjofUbfiTNtxNiW$TbVsR+cS#ah~)-!KJ)=rvfoByF}VL~772oj%ay8hhdIt zUuJo1dE(h)+a}z<$p=Sz)@ow(p*uq*@TLJ!P3dnvKgn4~(`1STTEF905?(rCoT=YYVB{WE9bP*_V&+qY zHPn=Pc5RoO6brwCVPQ3bvR4TiX$K$=0`+2p&E?aK>Cy4mU0FSs$MPjg=bpL7wnbRA8k8!}zj%Y-x&s!d=lPB!yk>F!A^ zybZfzbdx0R*&7WGKuszcHlb_Yw#xXrxWbR(46=d?Mty$EI@a}td77RAAfEY8_0lDI z%>u@RLC*%(qbL`;b6RLc3)ycK#JhTbLQB%1UE+Q}1Hqe29+zTXC6#=pxUye;z53v(5szK$!R~t_d6^9T zOr|7XWje-q<@dIQ_`=9rJS|{rq{(-IKsTWB)u#E>Hl=1{N5Aoa%d<>BYEXPbHM0L^ zHVc5wWV}noBGOZ}c8Umqe^l!%O_? zVey1moe*mrgoz(+1VI?XiF|E7Pc+A$Dj^>U8~&K;)F(=Byy1`IgIq5_^n()J6f8q#7h z@S8DxsCYr_oNpmJ$I`7+U≈@3pIw?Ae!Zr8 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%0AtPn0eI$wpS%5N^{ct>WWTvv=5rx#GF-um)J@wgs z4EiXc{>@66SenP>pxcO6$(Dw8OZ8RjuTgC~)xP5+XsniViL|VVxPE&bZcME;lj875 z*vR`!1KKk26CBPwbVm@u9{IfH*wp5!=QzDIOX(tZiF*{Z4YKnWG%&tUXLFy{zH(GL zyYZui>*V9j**SggFZVL<vpQR2b5quS3$Nr+jA(D=E@AJ=)3M$&Zs>1{9} ze0Iyle$$szcWphgIjwMQzfOr*}qCH)o=$&uvXT7|yME>xyPW$q7{THeQ?xtZuK zBmO=%R8)6#KqQaqx_oE}>|5g?`Hk0rF8u95uD#r>(KE#R_^4%7>82&zh5L{CTMSZo;b4)?QI#((#VM=+x+<7rm+9CLx>p zroIDeis5q`_7HChE^VK#AW(=68sE6N^*bDd$Xug2=Q_bGp@1wKv*^cH;4V#g-W_5u zB#^GcTyPCh8I2|*UUI&s*R!kQ1n|ILeH}6~j4AiV-JD zx+x*2%)Y-C{Svaw#T{Zu1jTXpkkrYj#e(#|ZH;&S2Y}|%`BvHWbw67vMZ`s;7ojJ> zbn`{t<8$3e1_kKq3B*dyyF|F9ZBIiyp0)dV{&b(F^`9R#wlEH4-e*OJT(=Gn;=N5v zD>I-i z0`>tm%>RP%@wLcXYpRR(zMi8zp`L7&2jFuN>QpX43V7XFR1A-ruXsu)=Y*T}pfqCV z2(#ljVjeI^EQ0{O`JHN{JC%<<3S9^MNz*_rizY<{} zVX7`dB3W5vr3Gl7?h*;Vm{0drEtr&^Ex8mVSGt|{!!Y7tV~=W`AuA{H$#YwPHP&5F zfq96BtMJzWDg!RL0DPEzv4^9HfLJc8JjgmKbt)Q=b$UZ`T}pMy#F$G0nzp?)h+?#W z7l&=%W%4=bCBz`N8)C}tEWY_NjL{!A1~ zg+n)s1vLk^d*}S*)2!d$gJ!=Zg^BeJ`E{ISioKGB;W>V`_~%CWMxs$Pr)cjyLwh@o zd68JYGIcD00I_~7^%u}t`a2h+uxz7}jNifqQ6YTngvsa`#@3MV-G6~+fDN#2P+2fs zdNeV!vbTB^a|tWVr#H&=YU8#vyG_H3F&49GX(+&~BcfLTWVG-3xUmNp(HY($9H$*P zWTwRYKo7!&zVxby6qd|oJ2JQZ>6#}?f+LwdI$L`7qR$a{?(AMP8nP6)%eqn+86#{m z)6g5fr!nm=D~6BdC{2qmRLx3b0)@PfdJu^XH%;gbL`UzSQZBB2RgjppOmidUABpC zweWkg(8@~r#VleG=X@brH#%_LL`6>6OEH<$V)Yn zWF8WUSR@EUf%0&mtd(BAu&F_9->a?S{!j#+7sr4@SxOLpOaE$zQGF^e^czqu?6A}D zUbHOgH@dfn1K>7%aM<>6-xLy*0$Mk0RQ43YHh*DfC_JY1`aB?8mi2g~e`%&UpkL1a2TdKIN9-92BR+j4Y^ zbB$&3CC!cYnO3wl5Uk!qBHRd1GycW26pN^i7X}!wxp+vv2NS%=q%hZ z#$je8veXJ7s(tVW$MUEy&IV$f#%P+u_ckYfHi*I3M5t zJpUW5tTtHg0Dua=r)~!R3g6SRnwp~!a>5O(eqdk~e`pe&h>Z+$hFbLEuQ#%L%2x#P zrK6qY&5Z%+qyR3-@((}K#;o2e9#~(?Hk1IONTrqg{_J5|`RU*OXnRMz?P2{agTq?z zU&Lre`-N#4mwk0E629%}D=ol1_7jG8i3Ne4=3R8x`+3fu{n}3ajkqbBMp#-oo5D~T z$BFX;OUvG=o4bj**su<~V1M`!WN~w&1M$iEs1;M23wF9Ag%lMv!!a8S^S{}ZU(mG1 zj4v3pl~NUzEQT`^i5T-QJWQl@e8%gt$g$_m6nFj9%sR;qK-f>{!Cd;?+WU(GWqJ;l zZQ&`gKLMQ}AB3h<^ZT57JF6}htW`Zb_91i0(-QzVee;X?&a9mE{+VAw=jlMa!!&gcy0fHoX%v}pP=Dn)C4PJ9%9ezNTL6SAJ5s*hSlJTL@ z9+`Ss7LK_H^})bQ^)7|-fldw<8mFrwW)O{G6&6-3WT3>m5S{$>XoU*{=_ZwHGIILA z1C%Jtvm9Vm=i{^8r>@-G(x4o5Bo5Jm+sL@BZSMB0@9v&yj?_+~EF?H+FN++;{Ejl- zHT&)V4K&E%b4sTqoLwyEkUFhlSAFq%&Mvae{vL*ITCw@E7NgvGVWVR*>eRZGaKMtu zKx2)5Kw1Cdf%B11HShmY5XvmiB{xdf2X^A?HTfrb5VV@G!Duf>75XWZm?#Gc1_%~1 zI7X&NURZ9~-@WE7JSg2gw33WO5G00KAWBy(Oa zguVZIM$Sqh8WOR=B;&Z<_VDYeufBW6*X8X#L|6*EAr_U$JPTF@n z4hfrRibigoMvf_0*MwAmpkFU=rb-xwiR3*ZZ#l&2w_C|UIRyNYzCQ7sV9zrRY7F-*yLe2+st#A! zvR#fXEY41!Ne^Gvw(NTyW6?R5Q;V=Zne#fHj#hN!u-*%DfHYGTo3IA`8hDn3ih{?% zgT&yE{2uIIa=*=8yK>wHe;!$dY(v#79E&`Dlzg@DI-7jR4>&$g-=NZg98|2yr*Ic{ zEwEn>|Ifw|1HH;6-?6_j^lzg_`<2|cqDDoupUIhM49EJ4;~f|932Px?Gn`|q=5FZo z1&BfFHhBl^p0E2@^N@SG`9|wz ze?CP2R`8XEB*Rh`oMgJ&J*1ZN!W_Vj&%n-)EA@6r4CKF03x$3ivL$s?AEiZe#4%S6 z;78M_(Nh8RMu;NC1(Ms(9-sn*X$9@@`puu7j7F+Y;G6Zkgt%LuTd>L26@D!Jzsg#4 zOk>>t0KB~CaZx5K)+_HZvSrtKO?x~;MMUbJm)>@~BQTAzuprPLQS{_|fpXleS`r19 zdbIY{9sUQn+y@GddYN^20E+k>co@DW5qjaXUG=8(r;WA?pRa%Pt9Brh69Eq4jh>v2 z=0&9q4cY6)`=z$Qoq>gdPk&dMdUL(~Zy;w)N5<=<=2~fTpfj#BQ);4JWc64d?iG^@h>T!C!ehMP9BxJT_IS z2!#Q`Tvy%s!Tj&B^ zM~`OTtCc?c_Vr?}9uuLDdyxtI%$mRBU%Zk&&zjB?w(vU07xBgOF8mh&R&l*gSKYXx z_wTLl!*zr(_d&K#fAA38QyMGlwvU7|UqFFdG>Z4th3nPzjm(m6xrmmmqJa~j+7pM7 zhWhi&tb1)EPBs3#;!2RcQDyW$fGzqWN_!dq@`Kh`!R_&;lvQSlqK7b-PS_$5HrC?2 zka<@a`>piiou+8j{K9WT51jR$wVK=Iip#l#Z#HEw*Q(S)K*5R23C&ivP=J7ibR ziuA$aIG56xi-^9xMJfL(6@tgNtJj`9eD1$uZ|R?q1Jtm|EXq@I7mJ0w@_JR*G)`{3 z41FQ@e`VklZ1Jtdk*!Mi=K-JdI3&2a@XEVjK5gTPxmsGe>g(Q31Tnj)7^8i>+M}HB z0Rl6#Pb0TV`h+lc=6XSHaHf{c9KM*JVeYg;xK)|3c2$;QY_T@v&HxJ_dk?fc3Xdx9 zZht6gun1VNDYtUu76^h_NgLcTOvwZIm_22wcKWoRoz#1V^%etjQPv2`3dn;#J(#DT zo&I>v?XwOQlpeT7LNG_V^SciZ98z=%Zj+R9a0#;L)u^hR)<`isah%t zTknS>C(cTu{|?m9;rS4sm_N)R7WyHNN8~^}OP$vn?}u$q`qB(LJp|Fu;q) z1A6G@9BIRH2?)5nPG@nI8H$Ruj{4sCudr$#ZulpGkme5TiPl_X#$)5}1fc93=#xDL zQH%)yS#?nRe}D&|9fMm2r)%-S?uruC;4(%mjQj$uhB)R1aW?_u;6~y zrR%fT_ttr~ZKyWQxr~B|6PZ6m%`8%Yesso%X4Z!}14+N}_z`XyHrX((vjLj(=|`ul zhqQ(ta6Rt|;7C4U5Y-0?8EV4lv`swj>PmKC`g7%2cu^ZY?AklVK0L=@m+?!xQvZDx zC^vCPpz?#N9^)u1w^0zlCD69*fScYGiY%nmhtn2doEIx#7!P~VWTe*Lgt2o9;vasU z$H!r3C#Ic*kO4$w9X`wXXDpB9TQe&yCDkZ33&-xxg8?O~G*Ruhl}FX;O=6a}Ebzy- zQ2iaYuJMO188IAm^k#lET2I-DFxFZG1iA2#g(A>BdNLWej{0cY?O*(r;5P!2uE&DWG_YQ6uIx7xvJwA$(k0j zF%!SfYu0)urPDu%BCV|@D;FdFI1^n)?#g90^IoO}{f@a4g4jyu zsMxR)`Ry6HxN&Kf+N0-r!M7$mM^;AWrO9{nJW)ILpij+&bKr{hW0w?r=O;oRhsoG% zz;&NfDt&9hJAFIE%s2kEtudtMma$NMaJD-fGw3ny#VDsOg&lK1W>=Cz|d;z;=a z%cc3h9N1s(16|XG_pheN7vcNJ<_IxwSwO^VJnq`)1(C0%f4PxPVLMQAlzGc;RazDF>94hmu+f$Ne zYl`&+@gx)lrN_K%p?!ETZ7m zvOSW)y_i8OMEjF=0QAqrp5>RgN+nh<5(e)aghXeqTe37>HCr!8wU4Eo?YEZ$yxoQztLt|`G2 zjm;qxt?hgd?T*9w5bC<`MIrr19+}@zf$N3c0UYap<4Rs=@vHKw=Slo+IW{h}DGx4) z`t;v0;6gl&MfZ*0*yGGS6ABhjClPV%KsHoHg_QG$W_lUd*NzH`BBL1)LW?16r0ve7 zdtWljF-gUmgfaVJ<)Ux|6U;F*d^WV=SF0g3t}Kb8hR4jG@JmT2@88DbuGH+zA;Ja1 zbeqKA=mFQUf@XQkA~_Tq<~dE~`Du8d{l4Vp~zVW*@6`+n8ui{osW?^^R+m?+84AJmpxBl&RG~4li`O|*&!cCM)iS+c{f(Xe(#}})=!H0 zWIdTYYmTJ37KZIHrkT8n*QqKqa`0M+0H|kylVddNw0X8BZ(yjcC@Kt;D&WpqDw@>- zHvjO&CRyilB(OU4g6j<$+8`pJ;Y&|VsHg>>&CZV=Lz-l05o8G>6c>@a#=WG8lRomR zIWtgQUQ+F&G1Y01Lg0oI&X`N7SZTy;Z#Yw;Crk{U3Lgo&s>xr=9i9HtHs$2oGtP;3 z(wmRtVLC4Uuz3wd>hQ#fHkX`g;`5%II~UOM#8z=b{UeVoW2WidtJ_N-}k!Djx#<^ z;!dNtPZQIsTZu2m12n}hOq^>2$Eju&BB2rUk^ci+m)!sThRDu2U(PZ7Ac?bP%q>b% zOLYC<@%vQ?YnKO3jkk$^nJ9#`xBU0~G=v)2&N%fxjveXK)_Hn*EIL=wY7zOQ(s_9N zDCE7;z(SBoaI3&9A@7R;sil|GaV>acR9s%xOaV$lEHs~$QJcCwVdkhb@Y&NBacUp%)oO2%^^)%- z3LN8^L_EjzlhQNa#sVs`E==sP38pUX!(FVzT`4jaM+zU!I%r6Yz`@?40+T^wW zWPS6Q!T6KQ)z6b=fSB-Rb|!Y^#cFmhbW0h0-)wOIGsn}mW`ggn>Govb89n%A#rhoS z;o|*l&Pj>)dK`X>TB2cH)4IZ~*fDvU_Bn4+3pd&aPo|Xl`lY+FKprh|Uz*@j3!bTf z>HJtBEdxCl*XT~D_Xj}T&jQmHhvRrR{zTd`p4Xi+S|p_!zd!fC#`f*#yrWhRC2u7d zor=6%eI>e(X+sg3>gN;EkEm7R?5o+*5)Z5TDyg9U$aL+D&#Akw={mdksEAa4=5WbUWNr*=S})mng*iqoZfjQM&I|@B1eS(}{F6b4TOObaWTa zmam<9Szel)_S>fdoqjWG=Cm^2KT(7kTTTO?WI8eNynqwI$dtq@&w?g%axs=~YD`}U zw7}RumdB#fw;Mtvx(|*&Ie4tYsD9E7g>82bqC;qK2%>}W;$4&1jpNobN;0eKO+&Vz zIDBSjSL*4zj3^%YKhGV6h*3o(WfDFd#AF(uWNze#cGrErXw{Z9b1W+I*`CO^2; z5M?4?+3mNYKmchyi8Jc5Dy=rb>s#qtg^blMU3#7OMP59sioaYx^^yy)sh`zz(tMLi z`TKJEN?@e&g!0PmtbD6>EW0x8iGxUq2ye;YQ(0RhL^$X;1xz#wj3aEU(C)YQ z94hGg)Dc3QJVTw!4E(M&2upWz;^ci{T~NWQ0?_XpR&J{5Kg*sG*tqbdM)levPWZSm z_sPd_!+sa^1@XF_hTVD=Fh-_yzL-nmbUQOYn)VAxeIrgd<12j+*gYTYMh-g^T}1p3 zz-sFna;-Qa={deY-#?KV$3*tg{9|6Eyq0Tnx!{;_m1@5#6FZX5iJ(cP{%sq79sFd% zJ4F2V$BG~h5j~>g;MEG5E&H*5`ESGh1=15f*f)?I^p$tZv(s^L$8i`MClo3!DiD1)I`TIoCV6I z_gT2&LM!hNat!N#EQMt5-{ll%@;PHad>-Ca*IL=hfwmBZ^Z>DBlvc3EKWAqrDc;_G zv_p16X_4S)eqr%Gf)?1mo2G|8 zPQo0gAUk!&O1`^4|9#S)Cc>9d`P~d45H$~i^65c}EdfMp&F|&PF!VS>xfa$YDly-! z$J)?s{lsf%VpJeoU;(X-lsDH-H{xI=0=F_Briur1gOEaQQZO_$^WRf@Exy3?LgfsH z*2nfA&0L#=O!C2J6C`!oTv#HR{DSDnE0YZUWE(Cic^0E^h#S;Hr%jTEtc(P|Dm{PW zXR{6NXW2*J+$O9|X<`dr$hI<}5(j1BflEgvPjvneM9&4dBwo7>0Z|IgXLnL=VWidqcp;YUI(ra1*bS znJFZY1mvkKr^0)Up^2COulXemO}A!3vI8+I3nEmmFV~H<4cR zP|KWmlK(Ewmnr`jVuz=f7{vjl4P{+`70%$E)5sjkKmt`G)n1811*At(SDwkTVa_&r z;NA7YOO>U;!9fb?R+0Jhq_BS*F|KMS6;7A`Iaqyh;oI$?veja{-gG!vXO0og0kN5D z&NHXs1Q}UX_eb13x}SQ_LK=0-?{!N`fwzR zbY2s-TM96(U{U#&wT^~JG=q$~a)y&T{+}z6fj7nM_nzJKNTDQ37U#Uo2S)^Gyf@R1 zv^F?na4k_Fmkx#&T?UR4Xj(z>y+fiyVtZ8?v1RNUg5Wp+Na!rsBYRP&Z8yFrOxU9H z>zxU5(HvLjGUO~NFfDo}C*tpzoyKdqQLalM5+@njv8f%q549bx{PK&X02its9`_#A zjezet6k&H$g{va1QkP^AnzYu|zs5|mtdXge- z4D`OvpO5OvR&TiJ(h&%WZEGL?GL2XWtF*E`rW)7;Lmv^Mz-`J%@ld;APJ zlqhLbm|3;K@N!LAamFu$XzWOSiI$WqUpFJ2Qb4whwgIH{G?vUln9QPaq&+N<`S18% z7hNNEQ!w)dN`+VKgw_746oyg@@NtjRXbqp2)Nc9v7FIYW@EF2fFU~#41zvVt2c&;cn*V+xNGEfNtx$V2Xh1l(ZQA~6{*%FP`xvV(q~;@9l^RvL*(Iv0 z@kHUTt9{0auC9{Wi)dxu?$DhJz7qEnD|n}j|J{Bqx|mH>sxv-(T8Ip_d8QX9^p*J! zfG#ZZ^sX=-UbfxNj@cIE61or6ryEzA4OgD5_En60UO>~`2$#UE&?U3}BGU=&O^;gF zY8n5tfkC^KjKNMj$!zb0W5mSTxv>%N3M8SIf%egI%mb8@kdVEk=#U<5Wvx}=jsuq? zpCHOn*yPL}6BD!KuLMZWfIxarqUNlnWdF_PMz$k4$k%A*TWX~%M_Z4Y!iU^4U7Kvk zu?4>^R;%A3ql9GD`NlCuoT$BGj;Le)1%Yn#&-Ji3vqHgFEp3SR&&op@rztgmJcx;y zl^@tWY3F-lrTYem7M26hISu5IwyD)~dsgl4TjxUq4At&mug3h$*^zdBnC|VUo@Va zPhW_wniSDVb;bKCs8|2>J4*fRXP4V?%zkVW6Moe-bLOF6ELWTW%bWT|n^DdWxT{u@ z_~FViiJxm7bIWz89jEP?`PW|_wix`qTr{MZqFU;uOZikG)6*pD_M!ewYCSsf3ZaH` z&3x@)d^~NV7@feK3jeBrwLd>IWXFq)@HcgI_yNq)d-zr3k4Wukmyv;^^E=-r8(P}J zkJw-y+@h7-;nM3<2pi-(0|x^Mgw8lNls2FoWOJQYp=}(i^VZYV8*pWmfJq;qV+3Ao zx_3DhiNkDP@tBkIp-yCFNP5Rfp7p;lY*!yoHpa)tNFM#~wEEOdkUYXmO-r`0EZ6TB z5Y_hy7+W(}t;ZwpodCfj*8?UUhnuDvuEJ`0eKV@Q7?)AC z{%}@~%?TImPsw>7CIp7+{^)J^;atK|D5vu~tOz6M`Bl zN#dEXK$s1m;Q7^jY+hif_0rv!BV)3dsaBNVyDCTtD^TW#&1EfI3xxZRwYZp>7oBui zZJrXI05j1@S)5ouoR%^6HpM9HBsrfS!vk6ImoTo^Deex1|&O)4$gc{ES6ZB7OSwQGZa+)&N5r zWRhK5_|f`ezy_O_lCp@ACa3$ouL=W843{RySyNOnjCpz(T!+d$!aP#qDLk?4^`n!? z7l1~UI%YMPVNSxefW=NK_q|0ikV7E@C#6xVGO95c*{AenXYqv8 zPlOk&xtLInsHkrF)RNxQQtF@7#@*TukrCaNT?|lwQ-%Q@N8~lP{X0d!4K`JWSAM1q zB2}~~X=fGMXUhE#beo*5FtHN*hV-;bO9imD?fW^cY9JmrwWIEfy1hDKI{*gLobG%`8_VTNy63UVd$Ok zM`|m)l>MWVvHNlXper7ff=okRGc7kQg~<0{TBJ_=hXWWxIjbt_0J9sPb?ft{9@J(t z)Kf%e?T0e)Vj$z4mdqTUmKAU)7e&&kL>*2*ZL+`L zF{?w$ZU`4dVKmo__h_}xq%QLSS42T{IIPxOSZH!;i^yuJ`L|v5o|mu9GY+IpMd6n5 zMsE-==ph8czhR=#6689)?X4_Ox~!6+UN5*f5`qb2{Bt*I)xydm^-Vo8cSg4qoJ}*# zz$hksmi_eoy*Pt^uO%NN(|VM2Xno#ibEJ37t>igaI-CNGf7JJiiGwOB36P1dbHT_-pRvew8WNB*e5-xD>gSSl;ilGSY5v=Q*jhmdEv~O`iD{U~q>`Y( z1O~WO|KP-x)WK#TcwQ0n1i?+l!kM+9N(22DMJy!}C1413o}UG*EX~onS>A#b5R|Aa zQL29&N`ugN#j7T)De}Z%TE<wM;_hv9?hLf6)%(@Fqqos)C8*eDk$-~;kWFr6E4wJ z8Zd?%fF7kD(r6w$Nc~@+Rg4B1l{rSPYT4 z#H<#RhA|40pUx}yZ7IuoO0Fo7nX$Q{`0`HK6wa0%C(bG!GHhdd-8{Nn3abqP^-0LJ zVemn{fx%g(l5@Du6^A9urxNyV-O7$Ph)_vwRvaYQSs!pw4ajBcITG|sMtx`7P9gKY z{oa*+kA`>nv@T`<*-r9g>`R=>^RRT--545;tgXlL!yaAYg@I{xK*3TFL4VZjNYbMI zF=8j9yLtQVr#vV{x`mjpMWcSz6M&D3WERp3Jd++0VdO&vUHzJ?H&zfr`Q+pI;Q5ok z5w}po4LnVwSIDkf&`o4I=RMR#+rnD=s-ly2+KC5Cj$WeW(zIM;nw-Nm!#&{MfIJsb zdh@}{#mb@aDdO8xU01z(>UaP-M{H4vR3IV;L`{jLSc>T>?!Glk0T;#YbIsArMl*zb z>Hr=J)kysZn5s=@5tnk)MA`!AZ_`Mu!O0p~d?cY3{?Kh@qWVK;e|9Bt>Q|*uQ_Uk*VU@*{nQ+j!&u<1 zFz*s(bx|k`DzvTtJ#AKSXBHh?YX7_~o3;nIop08!8mWHj3p=sU?H)4lEJX5~{`-W3 za4Po)3s1Z7ek8U+V#}a64rt5fB&xbKaz97-ekAVVHk~k7V=${dbro_qZS5Zx>ju%8 zvj+m$fhmy^e(G~sLOY$Uyj8WawKxeu7GWl#U4}5#h(HE!0lj9pO#)2F36LEp>8QLI zu=VRa%w7E2Dp$5-Lw4$ziI_y2YM8n@it=O7)w~gxdV__9;K-{iCe#}0mxm1ci0~
    KP5Zemz)YC8gn@4Cbu`gh}o08n1Kavcky? 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
    - - -Download music in true lossless FLAC from Tidal, Qobuz & Deezer — no account required. - -![Android](https://img.shields.io/badge/Android-7.0%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white) -![iOS](https://img.shields.io/badge/iOS-14.0%2B-000000?style=for-the-badge&logo=apple&logoColor=white) + + + + SpotiFLAC Mobile +
    @@ -24,6 +19,12 @@ Download music in true lossless FLAC from Tidal, Qobuz & Deezer — no account r

    +[![GitHub All Releases](https://img.shields.io/github/downloads/zarzet/SpotiFLAC-Mobile/total?style=for-the-badge&refresh=1)](https://github.com/zarzet/SpotiFLAC-Mobile/releases) +[![VirusTotal](https://img.shields.io/badge/VirusTotal-Safe-brightgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/0a2bd2a033551983fc9fcd83f82fd912c83914fd1094cd8d1c7c6a68eb23233f) +[![Crowdin](https://img.shields.io/badge/HELP%20TRANSLATE%20ON-CROWDIN-%2321252b?style=for-the-badge&logo=crowdin)](https://crowdin.com/project/spotiflac-mobile) +[![Telegram Channel](https://img.shields.io/badge/CHANNEL-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/spotiflac) +[![Telegram Community](https://img.shields.io/badge/COMMUNITY-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/spotiflac_chat) + ## Extensions Extensions allow the community to add new music sources and features without waiting for app updates. When a streaming service API changes or a new source becomes available, extensions can be updated independently. @@ -43,11 +44,6 @@ Want to create your own extension? Check out the [Extension Development Guide](h ### [SpotiFLAC (Desktop)](https://github.com/afkarxyz/SpotiFLAC) Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music for Windows, macOS & Linux -## Telegram - -[![Telegram Channel](https://img.shields.io/badge/CHANNEL-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/spotiflac) -[![Telegram Community](https://img.shields.io/badge/COMMUNITY-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/spotiflac_chat) - ## FAQ **Q: Why is my download failing with "Song not found"?** diff --git a/assets/images/banner-readme-dark.png b/assets/images/banner-readme-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6dfbdc83cbb483a416ed94fd89e1f65ec4cc7cf5 GIT binary patch literal 14813 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 literal 0 HcmV?d00001 diff --git a/assets/images/banner-readme-light.png b/assets/images/banner-readme-light.png new file mode 100644 index 0000000000000000000000000000000000000000..de5365f9b2fdf969fb498e86b5a4aced60079378 GIT binary patch literal 14871 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 Date: Wed, 11 Mar 2026 16:00:51 +0700 Subject: [PATCH 034/145] fix: make README banner more compact for better mobile visibility --- README.md | 9 ++++++--- assets/images/banner-readme-dark.png | Bin 14813 -> 34206 bytes assets/images/banner-readme-light.png | Bin 14871 -> 34628 bytes 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f23bf87c..c99cb71b 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,11 @@ - SpotiFLAC Mobile + SpotiFLAC Mobile -### [Download](https://github.com/zarzet/SpotiFLAC-Mobile/releases) - ## Screenshots

    @@ -19,12 +17,17 @@

    +
    + [![GitHub All Releases](https://img.shields.io/github/downloads/zarzet/SpotiFLAC-Mobile/total?style=for-the-badge&refresh=1)](https://github.com/zarzet/SpotiFLAC-Mobile/releases) [![VirusTotal](https://img.shields.io/badge/VirusTotal-Safe-brightgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/0a2bd2a033551983fc9fcd83f82fd912c83914fd1094cd8d1c7c6a68eb23233f) [![Crowdin](https://img.shields.io/badge/HELP%20TRANSLATE%20ON-CROWDIN-%2321252b?style=for-the-badge&logo=crowdin)](https://crowdin.com/project/spotiflac-mobile) + [![Telegram Channel](https://img.shields.io/badge/CHANNEL-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/spotiflac) [![Telegram Community](https://img.shields.io/badge/COMMUNITY-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/spotiflac_chat) +
    + ## Extensions Extensions allow the community to add new music sources and features without waiting for app updates. When a streaming service API changes or a new source becomes available, extensions can be updated independently. diff --git a/assets/images/banner-readme-dark.png b/assets/images/banner-readme-dark.png index 6dfbdc83cbb483a416ed94fd89e1f65ec4cc7cf5..13c40d0b990e40f9abca1e34c7ecedf96f84d3be 100644 GIT binary patch literal 34206 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 HSx5d4dZNg} literal 14813 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 diff --git a/assets/images/banner-readme-light.png b/assets/images/banner-readme-light.png index de5365f9b2fdf969fb498e86b5a4aced60079378..2aaaba69c36182f7fc062b5169ee7402908e9225 100644 GIT binary patch literal 34628 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%0Dj^Ut)OUkPn% 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 z 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 Ku|mP()&B>W7`poa literal 14871 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 hrtDn)$JuD8t|{9o&g!*^Qof Date: Wed, 11 Mar 2026 17:16:39 +0700 Subject: [PATCH 035/145] docs: add centered Trendshift badge below README banner --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c99cb71b..b52224b2 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ SpotiFLAC Mobile +

    + + zarzet%2FSpotiFLAC-Mobile | Trendshift + +

    + ## Screenshots @@ -40,7 +46,7 @@ Extensions allow the community to add new music sources and features without wai 5. Set provider priority in **Settings > Extensions > Provider Priority** ### Developing Extensions -Want to create your own extension? Check out the [Extension Development Guide](https://zarz.moe/docs) for complete documentation. +Want to create your own extension? Check out the [Extension Development Guide](https://zarzet.github.io/SpotiFLAC-Mobile/docs) for complete documentation. ## Other project From df77ae39865c24c021ce41261298e12c01af4b41 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Mar 2026 17:16:40 +0700 Subject: [PATCH 036/145] fix(ios): remove stale built-in Spotify bridge handlers --- ios/Runner/AppDelegate.swift | 43 ------------------------------------ 1 file changed, 43 deletions(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index b85020da..586ffbf6 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -160,38 +160,6 @@ import Gobackend // Import Go framework if let error = error { throw error } return response - case "getSpotifyMetadata": - let args = call.arguments as! [String: Any] - let url = args["url"] as! String - let response = GobackendGetSpotifyMetadata(url, &error) - if let error = error { throw error } - return response - - case "searchSpotify": - let args = call.arguments as! [String: Any] - let query = args["query"] as! String - let limit = args["limit"] as? Int ?? 10 - let response = GobackendSearchSpotify(query, Int(limit), &error) - if let error = error { throw error } - return response - - case "searchSpotifyAll": - let args = call.arguments as! [String: Any] - let query = args["query"] as! String - let trackLimit = args["track_limit"] as? Int ?? 15 - let artistLimit = args["artist_limit"] as? Int ?? 3 - let response = GobackendSearchSpotifyAll(query, Int(trackLimit), Int(artistLimit), &error) - if let error = error { throw error } - return response - - case "getSpotifyRelatedArtists": - let args = call.arguments as! [String: Any] - let artistId = args["artist_id"] as! String - let limit = args["limit"] as? Int ?? 12 - let response = GobackendGetSpotifyRelatedArtists(artistId, Int(limit), &error) - if let error = error { throw error } - return response - case "checkAvailability": let args = call.arguments as! [String: Any] let spotifyId = args["spotify_id"] as! String @@ -510,17 +478,6 @@ import Gobackend // Import Go framework GobackendClearTrackCache() return nil - case "setSpotifyCredentials": - let args = call.arguments as! [String: Any] - let clientId = args["client_id"] as! String - let clientSecret = args["client_secret"] as! String - GobackendSetSpotifyAPICredentials(clientId, clientSecret) - return nil - - case "hasSpotifyCredentials": - let hasCredentials = GobackendCheckSpotifyCredentials() - return hasCredentials - // Log methods case "getLogs": let response = GobackendGetLogs() From fffce6039ade143412708533e53096a58bfc3d02 Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 12 Mar 2026 02:51:37 +0700 Subject: [PATCH 037/145] feat: add Deezer entry in provider priority UI and improve release changelog - Add 'deezer' case with icon to _ProviderItem in provider_priority_page.dart - Fix release.yml: deterministic previous-tag lookup for Full Changelog link - Strip version header line and author attribution from Telegram changelog - cliff.toml: hide repo owner username from commit attribution - cliff.toml: remove PR number stripping preprocessor --- .github/workflows/release.yml | 16 ++++++++++++++-- cliff.toml | 4 +--- lib/screens/settings/provider_priority_page.dart | 6 ++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1129349f..57772b30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -344,9 +344,18 @@ jobs: VERSION=${{ needs.get-version.outputs.version }} REPO_OWNER="${{ github.repository_owner }}" REPO_NAME="${{ github.event.repository.name }}" + CURRENT_REF=$(git rev-list -n 1 "$VERSION" 2>/dev/null || git rev-parse HEAD) + PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${CURRENT_REF}^" 2>/dev/null || true) - # Start with git-cliff changelog - cp /tmp/changelog.txt /tmp/release_body.txt + # Start with git-cliff changelog, but replace its compare footer with a + # deterministic previous-tag lookup from git. + sed '/^## [0-9][0-9.[:alpha:]-]*$/d; /^\*\*Full Changelog\*\*/d' /tmp/changelog.txt > /tmp/release_body.txt + + if [ -n "$PREVIOUS_TAG" ]; then + printf '\n**Full Changelog**: [%s...%s](https://github.com/%s/%s/compare/%s...%s)\n' \ + "$PREVIOUS_TAG" "$VERSION" "$REPO_OWNER" "$REPO_NAME" "$PREVIOUS_TAG" "$VERSION" \ + >> /tmp/release_body.txt + fi # Append download section cat >> /tmp/release_body.txt << FOOTER @@ -424,7 +433,10 @@ jobs: else # Convert Markdown to Telegram HTML CHANGELOG=$(cat /tmp/cliff_tg.txt | \ + sed '/^## [0-9][0-9.[:alpha:]-]*$/d' | \ sed '/^\*\*Full Changelog\*\*/d' | \ + sed 's/ by \[@[^]]*\](https:\/\/github\.com\/[^)]*)//g' | \ + sed 's/ by @[A-Za-z0-9_-]\+//g' | \ sed 's/\[#\([0-9]*\)\]([^)]*)/#\1/g' | \ sed 's/\[@\([^]]*\)\]([^)]*)/@\1/g' | \ sed 's/&/\&/g' | \ diff --git a/cliff.toml b/cliff.toml index 0509818b..b5680d4b 100644 --- a/cliff.toml +++ b/cliff.toml @@ -22,7 +22,7 @@ body = """ {% if commit.github.pr_number %} \ ([#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}))\ {% endif %}\ - {%- if commit.github.username %} by [@{{ commit.github.username }}](https://github.com/{{ commit.github.username }}){%- endif %} + {%- if commit.github.username and commit.github.username != "zarzet" %} by [@{{ commit.github.username }}](https://github.com/{{ commit.github.username }}){%- endif %} {%- endfor %} {% endfor %} @@ -58,8 +58,6 @@ split_commits = false # Regex for preprocessing the commit messages commit_preprocessors = [ - # Remove PR number from message (we add it back via GitHub integration) - { pattern = '\(#(\d+)\)', replace = '' }, # Strip conventional commit prefix for cleaner messages # (group header already shows the type) ] diff --git a/lib/screens/settings/provider_priority_page.dart b/lib/screens/settings/provider_priority_page.dart index e28ad7e3..02c15b1a 100644 --- a/lib/screens/settings/provider_priority_page.dart +++ b/lib/screens/settings/provider_priority_page.dart @@ -334,6 +334,12 @@ class _ProviderItem extends StatelessWidget { ); case 'qobuz': return _ProviderInfo(name: 'Qobuz', icon: Icons.album, isBuiltIn: true); + case 'deezer': + return _ProviderInfo( + name: 'Deezer', + icon: Icons.graphic_eq, + isBuiltIn: true, + ); case 'youtube': return _ProviderInfo( name: 'YouTube', From f1eef476005d13a75268723dfa68a11cae1847f5 Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 12 Mar 2026 03:36:48 +0700 Subject: [PATCH 038/145] refactor: optimize SAF metadata reading, CUE sibling resolution, and startup initialization - Add fast-path SAF metadata reading via /proc/self/fd with displayNameHint support, falling back to temp copy - Replace repeated findFile() CUE audio sibling lookups with cached case-insensitive directory listing - Cache parsed CUE sheets to avoid redundant parsing during library scans - Optimize incremental scan CUE modTime lookup from O(N*M) to O(N+M) - Defer local library provider loading until localLibraryEnabled setting is true - Replace O(n) track+artist history lookup with O(1) map-based lookup - Delay startup maintenance tasks by 2s to reduce launch-time contention --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 279 +++++++++++------- go_backend/audio_metadata.go | 13 +- go_backend/cue_parser.go | 34 ++- go_backend/exports.go | 4 + go_backend/library_scan.go | 169 +++++++---- lib/main.dart | 40 ++- lib/providers/download_queue_provider.dart | 96 +++--- 7 files changed, 429 insertions(+), 206 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 5d5650da..50e6eda0 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -703,6 +703,80 @@ class MainActivity: FlutterFragmentActivity() { } } + private fun buildUriDisplayName( + uri: Uri, + displayNameHint: String? = null, + fallbackExt: String? = null, + ): String { + val explicitName = displayNameHint?.trim().orEmpty() + if (explicitName.isNotEmpty()) return explicitName + + val docName = try { DocumentFile.fromSingleUri(this, uri)?.name } catch (_: Exception) { null } + val uriName = uri.lastPathSegment + val resolvedName = (docName ?: uriName ?: "").trim() + if (resolvedName.isNotEmpty()) return resolvedName + + val ext = when { + fallbackExt.isNullOrBlank().not() -> fallbackExt + isMediaStoreUri(uri) -> resolveMediaStoreExt(uri, fallbackExt) + else -> "" + } + return if (ext.isNullOrBlank()) "audio" else "audio$ext" + } + + private fun readAudioMetadataFromUri( + uri: Uri, + displayNameHint: String? = null, + fallbackExt: String? = null, + ): JSONObject? { + val displayName = buildUriDisplayName(uri, displayNameHint, fallbackExt) + + try { + contentResolver.openFileDescriptor(uri, "r")?.use { pfd -> + val directPath = "/proc/self/fd/${pfd.fd}" + val metadataJson = Gobackend.readAudioMetadataWithHintJSON(directPath, displayName) + if (metadataJson.isNotBlank()) { + val obj = JSONObject(metadataJson) + if (!obj.has("error")) { + return obj + } + } + } + } catch (e: Exception) { + android.util.Log.d( + "SpotiFLAC", + "Direct SAF metadata read fallback for $uri: ${e.message}", + ) + } + + val tempPath = try { + copyUriToTemp(uri, fallbackExt) + } catch (e: Exception) { + android.util.Log.w( + "SpotiFLAC", + "SAF metadata fallback copy failed for $uri: ${e.message}", + ) + null + } ?: return null + + try { + val metadataJson = Gobackend.readAudioMetadataWithHintJSON(tempPath, displayName) + if (metadataJson.isBlank()) return null + val obj = JSONObject(metadataJson) + return if (obj.has("error")) null else obj + } catch (e: Exception) { + android.util.Log.w( + "SpotiFLAC", + "SAF metadata temp read failed for $uri: ${e.message}", + ) + return null + } finally { + try { + File(tempPath).delete() + } catch (_: Exception) {} + } + } + private fun writeUriFromPath(uri: Uri, srcPath: String): Boolean { val srcFile = File(srcPath) if (!srcFile.exists()) return false @@ -873,6 +947,66 @@ class MainActivity: FlutterFragmentActivity() { return null } + private val cueSiblingAudioExtensions = listOf( + ".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a" + ) + + private fun getSafChildFileLookup( + dir: DocumentFile, + cache: MutableMap>, + ): Map { + val dirKey = dir.uri.toString() + return cache.getOrPut(dirKey) { + try { + buildMap { + for (child in dir.listFiles()) { + if (!child.isFile) continue + val childName = child.name?.trim().orEmpty() + if (childName.isBlank()) continue + put(childName.lowercase(Locale.ROOT), child) + } + } + } catch (e: Exception) { + android.util.Log.w( + "SpotiFLAC", + "Failed to build SAF child lookup for $dirKey: ${e.message}", + ) + emptyMap() + } + } + } + + private fun resolveCueAudioSibling( + parentDir: DocumentFile, + cueName: String, + audioFileName: String?, + childLookupCache: MutableMap>, + ): DocumentFile? { + val childLookup = getSafChildFileLookup(parentDir, childLookupCache) + + val directMatch = audioFileName + ?.trim() + ?.takeIf { it.isNotEmpty() } + ?.substringAfterLast("/") + ?.substringAfterLast("\\") + ?.lowercase(Locale.ROOT) + ?.let(childLookup::get) + if (directMatch != null) { + return directMatch + } + + val cueBaseName = cueName.substringBeforeLast('.').trim() + if (cueBaseName.isBlank()) { + return null + } + + val cueBaseKey = cueBaseName.lowercase(Locale.ROOT) + for (ext in cueSiblingAudioExtensions) { + childLookup["$cueBaseKey$ext"]?.let { return it } + } + return null + } + private fun scanSafTree(treeUriStr: String): String { if (treeUriStr.isBlank()) return "[]" @@ -891,6 +1025,7 @@ class MainActivity: FlutterFragmentActivity() { // CUE files: (cueDoc, parentDir) — we need the parent to find sibling audio val cueFiles = mutableListOf>() val visitedDirUris = mutableSetOf() + val safChildLookupCache = mutableMapOf>() var traversalErrors = 0 val queue: ArrayDeque> = ArrayDeque() @@ -1000,23 +1135,12 @@ class MainActivity: FlutterFragmentActivity() { val audioFileName = extractCueAudioFileName(tempCuePath) // Find the referenced audio file as a sibling in the same SAF directory - var audioDoc: DocumentFile? = null - if (!audioFileName.isNullOrBlank()) { - audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null } - } - - // Fallback: try common audio extensions with the CUE base name - if (audioDoc == null) { - val cueBaseName = cueName.substringBeforeLast('.') - val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a") - for (ext in commonExts) { - audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null } - if (audioDoc != null) break - // Try uppercase - audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null } - if (audioDoc != null) break - } - } + val audioDoc = resolveCueAudioSibling( + parentDir = parentDir, + cueName = cueName, + audioFileName = audioFileName, + childLookupCache = safChildLookupCache, + ) if (audioDoc == null) { android.util.Log.w("SpotiFLAC", "SAF scan: no audio file found for CUE $cueName") @@ -1111,35 +1235,17 @@ class MainActivity: FlutterFragmentActivity() { val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT) val fallbackExt = if (ext.isNotBlank()) ".${ext}" else null - val tempPath = try { - copyUriToTemp(doc.uri, fallbackExt) - } catch (e: Exception) { - android.util.Log.w( - "SpotiFLAC", - "SAF scan: failed to copy ${doc.uri}: ${e.message}", - ) - null - } - if (tempPath == null) { + val metadataObj = readAudioMetadataFromUri(doc.uri, name, fallbackExt) + if (metadataObj == null) { errors++ } else { try { - val metadataJson = Gobackend.readAudioMetadataJSON(tempPath) - if (metadataJson.isNotBlank()) { - val obj = JSONObject(metadataJson) - val lastModified = try { doc.lastModified() } catch (_: Exception) { 0L } - obj.put("filePath", doc.uri.toString()) - obj.put("fileModTime", lastModified) - results.put(obj) - } else { - errors++ - } + val lastModified = try { doc.lastModified() } catch (_: Exception) { 0L } + metadataObj.put("filePath", doc.uri.toString()) + metadataObj.put("fileModTime", lastModified) + results.put(metadataObj) } catch (_: Exception) { errors++ - } finally { - try { - File(tempPath).delete() - } catch (_: Exception) {} } } @@ -1214,6 +1320,7 @@ class MainActivity: FlutterFragmentActivity() { val unchangedCueFiles = mutableListOf>() val currentUris = mutableSetOf() val visitedDirUris = mutableSetOf() + val safChildLookupCache = mutableMapOf>() var traversalErrors = 0 // Build a map of CUE base URIs -> existing virtual track URIs from the database. @@ -1398,22 +1505,12 @@ class MainActivity: FlutterFragmentActivity() { val audioFileName = extractCueAudioFileName(tempCuePath) // Find the referenced audio file as a sibling in the same SAF directory - var audioDoc: DocumentFile? = null - if (!audioFileName.isNullOrBlank()) { - audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null } - } - - // Fallback: try common audio extensions with the CUE base name - if (audioDoc == null) { - val cueBaseName = cueName.substringBeforeLast('.') - val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a") - for (ext in commonExts) { - audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null } - if (audioDoc != null) break - audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null } - if (audioDoc != null) break - } - } + val audioDoc = resolveCueAudioSibling( + parentDir = parentDir, + cueName = cueName, + audioFileName = audioFileName, + childLookupCache = safChildLookupCache, + ) if (audioDoc == null) { android.util.Log.w("SpotiFLAC", "SAF incremental scan: no audio file found for CUE $cueName") @@ -1501,24 +1598,13 @@ class MainActivity: FlutterFragmentActivity() { tempCue = copyUriToTemp(cueDoc.uri, ".cue") if (tempCue != null) { val audioFileName = extractCueAudioFileName(tempCue) - var audioDoc: DocumentFile? = null - if (!audioFileName.isNullOrBlank()) { - audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null } - } - // Fallback: try common extensions with CUE base name - if (audioDoc == null) { - val cueName = try { cueDoc.name ?: "" } catch (_: Exception) { "" } - val cueBaseName = cueName.substringBeforeLast('.') - if (cueBaseName.isNotBlank()) { - val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a") - for (ext in commonExts) { - audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null } - if (audioDoc != null) break - audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null } - if (audioDoc != null) break - } - } - } + val cueName = try { cueDoc.name ?: "" } catch (_: Exception) { "" } + val audioDoc = resolveCueAudioSibling( + parentDir = parentDir, + cueName = cueName, + audioFileName = audioFileName, + childLookupCache = safChildLookupCache, + ) if (audioDoc != null) { cueReferencedAudioUris.add(audioDoc.uri.toString()) } @@ -1566,36 +1652,18 @@ class MainActivity: FlutterFragmentActivity() { val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT) val fallbackExt = if (ext.isNotBlank()) ".${ext}" else null - val tempPath = try { - copyUriToTemp(doc.uri, fallbackExt) - } catch (e: Exception) { - android.util.Log.w( - "SpotiFLAC", - "SAF incremental scan: failed to copy ${doc.uri}: ${e.message}", - ) - null - } - if (tempPath == null) { + val metadataObj = readAudioMetadataFromUri(doc.uri, name, fallbackExt) + if (metadataObj == null) { errors++ } else { try { - val metadataJson = Gobackend.readAudioMetadataJSON(tempPath) - if (metadataJson.isNotBlank()) { - val obj = JSONObject(metadataJson) - val safeLastModified = try { doc.lastModified() } catch (_: Exception) { lastModified } - obj.put("filePath", doc.uri.toString()) - obj.put("fileModTime", safeLastModified) - obj.put("lastModified", safeLastModified) - results.put(obj) - } else { - errors++ - } + val safeLastModified = try { doc.lastModified() } catch (_: Exception) { lastModified } + metadataObj.put("filePath", doc.uri.toString()) + metadataObj.put("fileModTime", safeLastModified) + metadataObj.put("lastModified", safeLastModified) + results.put(metadataObj) } catch (_: Exception) { errors++ - } finally { - try { - File(tempPath).delete() - } catch (_: Exception) {} } } @@ -3116,13 +3184,10 @@ class MainActivity: FlutterFragmentActivity() { try { if (filePath.startsWith("content://")) { val uri = Uri.parse(filePath) - val tempPath = copyUriToTemp(uri) - ?: return@withContext """{"error":"Failed to copy SAF file to temp"}""" - try { - Gobackend.readAudioMetadataJSON(tempPath) - } finally { - try { File(tempPath).delete() } catch (_: Exception) {} - } + val metadata = readAudioMetadataFromUri(uri) + ?: return@withContext """{"error":"Failed to read SAF audio metadata"}""" + metadata.put("filePath", filePath) + metadata.toString() } else { Gobackend.readAudioMetadataJSON(filePath) } diff --git a/go_backend/audio_metadata.go b/go_backend/audio_metadata.go index faf4ccf2..e1e15925 100644 --- a/go_backend/audio_metadata.go +++ b/go_backend/audio_metadata.go @@ -1566,7 +1566,14 @@ func base64StdDecode(dst, src []byte) (int, error) { } func extractAnyCoverArt(filePath string) ([]byte, string, error) { + return extractAnyCoverArtWithHint(filePath, "") +} + +func extractAnyCoverArtWithHint(filePath, displayNameHint string) ([]byte, string, error) { ext := strings.ToLower(filepath.Ext(filePath)) + if ext == "" { + ext = strings.ToLower(filepath.Ext(displayNameHint)) + } switch ext { case ".flac": @@ -1595,6 +1602,10 @@ func extractAnyCoverArt(filePath string) ([]byte, string, error) { } func SaveCoverToCache(filePath, cacheDir string) (string, error) { + return SaveCoverToCacheWithHint(filePath, "", cacheDir) +} + +func SaveCoverToCacheWithHint(filePath, displayNameHint, cacheDir string) (string, error) { cacheKey := filePath if stat, err := os.Stat(filePath); err == nil { cacheKey = fmt.Sprintf("%s|%d|%d", filePath, stat.Size(), stat.ModTime().UnixNano()) @@ -1611,7 +1622,7 @@ func SaveCoverToCache(filePath, cacheDir string) (string, error) { return pngPath, nil } - imageData, mimeType, err := extractAnyCoverArt(filePath) + imageData, mimeType, err := extractAnyCoverArtWithHint(filePath, displayNameHint) if err != nil { return "", err } diff --git a/go_backend/cue_parser.go b/go_backend/cue_parser.go index a938be2a..914d3f4d 100644 --- a/go_backend/cue_parser.go +++ b/go_backend/cue_parser.go @@ -430,7 +430,15 @@ func ParseCueFileJSON(cuePath string, audioDir string) (string, error) { // entries, one per track. This is used by the library scanner to populate the // library with individual track entries from a single CUE+FLAC album. func ScanCueFileForLibrary(cuePath string, scanTime string) ([]LibraryScanResult, error) { - return scanCueFileForLibraryInternal(cuePath, "", "", 0, scanTime) + sheet, err := ParseCueFile(cuePath) + if err != nil { + return nil, err + } + audioPath, err := resolveCueAudioPathForLibrary(cuePath, sheet, "") + if err != nil { + return nil, err + } + return scanCueSheetForLibrary(cuePath, sheet, audioPath, "", 0, scanTime) } // ScanCueFileForLibraryExt is like ScanCueFileForLibrary but with extra parameters @@ -441,23 +449,35 @@ func ScanCueFileForLibrary(cuePath string, scanTime string) ([]LibraryScanResult // - fileModTime: if > 0, used as the FileModTime for all results instead of // stat-ing the cuePath on disk (useful when the real file lives behind SAF) func ScanCueFileForLibraryExt(cuePath, audioDir, virtualPathPrefix string, fileModTime int64, scanTime string) ([]LibraryScanResult, error) { - return scanCueFileForLibraryInternal(cuePath, audioDir, virtualPathPrefix, fileModTime, scanTime) -} - -func scanCueFileForLibraryInternal(cuePath, audioDir, virtualPathPrefix string, fileModTime int64, scanTime string) ([]LibraryScanResult, error) { sheet, err := ParseCueFile(cuePath) if err != nil { return nil, err } + audioPath, err := resolveCueAudioPathForLibrary(cuePath, sheet, audioDir) + if err != nil { + return nil, err + } + return scanCueSheetForLibrary(cuePath, sheet, audioPath, virtualPathPrefix, fileModTime, scanTime) +} - // Resolve audio file — optionally in an overridden directory +func resolveCueAudioPathForLibrary(cuePath string, sheet *CueSheet, audioDir string) (string, error) { + if sheet == nil { + return "", fmt.Errorf("cue sheet is nil for %s", cuePath) + } resolveBase := cuePath if audioDir != "" { resolveBase = filepath.Join(audioDir, filepath.Base(cuePath)) } audioPath := ResolveCueAudioPath(resolveBase, sheet.FileName) if audioPath == "" { - return nil, fmt.Errorf("audio file not found for cue: %s (referenced: %s)", cuePath, sheet.FileName) + return "", fmt.Errorf("audio file not found for cue: %s (referenced: %s)", cuePath, sheet.FileName) + } + return audioPath, nil +} + +func scanCueSheetForLibrary(cuePath string, sheet *CueSheet, audioPath, virtualPathPrefix string, fileModTime int64, scanTime string) ([]LibraryScanResult, error) { + if sheet == nil { + return nil, fmt.Errorf("cue sheet is nil for %s", cuePath) } // Try to get quality info from the audio file diff --git a/go_backend/exports.go b/go_backend/exports.go index 00791180..ec757a2d 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -3098,3 +3098,7 @@ func CancelLibraryScanJSON() { func ReadAudioMetadataJSON(filePath string) (string, error) { return ReadAudioMetadata(filePath) } + +func ReadAudioMetadataWithHintJSON(filePath, displayName string) (string, error) { + return ReadAudioMetadataWithDisplayName(filePath, displayName) +} diff --git a/go_backend/library_scan.go b/go_backend/library_scan.go index 34d5c718..cb831364 100644 --- a/go_backend/library_scan.go +++ b/go_backend/library_scan.go @@ -71,6 +71,11 @@ type libraryAudioFileInfo struct { modTime int64 } +type scannedCueFileInfo struct { + sheet *CueSheet + audioPath string +} + func collectLibraryAudioFiles(folderPath string, cancelCh <-chan struct{}) ([]libraryAudioFileInfo, error) { var files []libraryAudioFileInfo @@ -144,12 +149,7 @@ func ScanLibraryFolder(folderPath string) (string, error) { return "[]", err } - audioFiles := make([]string, 0, len(audioFileInfos)) - for _, fileInfo := range audioFileInfos { - audioFiles = append(audioFiles, fileInfo.path) - } - - totalFiles := len(audioFiles) + totalFiles := len(audioFileInfos) libraryScanProgressMu.Lock() libraryScanProgress.TotalFiles = totalFiles libraryScanProgressMu.Unlock() @@ -169,22 +169,29 @@ func ScanLibraryFolder(folderPath string) (string, error) { // Track audio files referenced by .cue sheets to avoid duplicates cueReferencedAudioFiles := make(map[string]bool) + parsedCueFiles := make(map[string]scannedCueFileInfo) // First pass: scan .cue files to collect referenced audio paths - for _, filePath := range audioFiles { + for _, fileInfo := range audioFileInfos { + filePath := fileInfo.path ext := strings.ToLower(filepath.Ext(filePath)) if ext == ".cue" { sheet, err := ParseCueFile(filePath) if err == nil && sheet.FileName != "" { audioPath := ResolveCueAudioPath(filePath, sheet.FileName) if audioPath != "" { + parsedCueFiles[filePath] = scannedCueFileInfo{ + sheet: sheet, + audioPath: audioPath, + } cueReferencedAudioFiles[audioPath] = true } } } } - for i, filePath := range audioFiles { + for i, fileInfo := range audioFileInfos { + filePath := fileInfo.path select { case <-cancelCh: return "[]", fmt.Errorf("scan cancelled") @@ -201,7 +208,20 @@ func ScanLibraryFolder(folderPath string) (string, error) { // Handle .cue files: produce multiple track results if ext == ".cue" { - cueResults, err := ScanCueFileForLibrary(filePath, scanTime) + var cueResults []LibraryScanResult + cueInfo, ok := parsedCueFiles[filePath] + if ok { + cueResults, err = scanCueSheetForLibrary( + filePath, + cueInfo.sheet, + cueInfo.audioPath, + "", + fileInfo.modTime, + scanTime, + ) + } else { + cueResults, err = ScanCueFileForLibrary(filePath, scanTime) + } if err != nil { errorCount++ GoLog("[LibraryScan] Error scanning cue %s: %v\n", filePath, err) @@ -219,7 +239,7 @@ func ScanLibraryFolder(folderPath string) (string, error) { continue } - result, err := scanAudioFile(filePath, scanTime) + result, err := scanAudioFileWithKnownModTime(filePath, scanTime, fileInfo.modTime) if err != nil { errorCount++ GoLog("[LibraryScan] Error scanning %s: %v\n", filePath, err) @@ -245,7 +265,15 @@ func ScanLibraryFolder(folderPath string) (string, error) { } func scanAudioFile(filePath, scanTime string) (*LibraryScanResult, error) { - ext := strings.ToLower(filepath.Ext(filePath)) + return scanAudioFileWithKnownModTimeAndDisplayName(filePath, "", scanTime, 0) +} + +func scanAudioFileWithKnownModTime(filePath, scanTime string, knownModTime int64) (*LibraryScanResult, error) { + return scanAudioFileWithKnownModTimeAndDisplayName(filePath, "", scanTime, knownModTime) +} + +func scanAudioFileWithKnownModTimeAndDisplayName(filePath, displayNameHint, scanTime string, knownModTime int64) (*LibraryScanResult, error) { + ext := resolveLibraryAudioExt(filePath, displayNameHint) result := &LibraryScanResult{ ID: generateLibraryID(filePath), @@ -254,7 +282,9 @@ func scanAudioFile(filePath, scanTime string) (*LibraryScanResult, error) { Format: strings.TrimPrefix(ext, "."), } - if info, err := os.Stat(filePath); err == nil { + if knownModTime > 0 { + result.FileModTime = knownModTime + } else if info, err := os.Stat(filePath); err == nil { result.FileModTime = info.ModTime().UnixMilli() } @@ -262,7 +292,7 @@ func scanAudioFile(filePath, scanTime string) (*LibraryScanResult, error) { coverCacheDir := libraryCoverCacheDir libraryCoverCacheMu.RUnlock() if coverCacheDir != "" && ext != ".m4a" { - coverPath, err := SaveCoverToCache(filePath, coverCacheDir) + coverPath, err := SaveCoverToCacheWithHint(filePath, displayNameHint, coverCacheDir) if err == nil && coverPath != "" { result.CoverPath = coverPath } @@ -276,15 +306,31 @@ func scanAudioFile(filePath, scanTime string) (*LibraryScanResult, error) { case ".mp3": return scanMP3File(filePath, result) case ".opus", ".ogg": - return scanOggFile(filePath, result) + return scanOggFile(filePath, result, displayNameHint) default: - return scanFromFilename(filePath, result) + return scanFromFilename(filePath, displayNameHint, result) } } -func applyDefaultLibraryMetadata(filePath string, result *LibraryScanResult) { +func resolveLibraryAudioExt(filePath, displayNameHint string) string { + ext := strings.ToLower(filepath.Ext(filePath)) + if ext != "" { + return ext + } + return strings.ToLower(filepath.Ext(displayNameHint)) +} + +func libraryDisplayNameOrPath(filePath, displayNameHint string) string { + if displayNameHint != "" { + return displayNameHint + } + return filePath +} + +func applyDefaultLibraryMetadata(filePath, displayNameHint string, result *LibraryScanResult) { + nameSource := libraryDisplayNameOrPath(filePath, displayNameHint) if result.TrackName == "" { - result.TrackName = strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) + result.TrackName = strings.TrimSuffix(filepath.Base(nameSource), filepath.Ext(nameSource)) } if result.ArtistName == "" { result.ArtistName = "Unknown Artist" @@ -297,7 +343,7 @@ func applyDefaultLibraryMetadata(filePath string, result *LibraryScanResult) { func scanFLACFile(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { metadata, err := ReadMetadata(filePath) if err != nil { - return scanFromFilename(filePath, result) + return scanFromFilename(filePath, "", result) } result.TrackName = metadata.Title @@ -319,7 +365,7 @@ func scanFLACFile(filePath string, result *LibraryScanResult) (*LibraryScanResul } } - applyDefaultLibraryMetadata(filePath, result) + applyDefaultLibraryMetadata(filePath, "", result) return result, nil } @@ -331,14 +377,14 @@ func scanM4AFile(filePath string, result *LibraryScanResult) (*LibraryScanResult result.SampleRate = quality.SampleRate } - return scanFromFilename(filePath, result) + return scanFromFilename(filePath, "", result) } func scanMP3File(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { metadata, err := ReadID3Tags(filePath) if err != nil { GoLog("[LibraryScan] ID3 read error for %s: %v\n", filePath, err) - return scanFromFilename(filePath, result) + return scanFromFilename(filePath, "", result) } result.TrackName = metadata.Title @@ -365,16 +411,16 @@ func scanMP3File(filePath string, result *LibraryScanResult) (*LibraryScanResult } } - applyDefaultLibraryMetadata(filePath, result) + applyDefaultLibraryMetadata(filePath, "", result) return result, nil } -func scanOggFile(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { +func scanOggFile(filePath string, result *LibraryScanResult, displayNameHint string) (*LibraryScanResult, error) { metadata, err := ReadOggVorbisComments(filePath) if err != nil { GoLog("[LibraryScan] Ogg/Opus read error for %s: %v\n", filePath, err) - return scanFromFilename(filePath, result) + return scanFromFilename(filePath, displayNameHint, result) } result.TrackName = metadata.Title @@ -397,13 +443,14 @@ func scanOggFile(filePath string, result *LibraryScanResult) (*LibraryScanResult } } - applyDefaultLibraryMetadata(filePath, result) + applyDefaultLibraryMetadata(filePath, displayNameHint, result) return result, nil } -func scanFromFilename(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { - filename := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) +func scanFromFilename(filePath, displayNameHint string, result *LibraryScanResult) (*LibraryScanResult, error) { + nameSource := libraryDisplayNameOrPath(filePath, displayNameHint) + filename := strings.TrimSuffix(filepath.Base(nameSource), filepath.Ext(nameSource)) parts := strings.SplitN(filename, " - ", 2) if len(parts) == 2 { @@ -426,7 +473,7 @@ func scanFromFilename(filePath string, result *LibraryScanResult) (*LibraryScanR dir := filepath.Dir(filePath) result.AlbumName = filepath.Base(dir) - if result.AlbumName == "." || result.AlbumName == "" { + if result.AlbumName == "." || result.AlbumName == "" || result.AlbumName == "fd" || result.AlbumName == "self" { result.AlbumName = "Unknown Album" } @@ -473,8 +520,12 @@ func CancelLibraryScan() { } func ReadAudioMetadata(filePath string) (string, error) { + return ReadAudioMetadataWithDisplayName(filePath, "") +} + +func ReadAudioMetadataWithDisplayName(filePath, displayNameHint string) (string, error) { scanTime := time.Now().UTC().Format(time.RFC3339) - result, err := scanAudioFile(filePath, scanTime) + result, err := scanAudioFileWithKnownModTimeAndDisplayName(filePath, displayNameHint, scanTime, 0) if err != nil { return "", err } @@ -541,14 +592,13 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, // Find files to scan (new or modified) var filesToScan []libraryAudioFileInfo skippedCount := 0 - - // Build a set of existing CUE virtual path base files for incremental matching. - // CUE tracks are stored with virtual paths like "/path/album.cue#track01". - // We need to match these against the actual .cue file's modTime. - cueBaseModTimes := make(map[string]int64) // base cue path -> modTime from disk - for _, f := range currentFiles { - if strings.ToLower(filepath.Ext(f.path)) == ".cue" { - cueBaseModTimes[f.path] = f.modTime + existingCueTrackModTimes := make(map[string]int64) + for existingPath, modTime := range existingFiles { + if idx := strings.LastIndex(existingPath, "#track"); idx > 0 { + baseCuePath := existingPath[:idx] + if _, exists := existingCueTrackModTimes[baseCuePath]; !exists { + existingCueTrackModTimes[baseCuePath] = modTime + } } } @@ -557,25 +607,12 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, if !exists { // For .cue files, also check if any virtual path entries exist if strings.ToLower(filepath.Ext(f.path)) == ".cue" { - hasCueTracks := false - for existingPath := range existingFiles { - if strings.HasPrefix(existingPath, f.path+"#track") { - hasCueTracks = true - break - } - } - if hasCueTracks { + if cueTrackModTime, hasCueTracks := existingCueTrackModTimes[f.path]; hasCueTracks { // CUE file exists in DB via virtual paths; check if modTime changed - // Use modTime from any virtual path (they all share the same .cue modTime) - for existingPath, modTime := range existingFiles { - if strings.HasPrefix(existingPath, f.path+"#track") { - if f.modTime == modTime { - skippedCount++ - } else { - filesToScan = append(filesToScan, f) - } - break - } + if f.modTime == cueTrackModTime { + skippedCount++ + } else { + filesToScan = append(filesToScan, f) } continue } @@ -630,6 +667,7 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, // Track audio files referenced by .cue sheets to avoid duplicates (incremental) cueReferencedAudioFilesInc := make(map[string]bool) + parsedCueFiles := make(map[string]scannedCueFileInfo) for _, f := range filesToScan { ext := strings.ToLower(filepath.Ext(f.path)) if ext == ".cue" { @@ -637,6 +675,10 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, if err == nil && sheet.FileName != "" { audioPath := ResolveCueAudioPath(f.path, sheet.FileName) if audioPath != "" { + parsedCueFiles[f.path] = scannedCueFileInfo{ + sheet: sheet, + audioPath: audioPath, + } cueReferencedAudioFilesInc[audioPath] = true } } @@ -660,7 +702,20 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, // Handle .cue files: produce multiple track results if ext == ".cue" { - cueResults, err := ScanCueFileForLibrary(f.path, scanTime) + var cueResults []LibraryScanResult + cueInfo, ok := parsedCueFiles[f.path] + if ok { + cueResults, err = scanCueSheetForLibrary( + f.path, + cueInfo.sheet, + cueInfo.audioPath, + "", + f.modTime, + scanTime, + ) + } else { + cueResults, err = ScanCueFileForLibrary(f.path, scanTime) + } if err != nil { errorCount++ GoLog("[LibraryScan] Error scanning cue %s: %v\n", f.path, err) @@ -675,7 +730,7 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, continue } - result, err := scanAudioFile(f.path, scanTime) + result, err := scanAudioFileWithKnownModTime(f.path, scanTime, f.modTime) if err != nil { errorCount++ GoLog("[LibraryScan] Error scanning %s: %v\n", f.path, err) diff --git a/lib/main.dart b/lib/main.dart index 6963a9ee..e0f155e5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; +import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/services/notification_service.dart'; import 'package:spotiflac_android/services/share_intent_service.dart'; import 'package:spotiflac_android/services/cover_cache_manager.dart'; @@ -89,14 +90,51 @@ class _EagerInitialization extends ConsumerStatefulWidget { } class _EagerInitializationState extends ConsumerState<_EagerInitialization> { + ProviderSubscription? _localLibraryEnabledSub; + bool _localLibraryPreloaded = false; + @override void initState() { super.initState(); _initializeAppServices(); _initializeExtensions(); ref.read(downloadHistoryProvider); - ref.read(localLibraryProvider); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _initializeDeferredProviders(); + }); + } + + @override + void dispose() { + _localLibraryEnabledSub?.close(); + super.dispose(); + } + + void _initializeDeferredProviders() { ref.read(libraryCollectionsProvider); + _maybePreloadLocalLibrary( + ref.read( + settingsProvider.select((settings) => settings.localLibraryEnabled), + ), + ); + + _localLibraryEnabledSub = ref.listenManual( + settingsProvider.select((settings) => settings.localLibraryEnabled), + (previous, next) { + if (next == true) { + _maybePreloadLocalLibrary(true); + } + }, + ); + } + + void _maybePreloadLocalLibrary(bool enabled) { + if (!enabled || _localLibraryPreloaded) return; + _localLibraryPreloaded = true; + ref.read(localLibraryProvider); + _localLibraryEnabledSub?.close(); + _localLibraryEnabledSub = null; } Future _initializeAppServices() async { diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 18348f96..7dc37dd3 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -205,6 +205,7 @@ class DownloadHistoryState { final List items; final Map _bySpotifyId; final Map _byIsrc; + final Map _byTrackArtistKey; DownloadHistoryState({this.items = const []}) : _bySpotifyId = Map.fromEntries( @@ -218,8 +219,25 @@ class DownloadHistoryState { items .where((item) => item.isrc != null && item.isrc!.isNotEmpty) .map((item) => MapEntry(item.isrc!, item)), + ), + _byTrackArtistKey = Map.fromEntries( + items + .map( + (item) => MapEntry( + _trackArtistKey(item.trackName, item.artistName), + item, + ), + ) + .where((entry) => entry.key.isNotEmpty), ); + static String _trackArtistKey(String trackName, String artistName) { + final normalizedTrack = trackName.trim().toLowerCase(); + if (normalizedTrack.isEmpty) return ''; + final normalizedArtist = artistName.trim().toLowerCase(); + return '$normalizedTrack|$normalizedArtist'; + } + bool isDownloaded(String spotifyId) => _bySpotifyId.containsKey(spotifyId); DownloadHistoryItem? getBySpotifyId(String spotifyId) => @@ -231,16 +249,9 @@ class DownloadHistoryState { String trackName, String artistName, ) { - final normalizedTrack = trackName.trim().toLowerCase(); - final normalizedArtist = artistName.trim().toLowerCase(); - if (normalizedTrack.isEmpty) return null; - for (final item in items) { - if (item.trackName.trim().toLowerCase() == normalizedTrack && - item.artistName.trim().toLowerCase() == normalizedArtist) { - return item; - } - } - return null; + final key = _trackArtistKey(trackName, artistName); + if (key.isEmpty) return null; + return _byTrackArtistKey[key]; } DownloadHistoryState copyWith({List? items}) { @@ -252,10 +263,12 @@ class DownloadHistoryNotifier extends Notifier { static const int _safRepairBatchSize = 20; static const int _safRepairMaxPerLaunch = 60; static const int _audioMetadataBackfillMaxPerLaunch = 24; + static const _startupMaintenanceDelay = Duration(seconds: 2); final HistoryDatabase _db = HistoryDatabase.instance; bool _isLoaded = false; bool _isSafRepairInProgress = false; bool _isAudioMetadataBackfillInProgress = false; + bool _startupMaintenanceScheduled = false; @override DownloadHistoryState build() { @@ -292,33 +305,45 @@ class DownloadHistoryNotifier extends Notifier { state = state.copyWith(items: items); _historyLog.i('Loaded ${items.length} items from SQLite database'); - - if (Platform.isAndroid) { - Future.microtask(() async { - await _repairMissingSafEntries( - items, - maxItems: _safRepairMaxPerLaunch, - ); - await cleanupOrphanedDownloads(); - await _backfillAudioMetadata( - state.items, - maxItems: _audioMetadataBackfillMaxPerLaunch, - ); - }); - } else { - Future.microtask(() async { - await cleanupOrphanedDownloads(); - await _backfillAudioMetadata( - state.items, - maxItems: _audioMetadataBackfillMaxPerLaunch, - ); - }); - } + _scheduleStartupMaintenance(items); } catch (e, stack) { _historyLog.e('Failed to load history from database: $e', e, stack); } } + void _scheduleStartupMaintenance(List initialItems) { + if (_startupMaintenanceScheduled) { + return; + } + _startupMaintenanceScheduled = true; + + unawaited( + Future.delayed(_startupMaintenanceDelay, () async { + try { + if (Platform.isAndroid) { + await _repairMissingSafEntries( + initialItems, + maxItems: _safRepairMaxPerLaunch, + ); + } + + await cleanupOrphanedDownloads(); + + final currentItems = state.items; + if (currentItems.isNotEmpty) { + await _backfillAudioMetadata( + currentItems, + maxItems: _audioMetadataBackfillMaxPerLaunch, + ); + } + } catch (e, stack) { + _historyLog.w('Startup history maintenance failed: $e'); + _historyLog.d('$stack'); + } + }), + ); + } + String _fileNameFromUri(String uri) { try { final parsed = Uri.parse(uri); @@ -1912,7 +1937,12 @@ class DownloadQueueNotifier extends Notifier { ); } - String addToQueue(Track track, String service, {String? qualityOverride, String? playlistName}) { + String addToQueue( + Track track, + String service, { + String? qualityOverride, + String? playlistName, + }) { final settings = ref.read(settingsProvider); updateSettings(settings); From 16669d8b7adc1dfe47f27a5bc755d9edec09ba75 Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 12 Mar 2026 04:02:14 +0700 Subject: [PATCH 039/145] feat: show 'Internal' version in debug builds, optimize download timeouts, and fix navigation safety - Add displayVersion getter using kDebugMode: debug shows 'Internal', release shows actual version - Defer Spotify URL resolution in Deezer downloader until fallback is actually needed - Unify download timeouts to 24h constant (connection-level timeouts still protect hung connections) - Fix context shadowing in track metadata options menu and delete dialog - Use addPostFrameCallback + mounted guards for safer sheet/dialog navigation --- go_backend/deezer_download.go | 16 +++- go_backend/extension_providers.go | 2 +- go_backend/httputil.go | 2 +- go_backend/tidal.go | 2 +- go_backend/youtube.go | 3 +- lib/constants/app_info.dart | 5 + lib/screens/settings/about_page.dart | 4 +- lib/screens/settings/settings_tab.dart | 2 +- lib/screens/track_metadata_screen.dart | 123 +++++++++++++++---------- lib/widgets/update_dialog.dart | 2 +- 10 files changed, 99 insertions(+), 62 deletions(-) diff --git a/go_backend/deezer_download.go b/go_backend/deezer_download.go index 01e5c654..bef0e2ff 100644 --- a/go_backend/deezer_download.go +++ b/go_backend/deezer_download.go @@ -394,11 +394,6 @@ func downloadFromDeezer(req DownloadRequest) (DeezerDownloadResult, error) { } } - spotifyURL, err := resolveSpotifyURLForYoinkify(req) - if err != nil { - return DeezerDownloadResult{}, err - } - filename := buildFilenameFromTemplate(req.FilenameFormat, map[string]interface{}{ "title": req.TrackName, "artist": req.ArtistName, @@ -461,6 +456,17 @@ func downloadFromDeezer(req DownloadRequest) (DeezerDownloadResult, error) { } if downloadErr != nil || deezerURLErr != nil { + spotifyURL, err := resolveSpotifyURLForYoinkify(req) + if err != nil { + if deezerURLErr != nil { + return DeezerDownloadResult{}, fmt.Errorf( + "deezer download failed: direct Deezer resolution error: %v; Yoinkify fallback error: %w", + deezerURLErr, + err, + ) + } + return DeezerDownloadResult{}, err + } downloadErr = deezerClient.DownloadFromYoinkify(spotifyURL, outputPath, req.OutputFD, req.ItemID) if downloadErr != nil { if errors.Is(downloadErr, ErrDownloadCancelled) { diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 89cf02a2..9da7c344 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -484,7 +484,7 @@ func (p *ExtensionProviderWrapper) GetDownloadURL(trackID, quality string) (*Ext return &urlResult, nil } -const ExtDownloadTimeout = 5 * time.Minute +const ExtDownloadTimeout = DownloadTimeout func (p *ExtensionProviderWrapper) Download(trackID, quality, outputPath string, onProgress func(percent int)) (*ExtDownloadResult, error) { if !p.extension.Manifest.IsDownloadProvider() { diff --git a/go_backend/httputil.go b/go_backend/httputil.go index b54ec489..08fac2a8 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -31,7 +31,7 @@ func getRandomUserAgent() string { const ( DefaultTimeout = 60 * time.Second - DownloadTimeout = 120 * time.Second + DownloadTimeout = 24 * time.Hour SongLinkTimeout = 30 * time.Second DefaultMaxRetries = 3 DefaultRetryDelay = 1 * time.Second diff --git a/go_backend/tidal.go b/go_backend/tidal.go index ea9a8886..1e540110 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -583,7 +583,7 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, GoLog("[Tidal] Manifest parsed - directURL: %v, initURL: %v, mediaURLs count: %d\n", directURL != "", initURL != "", len(mediaURLs)) - client := NewHTTPClientWithTimeout(120 * time.Second) + client := NewHTTPClientWithTimeout(DownloadTimeout) if directURL != "" { GoLog("[Tidal] BTS format - downloading from direct URL: %s...\n", directURL[:min(80, len(directURL))]) diff --git a/go_backend/youtube.go b/go_backend/youtube.go index e43d0e39..b8038032 100644 --- a/go_backend/youtube.go +++ b/go_backend/youtube.go @@ -11,7 +11,6 @@ import ( "strconv" "strings" "sync" - "time" ) type YouTubeDownloader struct { @@ -82,7 +81,7 @@ type YouTubeDownloadResult struct { func NewYouTubeDownloader() *YouTubeDownloader { youtubeDownloaderOnce.Do(func() { globalYouTubeDownloader = &YouTubeDownloader{ - client: NewHTTPClientWithTimeout(120 * time.Second), + client: NewHTTPClientWithTimeout(DownloadTimeout), apiURL: "https://api.qwkuns.me", } }) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index d423fac6..911e99fe 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -1,9 +1,14 @@ +import 'package:flutter/foundation.dart'; + /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { static const String version = '3.7.2'; static const String buildNumber = '105'; static const String fullVersion = '$version+$buildNumber'; + + /// Shows "Internal" in debug builds, actual version in release. + static String get displayVersion => kDebugMode ? 'Internal' : version; static const String appName = 'SpotiFLAC'; diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 81fd1aa5..1ed5ce43 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -234,7 +234,7 @@ class AboutPage extends StatelessWidget { icon: Icons.info_outline, title: context.l10n.aboutVersion, subtitle: - 'v${AppInfo.version} (build ${AppInfo.buildNumber})', + 'v${AppInfo.displayVersion} (build ${AppInfo.buildNumber})', showDivider: false, ), ], @@ -341,7 +341,7 @@ class _AppHeaderCard extends StatelessWidget { borderRadius: BorderRadius.circular(12), ), child: Text( - 'v${AppInfo.version}', + 'v${AppInfo.displayVersion}', style: Theme.of(context).textTheme.labelMedium?.copyWith( color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index df5bc8d0..140f933b 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -133,7 +133,7 @@ class SettingsTab extends ConsumerWidget { SettingsItem( icon: Icons.info_outline, title: l10n.settingsAbout, - subtitle: '${l10n.aboutVersion} ${AppInfo.version}', + subtitle: '${l10n.aboutVersion} ${AppInfo.displayVersion}', onTap: () => _navigateTo(context, const AboutPage()), showDivider: false, ), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index d8693612..4394592f 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -2455,21 +2455,21 @@ class _TrackMetadataScreenState extends ConsumerState { } void _showOptionsMenu( - BuildContext context, + BuildContext screenContext, WidgetRef ref, ColorScheme colorScheme, ) { showModalBottomSheet( - context: context, + context: screenContext, useRootNavigator: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), isScrollControlled: true, constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.7, + maxHeight: MediaQuery.of(screenContext).size.height * 0.7, ), - builder: (context) => SafeArea( + builder: (sheetContext) => SafeArea( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -2486,89 +2486,99 @@ class _TrackMetadataScreenState extends ConsumerState { const SizedBox(height: 16), ListTile( leading: const Icon(Icons.copy), - title: Text(context.l10n.trackCopyFilePath), + title: Text(sheetContext.l10n.trackCopyFilePath), onTap: () { - Navigator.pop(context); - _copyToClipboard(context, cleanFilePath); + _closeOptionsMenuAndRun( + sheetContext, + () => _copyToClipboard(screenContext, cleanFilePath), + ); }, ), if (_fileExists) ListTile( leading: const Icon(Icons.edit_outlined), - title: Text(context.l10n.trackEditMetadata), + title: Text(sheetContext.l10n.trackEditMetadata), onTap: () { - Navigator.pop(context); - _showEditMetadataSheet(context, ref, colorScheme); + _closeOptionsMenuAndRun( + sheetContext, + () => + _showEditMetadataSheet(screenContext, ref, colorScheme), + ); }, ), if (!_isLocalItem && (_coverUrl != null || _fileExists)) ListTile( leading: const Icon(Icons.image_outlined), - title: Text(context.l10n.trackSaveCoverArt), - subtitle: Text(context.l10n.trackSaveCoverArtSubtitle), + title: Text(sheetContext.l10n.trackSaveCoverArt), + subtitle: Text(sheetContext.l10n.trackSaveCoverArtSubtitle), onTap: () { - Navigator.pop(context); - _saveCoverArt(); + _closeOptionsMenuAndRun(sheetContext, _saveCoverArt); }, ), if (!_isLocalItem) ListTile( leading: const Icon(Icons.lyrics_outlined), - title: Text(context.l10n.trackSaveLyrics), - subtitle: Text(context.l10n.trackSaveLyricsSubtitle), + title: Text(sheetContext.l10n.trackSaveLyrics), + subtitle: Text(sheetContext.l10n.trackSaveLyricsSubtitle), onTap: () { - Navigator.pop(context); - _saveLyrics(); + _closeOptionsMenuAndRun(sheetContext, _saveLyrics); }, ), if (_fileExists) ListTile( leading: const Icon(Icons.travel_explore), - title: Text(context.l10n.trackReEnrich), - subtitle: Text(context.l10n.trackReEnrichOnlineSubtitle), + title: Text(sheetContext.l10n.trackReEnrich), + subtitle: Text(sheetContext.l10n.trackReEnrichOnlineSubtitle), onTap: () { - Navigator.pop(context); - _reEnrichMetadata(); + _closeOptionsMenuAndRun(sheetContext, _reEnrichMetadata); }, ), if (_fileExists && _isConvertibleFormat) ListTile( leading: const Icon(Icons.swap_horiz), - title: Text(context.l10n.trackConvertFormat), - subtitle: Text(context.l10n.trackConvertFormatSubtitle), + title: Text(sheetContext.l10n.trackConvertFormat), + subtitle: Text(sheetContext.l10n.trackConvertFormatSubtitle), onTap: () { - Navigator.pop(context); - _showConvertSheet(context); + _closeOptionsMenuAndRun( + sheetContext, + () => _showConvertSheet(screenContext), + ); }, ), if (_fileExists && _isCueFile) ListTile( leading: const Icon(Icons.call_split), - title: Text(context.l10n.cueSplitTitle), - subtitle: Text(context.l10n.cueSplitSubtitle), + title: Text(sheetContext.l10n.cueSplitTitle), + subtitle: Text(sheetContext.l10n.cueSplitSubtitle), onTap: () { - Navigator.pop(context); - _showCueSplitSheet(context); + _closeOptionsMenuAndRun( + sheetContext, + () => _showCueSplitSheet(screenContext), + ); }, ), const Divider(height: 1), ListTile( leading: const Icon(Icons.share), - title: Text(context.l10n.trackMetadataShare), + title: Text(sheetContext.l10n.trackMetadataShare), onTap: () { - Navigator.pop(context); - _shareFile(context); + _closeOptionsMenuAndRun( + sheetContext, + () => _shareFile(screenContext), + ); }, ), ListTile( leading: Icon(Icons.delete, color: colorScheme.error), title: Text( - context.l10n.trackRemoveFromDevice, + sheetContext.l10n.trackRemoveFromDevice, style: TextStyle(color: colorScheme.error), ), onTap: () { - Navigator.pop(context); - _confirmDelete(context, ref, colorScheme); + _closeOptionsMenuAndRun( + sheetContext, + () => _confirmDelete(screenContext, ref, colorScheme), + ); }, ), const SizedBox(height: 16), @@ -3650,20 +3660,20 @@ class _TrackMetadataScreenState extends ConsumerState { } void _confirmDelete( - BuildContext context, + BuildContext screenContext, WidgetRef ref, ColorScheme colorScheme, ) { showDialog( - context: context, - useRootNavigator: false, - builder: (context) => AlertDialog( - title: Text(context.l10n.trackDeleteConfirmTitle), - content: Text(context.l10n.trackDeleteConfirmMessage), + context: screenContext, + useRootNavigator: true, + builder: (dialogContext) => AlertDialog( + title: Text(dialogContext.l10n.trackDeleteConfirmTitle), + content: Text(dialogContext.l10n.trackDeleteConfirmMessage), actions: [ TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.dialogCancel), + onPressed: () => Navigator.pop(dialogContext), + child: Text(dialogContext.l10n.dialogCancel), ), TextButton( onPressed: () async { @@ -3691,13 +3701,19 @@ class _TrackMetadataScreenState extends ConsumerState { .removeFromHistory(_downloadItem!.id); } - if (context.mounted) { - Navigator.pop(context); - Navigator.pop(context); + if (dialogContext.mounted) { + Navigator.pop(dialogContext); } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final navigator = Navigator.of(context); + if (navigator.canPop()) { + navigator.pop(true); + } + }); }, child: Text( - context.l10n.dialogDelete, + dialogContext.l10n.dialogDelete, style: TextStyle(color: colorScheme.error), ), ), @@ -3706,6 +3722,17 @@ class _TrackMetadataScreenState extends ConsumerState { ); } + void _closeOptionsMenuAndRun( + BuildContext sheetContext, + VoidCallback action, + ) { + Navigator.pop(sheetContext); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + action(); + }); + } + Future _openFile(BuildContext context, String filePath) async { if (isCueVirtualPath(filePath)) { _showCueVirtualTrackSnackBar(context); diff --git a/lib/widgets/update_dialog.dart b/lib/widgets/update_dialog.dart index f46c4d04..b015a293 100644 --- a/lib/widgets/update_dialog.dart +++ b/lib/widgets/update_dialog.dart @@ -157,7 +157,7 @@ class _UpdateDialogState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - _VersionChip(version: AppInfo.version, label: context.l10n.updateCurrent, colorScheme: colorScheme), + _VersionChip(version: AppInfo.displayVersion, label: context.l10n.updateCurrent, colorScheme: colorScheme), const SizedBox(width: 12), Icon(Icons.arrow_forward_rounded, size: 20, color: colorScheme.primary), const SizedBox(width: 12), From 48f614359e12daefe56aaa9f454ed50bf9df8fa8 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 13 Mar 2026 15:12:12 +0700 Subject: [PATCH 040/145] feat(i18n): replace all hardcoded strings with l10n keys across 13 screens - Added 80+ new keys to app_en.arb covering lyrics, SAF, download settings, snackbars, dialogs, home, cache, and store screens - Replaced hardcoded strings in main_shell, album_screen, playlist_screen, library_tracks_folder_screen, home_tab, settings_tab, download_settings_page, lyrics_provider_priority_page, track_metadata_screen, extension_detail_page, cache_management_page, local_album_screen, downloaded_album_screen, search_screen - Fixed structural bug in track_metadata_screen (duplicate closing brace) - Added missing l10n.dart import to search_screen.dart - Regenerated all app_localizations*.dart files via flutter gen-l10n --- lib/l10n/app_localizations.dart | 614 +++++++++++++++++- lib/l10n/app_localizations_de.dart | 354 ++++++++++ lib/l10n/app_localizations_en.dart | 354 ++++++++++ lib/l10n/app_localizations_es.dart | 354 ++++++++++ lib/l10n/app_localizations_fr.dart | 354 ++++++++++ lib/l10n/app_localizations_hi.dart | 354 ++++++++++ lib/l10n/app_localizations_id.dart | 354 ++++++++++ lib/l10n/app_localizations_ja.dart | 354 ++++++++++ lib/l10n/app_localizations_ko.dart | 354 ++++++++++ lib/l10n/app_localizations_nl.dart | 354 ++++++++++ lib/l10n/app_localizations_pt.dart | 354 ++++++++++ lib/l10n/app_localizations_ru.dart | 354 ++++++++++ lib/l10n/app_localizations_tr.dart | 354 ++++++++++ lib/l10n/app_localizations_zh.dart | 354 ++++++++++ lib/l10n/arb/app_en.arb | 452 +++++++++++++ lib/screens/album_screen.dart | 16 +- lib/screens/downloaded_album_screen.dart | 2 +- lib/screens/home_tab.dart | 6 +- lib/screens/library_tracks_folder_screen.dart | 6 +- lib/screens/local_album_screen.dart | 2 +- lib/screens/main_shell.dart | 23 +- lib/screens/playlist_screen.dart | 22 +- lib/screens/search_screen.dart | 3 +- .../settings/cache_management_page.dart | 6 +- .../settings/download_settings_page.dart | 152 ++--- .../settings/extension_detail_page.dart | 4 +- .../lyrics_provider_priority_page.dart | 54 +- lib/screens/settings/settings_tab.dart | 4 +- lib/screens/track_metadata_screen.dart | 24 +- 29 files changed, 5831 insertions(+), 161 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 488563dc..53bcba7f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -256,7 +256,7 @@ abstract class AppLocalizations { /// **'Filename Format'** String get downloadFilenameFormat; - /// Setting for folder structure + /// Title of the folder organization picker bottom sheet /// /// In en, this message translates to: /// **'Folder Organization'** @@ -2236,6 +2236,84 @@ abstract class AppLocalizations { /// **'Clear filters'** String get storeClearFilters; + /// Store setup screen - heading when no repo is configured + /// + /// In en, this message translates to: + /// **'Add Extension Repository'** + String get storeAddRepoTitle; + + /// Store setup screen - explanatory text + /// + /// In en, this message translates to: + /// **'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'** + String get storeAddRepoDescription; + + /// Label for the repository URL input field + /// + /// In en, this message translates to: + /// **'Repository URL'** + String get storeRepoUrlLabel; + + /// Hint/placeholder for the repository URL input field + /// + /// In en, this message translates to: + /// **'https://github.com/user/repo'** + String get storeRepoUrlHint; + + /// Helper text below the repository URL input field + /// + /// In en, this message translates to: + /// **'e.g. https://github.com/user/extensions-repo'** + String get storeRepoUrlHelper; + + /// Button to submit a new repository URL + /// + /// In en, this message translates to: + /// **'Add Repository'** + String get storeAddRepoButton; + + /// Tooltip for the change-repository icon button in the app bar + /// + /// In en, this message translates to: + /// **'Change repository'** + String get storeChangeRepoTooltip; + + /// Title of the change/remove repository dialog + /// + /// In en, this message translates to: + /// **'Extension Repository'** + String get storeRepoDialogTitle; + + /// Label shown above the current repository URL in the dialog + /// + /// In en, this message translates to: + /// **'Current repository:'** + String get storeRepoDialogCurrent; + + /// Label for the new repository URL field inside the dialog + /// + /// In en, this message translates to: + /// **'New Repository URL'** + String get storeNewRepoUrlLabel; + + /// Error heading when the store cannot be loaded + /// + /// In en, this message translates to: + /// **'Failed to load store'** + String get storeLoadError; + + /// Message when store has no extensions + /// + /// In en, this message translates to: + /// **'No extensions available'** + String get storeEmptyNoExtensions; + + /// Message when search/filter returns no results + /// + /// In en, this message translates to: + /// **'No extensions found'** + String get storeEmptyNoResults; + /// Default search provider option /// /// In en, this message translates to: @@ -4205,6 +4283,540 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Artist folders use Track Artist only'** String get downloadUseAlbumArtistForFoldersTrackSubtitle; + + /// Title for the lyrics provider priority page + /// + /// In en, this message translates to: + /// **'Lyrics Providers'** + String get lyricsProvidersTitle; + + /// Description on the lyrics provider priority page + /// + /// In en, this message translates to: + /// **'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'** + String get lyricsProvidersDescription; + + /// Info tip on lyrics provider priority page + /// + /// In en, this message translates to: + /// **'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'** + String get lyricsProvidersInfoText; + + /// Section header for enabled providers + /// + /// In en, this message translates to: + /// **'Enabled ({count})'** + String lyricsProvidersEnabledSection(int count); + + /// Section header for disabled providers + /// + /// In en, this message translates to: + /// **'Disabled ({count})'** + String lyricsProvidersDisabledSection(int count); + + /// Snackbar when user tries to disable the last enabled provider + /// + /// In en, this message translates to: + /// **'At least one provider must remain enabled'** + String get lyricsProvidersAtLeastOne; + + /// Snackbar after saving lyrics provider priority + /// + /// In en, this message translates to: + /// **'Lyrics provider priority saved'** + String get lyricsProvidersSaved; + + /// Body text of the discard-changes dialog on lyrics provider page + /// + /// In en, this message translates to: + /// **'You have unsaved changes that will be lost.'** + String get lyricsProvidersDiscardContent; + + /// Description for Spotify Lyrics API provider + /// + /// In en, this message translates to: + /// **'Spotify-sourced synced lyrics via community API'** + String get lyricsProviderSpotifyApiDesc; + + /// Description for LRCLIB provider + /// + /// In en, this message translates to: + /// **'Open-source synced lyrics database'** + String get lyricsProviderLrclibDesc; + + /// Description for Netease provider + /// + /// In en, this message translates to: + /// **'NetEase Cloud Music (good for Asian songs)'** + String get lyricsProviderNeteaseDesc; + + /// Description for Musixmatch provider + /// + /// In en, this message translates to: + /// **'Largest lyrics database (multi-language)'** + String get lyricsProviderMusixmatchDesc; + + /// Description for Apple Music provider + /// + /// In en, this message translates to: + /// **'Word-by-word synced lyrics (via proxy)'** + String get lyricsProviderAppleMusicDesc; + + /// Description for QQ Music provider + /// + /// In en, this message translates to: + /// **'QQ Music (good for Chinese songs, via proxy)'** + String get lyricsProviderQqMusicDesc; + + /// Generic description for extension-based lyrics providers + /// + /// In en, this message translates to: + /// **'Extension provider'** + String get lyricsProviderExtensionDesc; + + /// Title of SAF migration dialog + /// + /// In en, this message translates to: + /// **'Storage Update Required'** + String get safMigrationTitle; + + /// First paragraph of SAF migration dialog + /// + /// In en, this message translates to: + /// **'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'** + String get safMigrationMessage1; + + /// Second paragraph of SAF migration dialog + /// + /// In en, this message translates to: + /// **'Please select your download folder again to switch to the new storage system.'** + String get safMigrationMessage2; + + /// Snackbar after successfully migrating to SAF + /// + /// In en, this message translates to: + /// **'Download folder updated to SAF mode'** + String get safMigrationSuccess; + + /// Settings menu item - donate + /// + /// In en, this message translates to: + /// **'Donate'** + String get settingsDonate; + + /// Subtitle for donate menu item + /// + /// In en, this message translates to: + /// **'Support SpotiFLAC-Mobile development'** + String get settingsDonateSubtitle; + + /// Tooltip for the Love All button on album/playlist screens + /// + /// In en, this message translates to: + /// **'Love All'** + String get tooltipLoveAll; + + /// Tooltip for the Add to Playlist button + /// + /// In en, this message translates to: + /// **'Add to Playlist'** + String get tooltipAddToPlaylist; + + /// Snackbar after removing multiple tracks from Loved folder + /// + /// In en, this message translates to: + /// **'Removed {count} tracks from Loved'** + String snackbarRemovedTracksFromLoved(int count); + + /// Snackbar after adding multiple tracks to Loved folder + /// + /// In en, this message translates to: + /// **'Added {count} tracks to Loved'** + String snackbarAddedTracksToLoved(int count); + + /// Title of the Download All confirmation dialog + /// + /// In en, this message translates to: + /// **'Download All'** + String get dialogDownloadAllTitle; + + /// Body of the Download All confirmation dialog + /// + /// In en, this message translates to: + /// **'Download {count} tracks?'** + String dialogDownloadAllMessage(int count); + + /// Confirm button in Download All dialog + /// + /// In en, this message translates to: + /// **'Download'** + String get dialogDownload; + + /// Checkbox label in import dialog to skip already-downloaded songs + /// + /// In en, this message translates to: + /// **'Skip already downloaded songs'** + String get homeSkipAlreadyDownloaded; + + /// Context menu item to navigate to the album page + /// + /// In en, this message translates to: + /// **'Go to Album'** + String get homeGoToAlbum; + + /// Snackbar when album info cannot be loaded + /// + /// In en, this message translates to: + /// **'Album info not available'** + String get homeAlbumInfoUnavailable; + + /// Snackbar while loading a CUE sheet file + /// + /// In en, this message translates to: + /// **'Loading CUE sheet...'** + String get snackbarLoadingCueSheet; + + /// Snackbar after successfully saving track metadata + /// + /// In en, this message translates to: + /// **'Metadata saved successfully'** + String get snackbarMetadataSaved; + + /// Snackbar when lyrics embedding fails + /// + /// In en, this message translates to: + /// **'Failed to embed lyrics'** + String get snackbarFailedToEmbedLyrics; + + /// Snackbar when writing metadata back to file fails + /// + /// In en, this message translates to: + /// **'Failed to write back to storage'** + String get snackbarFailedToWriteStorage; + + /// Generic error snackbar with error detail + /// + /// In en, this message translates to: + /// **'Error: {error}'** + String snackbarError(String error); + + /// Snackbar when an extension button has no action configured + /// + /// In en, this message translates to: + /// **'No action defined for this button'** + String get snackbarNoActionDefined; + + /// Empty state message when an album has no tracks + /// + /// In en, this message translates to: + /// **'No tracks found for this album'** + String get noTracksFoundForAlbum; + + /// Subtitle text in Android download location bottom sheet + /// + /// In en, this message translates to: + /// **'Choose storage mode for downloaded files.'** + String get downloadLocationSubtitle; + + /// Storage mode option - use legacy app folder + /// + /// In en, this message translates to: + /// **'App folder (non-SAF)'** + String get storageModeAppFolder; + + /// Subtitle for app folder storage mode + /// + /// In en, this message translates to: + /// **'Use default Music/SpotiFLAC path'** + String get storageModeAppFolderSubtitle; + + /// Storage mode option - use Android SAF picker + /// + /// In en, this message translates to: + /// **'SAF folder'** + String get storageModeSaf; + + /// Subtitle for SAF storage mode + /// + /// In en, this message translates to: + /// **'Pick folder via Android Storage Access Framework'** + String get storageModeSafSubtitle; + + /// Description text in filename format bottom sheet + /// + /// In en, this message translates to: + /// **'Customize how your files are named.'** + String get downloadFilenameDescription; + + /// Label above filename tag chips + /// + /// In en, this message translates to: + /// **'Tap to insert tag:'** + String get downloadFilenameInsertTag; + + /// Subtitle when separate singles folder is enabled + /// + /// In en, this message translates to: + /// **'Albums/ and Singles/ folders'** + String get downloadSeparateSinglesEnabled; + + /// Subtitle when separate singles folder is disabled + /// + /// In en, this message translates to: + /// **'All files in same structure'** + String get downloadSeparateSinglesDisabled; + + /// Setting title for artist folder filter options + /// + /// In en, this message translates to: + /// **'Artist Name Filters'** + String get downloadArtistNameFilters; + + /// Setting title for SongLink country region + /// + /// In en, this message translates to: + /// **'SongLink Region'** + String get downloadSongLinkRegion; + + /// Setting title for network compatibility toggle + /// + /// In en, this message translates to: + /// **'Network compatibility mode'** + String get downloadNetworkCompatibilityMode; + + /// Subtitle when network compatibility mode is enabled + /// + /// In en, this message translates to: + /// **'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'** + String get downloadNetworkCompatibilityModeEnabled; + + /// Subtitle when network compatibility mode is disabled + /// + /// In en, this message translates to: + /// **'Off: strict HTTPS certificate validation (recommended)'** + String get downloadNetworkCompatibilityModeDisabled; + + /// Hint shown instead of Ask-quality subtitle when no built-in service selected + /// + /// In en, this message translates to: + /// **'Select a built-in service to enable'** + String get downloadSelectServiceToEnable; + + /// Quality option label for Tidal lossy 320kbps + /// + /// In en, this message translates to: + /// **'Lossy 320kbps'** + String get downloadLossy320; + + /// Setting title to pick output format for Tidal lossy downloads + /// + /// In en, this message translates to: + /// **'Lossy Format'** + String get downloadLossyFormat; + + /// Info hint when non-Tidal/Qobuz service is selected + /// + /// In en, this message translates to: + /// **'Select Tidal or Qobuz above to configure quality'** + String get downloadSelectTidalQobuz; + + /// Subtitle for Embed Lyrics when Embed Metadata is disabled + /// + /// In en, this message translates to: + /// **'Disabled while Embed Metadata is turned off'** + String get downloadEmbedLyricsDisabled; + + /// Toggle title for including Netease translated lyrics + /// + /// In en, this message translates to: + /// **'Netease: Include Translation'** + String get downloadNeteaseIncludeTranslation; + + /// Subtitle when Netease translation is enabled + /// + /// In en, this message translates to: + /// **'Append translated lyrics when available'** + String get downloadNeteaseIncludeTranslationEnabled; + + /// Subtitle when Netease translation is disabled + /// + /// In en, this message translates to: + /// **'Use original lyrics only'** + String get downloadNeteaseIncludeTranslationDisabled; + + /// Toggle title for including Netease romanized lyrics + /// + /// In en, this message translates to: + /// **'Netease: Include Romanization'** + String get downloadNeteaseIncludeRomanization; + + /// Subtitle when Netease romanization is enabled + /// + /// In en, this message translates to: + /// **'Append romanized lyrics when available'** + String get downloadNeteaseIncludeRomanizationEnabled; + + /// Subtitle when Netease romanization is disabled + /// + /// In en, this message translates to: + /// **'Disabled'** + String get downloadNeteaseIncludeRomanizationDisabled; + + /// Toggle title for Apple/QQ multi-person word-by-word lyrics + /// + /// In en, this message translates to: + /// **'Apple/QQ Multi-Person Word-by-Word'** + String get downloadAppleQqMultiPerson; + + /// Subtitle when multi-person word-by-word is enabled + /// + /// In en, this message translates to: + /// **'Enable v1/v2 speaker and [bg:] tags'** + String get downloadAppleQqMultiPersonEnabled; + + /// Subtitle when multi-person word-by-word is disabled + /// + /// In en, this message translates to: + /// **'Simplified word-by-word formatting'** + String get downloadAppleQqMultiPersonDisabled; + + /// Setting title for Musixmatch language preference + /// + /// In en, this message translates to: + /// **'Musixmatch Language'** + String get downloadMusixmatchLanguage; + + /// Option label when Musixmatch uses original language + /// + /// In en, this message translates to: + /// **'Auto (original)'** + String get downloadMusixmatchLanguageAuto; + + /// Toggle title for filtering contributing artists in Album Artist metadata + /// + /// In en, this message translates to: + /// **'Filter contributing artists in Album Artist'** + String get downloadFilterContributing; + + /// Subtitle when contributing artist filter is enabled + /// + /// In en, this message translates to: + /// **'Album Artist metadata uses primary artist only'** + String get downloadFilterContributingEnabled; + + /// Subtitle when contributing artist filter is disabled + /// + /// In en, this message translates to: + /// **'Keep full Album Artist metadata value'** + String get downloadFilterContributingDisabled; + + /// Subtitle for lyrics providers setting when no providers are enabled + /// + /// In en, this message translates to: + /// **'None enabled'** + String get downloadProvidersNoneEnabled; + + /// Label for the Musixmatch language code text field + /// + /// In en, this message translates to: + /// **'Language code'** + String get downloadMusixmatchLanguageCode; + + /// Hint text for the Musixmatch language code field + /// + /// In en, this message translates to: + /// **'auto / en / es / ja'** + String get downloadMusixmatchLanguageHint; + + /// Description in the Musixmatch language picker + /// + /// In en, this message translates to: + /// **'Set preferred language code (example: en, es, ja). Leave empty for auto.'** + String get downloadMusixmatchLanguageDesc; + + /// Button to reset Musixmatch language to automatic + /// + /// In en, this message translates to: + /// **'Auto'** + String get downloadMusixmatchAuto; + + /// Title of the Tidal lossy format picker bottom sheet + /// + /// In en, this message translates to: + /// **'Lossy 320kbps Format'** + String get downloadLossy320Format; + + /// Description in the Tidal lossy format picker + /// + /// In en, this message translates to: + /// **'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'** + String get downloadLossy320FormatDesc; + + /// Tidal lossy format option - MP3 320kbps + /// + /// In en, this message translates to: + /// **'MP3 320kbps'** + String get downloadLossyMp3; + + /// Subtitle for MP3 320kbps option + /// + /// In en, this message translates to: + /// **'Best compatibility, ~10MB per track'** + String get downloadLossyMp3Subtitle; + + /// Tidal lossy format option - Opus 256kbps + /// + /// In en, this message translates to: + /// **'Opus 256kbps'** + String get downloadLossyOpus256; + + /// Subtitle for Opus 256kbps option + /// + /// In en, this message translates to: + /// **'Best quality Opus, ~8MB per track'** + String get downloadLossyOpus256Subtitle; + + /// Tidal lossy format option - Opus 128kbps + /// + /// In en, this message translates to: + /// **'Opus 128kbps'** + String get downloadLossyOpus128; + + /// Subtitle for Opus 128kbps option + /// + /// In en, this message translates to: + /// **'Smallest size, ~4MB per track'** + String get downloadLossyOpus128Subtitle; + + /// Subtitle for 'Any' network mode option + /// + /// In en, this message translates to: + /// **'WiFi + Mobile Data'** + String get downloadNetworkAnySubtitle; + + /// Subtitle for 'WiFi only' network mode option + /// + /// In en, this message translates to: + /// **'Pause downloads on mobile data'** + String get downloadNetworkWifiOnlySubtitle; + + /// Description in the SongLink region picker + /// + /// In en, this message translates to: + /// **'Used as userCountry for SongLink API lookup.'** + String get downloadSongLinkRegionDesc; + + /// Snackbar when the audio format is not supported for the requested operation + /// + /// In en, this message translates to: + /// **'Unsupported audio format'** + String get snackbarUnsupportedAudioFormat; + + /// Tooltip for refresh button on cache management page + /// + /// In en, this message translates to: + /// **'Refresh'** + String get cacheRefresh; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 4cb5b75d..a3d43076 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1215,6 +1215,47 @@ class AppLocalizationsDe extends AppLocalizations { @override String get storeClearFilters => 'Filter entfernen'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Standard (Deezer/Spotify)'; @@ -2436,4 +2477,317 @@ class AppLocalizationsDe extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 646f9a58..03adf67d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1195,6 +1195,47 @@ class AppLocalizationsEn extends AppLocalizations { @override String get storeClearFilters => 'Clear filters'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; @@ -2408,4 +2449,317 @@ class AppLocalizationsEn extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e44fa9d8..c953dbc8 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1195,6 +1195,47 @@ class AppLocalizationsEs extends AppLocalizations { @override String get storeClearFilters => 'Clear filters'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; @@ -2408,6 +2449,319 @@ class AppLocalizationsEs extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } /// The translations for Spanish Castilian, as used in Spain (`es_ES`). diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index ae24b7e9..4baf9cba 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1197,6 +1197,47 @@ class AppLocalizationsFr extends AppLocalizations { @override String get storeClearFilters => 'Clear filters'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; @@ -2410,4 +2451,317 @@ class AppLocalizationsFr extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 08101eac..46d6572a 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1195,6 +1195,47 @@ class AppLocalizationsHi extends AppLocalizations { @override String get storeClearFilters => 'Clear filters'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; @@ -2408,4 +2449,317 @@ class AppLocalizationsHi extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index cab860dd..6aa97510 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1200,6 +1200,47 @@ class AppLocalizationsId extends AppLocalizations { @override String get storeClearFilters => 'Hapus filter'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; @@ -2415,4 +2456,317 @@ class AppLocalizationsId extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 6ce38247..92184e0b 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1189,6 +1189,47 @@ class AppLocalizationsJa extends AppLocalizations { @override String get storeClearFilters => 'フィルターを消去'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'デフォルト (Deezer/Spotify)'; @@ -2395,4 +2436,317 @@ class AppLocalizationsJa extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 56287787..aed486d5 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1175,6 +1175,47 @@ class AppLocalizationsKo extends AppLocalizations { @override String get storeClearFilters => 'Clear filters'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; @@ -2388,4 +2429,317 @@ class AppLocalizationsKo extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 5f4975d9..87c5385a 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1195,6 +1195,47 @@ class AppLocalizationsNl extends AppLocalizations { @override String get storeClearFilters => 'Clear filters'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; @@ -2408,4 +2449,317 @@ class AppLocalizationsNl extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 62b648ae..d45df4f9 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1195,6 +1195,47 @@ class AppLocalizationsPt extends AppLocalizations { @override String get storeClearFilters => 'Clear filters'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; @@ -2408,6 +2449,319 @@ class AppLocalizationsPt extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } /// The translations for Portuguese, as used in Portugal (`pt_PT`). diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 66c75d45..3e9d7e73 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1216,6 +1216,47 @@ class AppLocalizationsRu extends AppLocalizations { @override String get storeClearFilters => 'Очистить фильтры'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'По умолчанию (Deezer/Spotify)'; @@ -2467,4 +2508,317 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Папки исполнителя используют только трек исполнителя'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 3a88e179..48a9a7d2 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1206,6 +1206,47 @@ class AppLocalizationsTr extends AppLocalizations { @override String get storeClearFilters => 'Filtreleri temizle'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Varsayılan (Deezer/Spotify)'; @@ -2420,4 +2461,317 @@ class AppLocalizationsTr extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 3306bb1c..2287f76a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1195,6 +1195,47 @@ class AppLocalizationsZh extends AppLocalizations { @override String get storeClearFilters => 'Clear filters'; + @override + String get storeAddRepoTitle => 'Add Extension Repository'; + + @override + String get storeAddRepoDescription => + 'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.'; + + @override + String get storeRepoUrlLabel => 'Repository URL'; + + @override + String get storeRepoUrlHint => 'https://github.com/user/repo'; + + @override + String get storeRepoUrlHelper => + 'e.g. https://github.com/user/extensions-repo'; + + @override + String get storeAddRepoButton => 'Add Repository'; + + @override + String get storeChangeRepoTooltip => 'Change repository'; + + @override + String get storeRepoDialogTitle => 'Extension Repository'; + + @override + String get storeRepoDialogCurrent => 'Current repository:'; + + @override + String get storeNewRepoUrlLabel => 'New Repository URL'; + + @override + String get storeLoadError => 'Failed to load store'; + + @override + String get storeEmptyNoExtensions => 'No extensions available'; + + @override + String get storeEmptyNoResults => 'No extensions found'; + @override String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; @@ -2408,6 +2449,319 @@ class AppLocalizationsZh extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + + @override + String get lyricsProvidersTitle => 'Lyrics Providers'; + + @override + String get lyricsProvidersDescription => + 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.'; + + @override + String get lyricsProvidersInfoText => + 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.'; + + @override + String lyricsProvidersEnabledSection(int count) { + return 'Enabled ($count)'; + } + + @override + String lyricsProvidersDisabledSection(int count) { + return 'Disabled ($count)'; + } + + @override + String get lyricsProvidersAtLeastOne => + 'At least one provider must remain enabled'; + + @override + String get lyricsProvidersSaved => 'Lyrics provider priority saved'; + + @override + String get lyricsProvidersDiscardContent => + 'You have unsaved changes that will be lost.'; + + @override + String get lyricsProviderSpotifyApiDesc => + 'Spotify-sourced synced lyrics via community API'; + + @override + String get lyricsProviderLrclibDesc => 'Open-source synced lyrics database'; + + @override + String get lyricsProviderNeteaseDesc => + 'NetEase Cloud Music (good for Asian songs)'; + + @override + String get lyricsProviderMusixmatchDesc => + 'Largest lyrics database (multi-language)'; + + @override + String get lyricsProviderAppleMusicDesc => + 'Word-by-word synced lyrics (via proxy)'; + + @override + String get lyricsProviderQqMusicDesc => + 'QQ Music (good for Chinese songs, via proxy)'; + + @override + String get lyricsProviderExtensionDesc => 'Extension provider'; + + @override + String get safMigrationTitle => 'Storage Update Required'; + + @override + String get safMigrationMessage1 => + 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.'; + + @override + String get safMigrationMessage2 => + 'Please select your download folder again to switch to the new storage system.'; + + @override + String get safMigrationSuccess => 'Download folder updated to SAF mode'; + + @override + String get settingsDonate => 'Donate'; + + @override + String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development'; + + @override + String get tooltipLoveAll => 'Love All'; + + @override + String get tooltipAddToPlaylist => 'Add to Playlist'; + + @override + String snackbarRemovedTracksFromLoved(int count) { + return 'Removed $count tracks from Loved'; + } + + @override + String snackbarAddedTracksToLoved(int count) { + return 'Added $count tracks to Loved'; + } + + @override + String get dialogDownloadAllTitle => 'Download All'; + + @override + String dialogDownloadAllMessage(int count) { + return 'Download $count tracks?'; + } + + @override + String get dialogDownload => 'Download'; + + @override + String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; + + @override + String get homeGoToAlbum => 'Go to Album'; + + @override + String get homeAlbumInfoUnavailable => 'Album info not available'; + + @override + String get snackbarLoadingCueSheet => 'Loading CUE sheet...'; + + @override + String get snackbarMetadataSaved => 'Metadata saved successfully'; + + @override + String get snackbarFailedToEmbedLyrics => 'Failed to embed lyrics'; + + @override + String get snackbarFailedToWriteStorage => 'Failed to write back to storage'; + + @override + String snackbarError(String error) { + return 'Error: $error'; + } + + @override + String get snackbarNoActionDefined => 'No action defined for this button'; + + @override + String get noTracksFoundForAlbum => 'No tracks found for this album'; + + @override + String get downloadLocationSubtitle => + 'Choose storage mode for downloaded files.'; + + @override + String get storageModeAppFolder => 'App folder (non-SAF)'; + + @override + String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path'; + + @override + String get storageModeSaf => 'SAF folder'; + + @override + String get storageModeSafSubtitle => + 'Pick folder via Android Storage Access Framework'; + + @override + String get downloadFilenameDescription => + 'Customize how your files are named.'; + + @override + String get downloadFilenameInsertTag => 'Tap to insert tag:'; + + @override + String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders'; + + @override + String get downloadSeparateSinglesDisabled => 'All files in same structure'; + + @override + String get downloadArtistNameFilters => 'Artist Name Filters'; + + @override + String get downloadSongLinkRegion => 'SongLink Region'; + + @override + String get downloadNetworkCompatibilityMode => 'Network compatibility mode'; + + @override + String get downloadNetworkCompatibilityModeEnabled => + 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'; + + @override + String get downloadNetworkCompatibilityModeDisabled => + 'Off: strict HTTPS certificate validation (recommended)'; + + @override + String get downloadSelectServiceToEnable => + 'Select a built-in service to enable'; + + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadSelectTidalQobuz => + 'Select Tidal or Qobuz above to configure quality'; + + @override + String get downloadEmbedLyricsDisabled => + 'Disabled while Embed Metadata is turned off'; + + @override + String get downloadNeteaseIncludeTranslation => + 'Netease: Include Translation'; + + @override + String get downloadNeteaseIncludeTranslationEnabled => + 'Append translated lyrics when available'; + + @override + String get downloadNeteaseIncludeTranslationDisabled => + 'Use original lyrics only'; + + @override + String get downloadNeteaseIncludeRomanization => + 'Netease: Include Romanization'; + + @override + String get downloadNeteaseIncludeRomanizationEnabled => + 'Append romanized lyrics when available'; + + @override + String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled'; + + @override + String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word'; + + @override + String get downloadAppleQqMultiPersonEnabled => + 'Enable v1/v2 speaker and [bg:] tags'; + + @override + String get downloadAppleQqMultiPersonDisabled => + 'Simplified word-by-word formatting'; + + @override + String get downloadMusixmatchLanguage => 'Musixmatch Language'; + + @override + String get downloadMusixmatchLanguageAuto => 'Auto (original)'; + + @override + String get downloadFilterContributing => + 'Filter contributing artists in Album Artist'; + + @override + String get downloadFilterContributingEnabled => + 'Album Artist metadata uses primary artist only'; + + @override + String get downloadFilterContributingDisabled => + 'Keep full Album Artist metadata value'; + + @override + String get downloadProvidersNoneEnabled => 'None enabled'; + + @override + String get downloadMusixmatchLanguageCode => 'Language code'; + + @override + String get downloadMusixmatchLanguageHint => 'auto / en / es / ja'; + + @override + String get downloadMusixmatchLanguageDesc => + 'Set preferred language code (example: en, es, ja). Leave empty for auto.'; + + @override + String get downloadMusixmatchAuto => 'Auto'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + + @override + String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; + + @override + String get downloadNetworkWifiOnlySubtitle => + 'Pause downloads on mobile data'; + + @override + String get downloadSongLinkRegionDesc => + 'Used as userCountry for SongLink API lookup.'; + + @override + String get snackbarUnsupportedAudioFormat => 'Unsupported audio format'; + + @override + String get cacheRefresh => 'Refresh'; } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 03313d7b..10bf4938 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1570,6 +1570,58 @@ "@storeClearFilters": { "description": "Button to clear all filters" }, + "storeAddRepoTitle": "Add Extension Repository", + "@storeAddRepoTitle": { + "description": "Store setup screen - heading when no repo is configured" + }, + "storeAddRepoDescription": "Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.", + "@storeAddRepoDescription": { + "description": "Store setup screen - explanatory text" + }, + "storeRepoUrlLabel": "Repository URL", + "@storeRepoUrlLabel": { + "description": "Label for the repository URL input field" + }, + "storeRepoUrlHint": "https://github.com/user/repo", + "@storeRepoUrlHint": { + "description": "Hint/placeholder for the repository URL input field" + }, + "storeRepoUrlHelper": "e.g. https://github.com/user/extensions-repo", + "@storeRepoUrlHelper": { + "description": "Helper text below the repository URL input field" + }, + "storeAddRepoButton": "Add Repository", + "@storeAddRepoButton": { + "description": "Button to submit a new repository URL" + }, + "storeChangeRepoTooltip": "Change repository", + "@storeChangeRepoTooltip": { + "description": "Tooltip for the change-repository icon button in the app bar" + }, + "storeRepoDialogTitle": "Extension Repository", + "@storeRepoDialogTitle": { + "description": "Title of the change/remove repository dialog" + }, + "storeRepoDialogCurrent": "Current repository:", + "@storeRepoDialogCurrent": { + "description": "Label shown above the current repository URL in the dialog" + }, + "storeNewRepoUrlLabel": "New Repository URL", + "@storeNewRepoUrlLabel": { + "description": "Label for the new repository URL field inside the dialog" + }, + "storeLoadError": "Failed to load store", + "@storeLoadError": { + "description": "Error heading when the store cannot be loaded" + }, + "storeEmptyNoExtensions": "No extensions available", + "@storeEmptyNoExtensions": { + "description": "Message when store has no extensions" + }, + "storeEmptyNoResults": "No extensions found", + "@storeEmptyNoResults": { + "description": "Message when search/filter returns no results" + }, "extensionDefaultProvider": "Default (Deezer/Spotify)", "@extensionDefaultProvider": { "description": "Default search provider option" @@ -3205,5 +3257,405 @@ "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", "@downloadUseAlbumArtistForFoldersTrackSubtitle": { "description": "Subtitle when Track Artist is used for folder naming" + }, + + "lyricsProvidersTitle": "Lyrics Providers", + "@lyricsProvidersTitle": { + "description": "Title for the lyrics provider priority page" + }, + "lyricsProvidersDescription": "Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.", + "@lyricsProvidersDescription": { + "description": "Description on the lyrics provider priority page" + }, + "lyricsProvidersInfoText": "Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.", + "@lyricsProvidersInfoText": { + "description": "Info tip on lyrics provider priority page" + }, + "lyricsProvidersEnabledSection": "Enabled ({count})", + "@lyricsProvidersEnabledSection": { + "description": "Section header for enabled providers", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "lyricsProvidersDisabledSection": "Disabled ({count})", + "@lyricsProvidersDisabledSection": { + "description": "Section header for disabled providers", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "lyricsProvidersAtLeastOne": "At least one provider must remain enabled", + "@lyricsProvidersAtLeastOne": { + "description": "Snackbar when user tries to disable the last enabled provider" + }, + "lyricsProvidersSaved": "Lyrics provider priority saved", + "@lyricsProvidersSaved": { + "description": "Snackbar after saving lyrics provider priority" + }, + "lyricsProvidersDiscardContent": "You have unsaved changes that will be lost.", + "@lyricsProvidersDiscardContent": { + "description": "Body text of the discard-changes dialog on lyrics provider page" + }, + "lyricsProviderSpotifyApiDesc": "Spotify-sourced synced lyrics via community API", + "@lyricsProviderSpotifyApiDesc": { + "description": "Description for Spotify Lyrics API provider" + }, + "lyricsProviderLrclibDesc": "Open-source synced lyrics database", + "@lyricsProviderLrclibDesc": { + "description": "Description for LRCLIB provider" + }, + "lyricsProviderNeteaseDesc": "NetEase Cloud Music (good for Asian songs)", + "@lyricsProviderNeteaseDesc": { + "description": "Description for Netease provider" + }, + "lyricsProviderMusixmatchDesc": "Largest lyrics database (multi-language)", + "@lyricsProviderMusixmatchDesc": { + "description": "Description for Musixmatch provider" + }, + "lyricsProviderAppleMusicDesc": "Word-by-word synced lyrics (via proxy)", + "@lyricsProviderAppleMusicDesc": { + "description": "Description for Apple Music provider" + }, + "lyricsProviderQqMusicDesc": "QQ Music (good for Chinese songs, via proxy)", + "@lyricsProviderQqMusicDesc": { + "description": "Description for QQ Music provider" + }, + "lyricsProviderExtensionDesc": "Extension provider", + "@lyricsProviderExtensionDesc": { + "description": "Generic description for extension-based lyrics providers" + }, + + "safMigrationTitle": "Storage Update Required", + "@safMigrationTitle": { + "description": "Title of SAF migration dialog" + }, + "safMigrationMessage1": "SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.", + "@safMigrationMessage1": { + "description": "First paragraph of SAF migration dialog" + }, + "safMigrationMessage2": "Please select your download folder again to switch to the new storage system.", + "@safMigrationMessage2": { + "description": "Second paragraph of SAF migration dialog" + }, + "safMigrationSuccess": "Download folder updated to SAF mode", + "@safMigrationSuccess": { + "description": "Snackbar after successfully migrating to SAF" + }, + + "settingsDonate": "Donate", + "@settingsDonate": { + "description": "Settings menu item - donate" + }, + "settingsDonateSubtitle": "Support SpotiFLAC-Mobile development", + "@settingsDonateSubtitle": { + "description": "Subtitle for donate menu item" + }, + + "tooltipLoveAll": "Love All", + "@tooltipLoveAll": { + "description": "Tooltip for the Love All button on album/playlist screens" + }, + "tooltipAddToPlaylist": "Add to Playlist", + "@tooltipAddToPlaylist": { + "description": "Tooltip for the Add to Playlist button" + }, + "snackbarRemovedTracksFromLoved": "Removed {count} tracks from Loved", + "@snackbarRemovedTracksFromLoved": { + "description": "Snackbar after removing multiple tracks from Loved folder", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedTracksToLoved": "Added {count} tracks to Loved", + "@snackbarAddedTracksToLoved": { + "description": "Snackbar after adding multiple tracks to Loved folder", + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "dialogDownloadAllTitle": "Download All", + "@dialogDownloadAllTitle": { + "description": "Title of the Download All confirmation dialog" + }, + "dialogDownloadAllMessage": "Download {count} tracks?", + "@dialogDownloadAllMessage": { + "description": "Body of the Download All confirmation dialog", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogDownload": "Download", + "@dialogDownload": { + "description": "Confirm button in Download All dialog" + }, + + "homeSkipAlreadyDownloaded": "Skip already downloaded songs", + "@homeSkipAlreadyDownloaded": { + "description": "Checkbox label in import dialog to skip already-downloaded songs" + }, + "homeGoToAlbum": "Go to Album", + "@homeGoToAlbum": { + "description": "Context menu item to navigate to the album page" + }, + "homeAlbumInfoUnavailable": "Album info not available", + "@homeAlbumInfoUnavailable": { + "description": "Snackbar when album info cannot be loaded" + }, + + "snackbarLoadingCueSheet": "Loading CUE sheet...", + "@snackbarLoadingCueSheet": { + "description": "Snackbar while loading a CUE sheet file" + }, + "snackbarMetadataSaved": "Metadata saved successfully", + "@snackbarMetadataSaved": { + "description": "Snackbar after successfully saving track metadata" + }, + "snackbarFailedToEmbedLyrics": "Failed to embed lyrics", + "@snackbarFailedToEmbedLyrics": { + "description": "Snackbar when lyrics embedding fails" + }, + "snackbarFailedToWriteStorage": "Failed to write back to storage", + "@snackbarFailedToWriteStorage": { + "description": "Snackbar when writing metadata back to file fails" + }, + "snackbarError": "Error: {error}", + "@snackbarError": { + "description": "Generic error snackbar with error detail", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarNoActionDefined": "No action defined for this button", + "@snackbarNoActionDefined": { + "description": "Snackbar when an extension button has no action configured" + }, + + "noTracksFoundForAlbum": "No tracks found for this album", + "@noTracksFoundForAlbum": { + "description": "Empty state message when an album has no tracks" + }, + + "downloadLocationSubtitle": "Choose storage mode for downloaded files.", + "@downloadLocationSubtitle": { + "description": "Subtitle text in Android download location bottom sheet" + }, + "storageModeAppFolder": "App folder (non-SAF)", + "@storageModeAppFolder": { + "description": "Storage mode option - use legacy app folder" + }, + "storageModeAppFolderSubtitle": "Use default Music/SpotiFLAC path", + "@storageModeAppFolderSubtitle": { + "description": "Subtitle for app folder storage mode" + }, + "storageModeSaf": "SAF folder", + "@storageModeSaf": { + "description": "Storage mode option - use Android SAF picker" + }, + "storageModeSafSubtitle": "Pick folder via Android Storage Access Framework", + "@storageModeSafSubtitle": { + "description": "Subtitle for SAF storage mode" + }, + "downloadFilenameDescription": "Customize how your files are named.", + "@downloadFilenameDescription": { + "description": "Description text in filename format bottom sheet" + }, + "downloadFilenameInsertTag": "Tap to insert tag:", + "@downloadFilenameInsertTag": { + "description": "Label above filename tag chips" + }, + "downloadSeparateSinglesEnabled": "Albums/ and Singles/ folders", + "@downloadSeparateSinglesEnabled": { + "description": "Subtitle when separate singles folder is enabled" + }, + "downloadSeparateSinglesDisabled": "All files in same structure", + "@downloadSeparateSinglesDisabled": { + "description": "Subtitle when separate singles folder is disabled" + }, + "downloadArtistNameFilters": "Artist Name Filters", + "@downloadArtistNameFilters": { + "description": "Setting title for artist folder filter options" + }, + "downloadSongLinkRegion": "SongLink Region", + "@downloadSongLinkRegion": { + "description": "Setting title for SongLink country region" + }, + "downloadNetworkCompatibilityMode": "Network compatibility mode", + "@downloadNetworkCompatibilityMode": { + "description": "Setting title for network compatibility toggle" + }, + "downloadNetworkCompatibilityModeEnabled": "Enabled: try HTTP + accept invalid TLS certificates (unsafe)", + "@downloadNetworkCompatibilityModeEnabled": { + "description": "Subtitle when network compatibility mode is enabled" + }, + "downloadNetworkCompatibilityModeDisabled": "Off: strict HTTPS certificate validation (recommended)", + "@downloadNetworkCompatibilityModeDisabled": { + "description": "Subtitle when network compatibility mode is disabled" + }, + "downloadSelectServiceToEnable": "Select a built-in service to enable", + "@downloadSelectServiceToEnable": { + "description": "Hint shown instead of Ask-quality subtitle when no built-in service selected" + }, + "downloadLossy320": "Lossy 320kbps", + "@downloadLossy320": { + "description": "Quality option label for Tidal lossy 320kbps" + }, + "downloadLossyFormat": "Lossy Format", + "@downloadLossyFormat": { + "description": "Setting title to pick output format for Tidal lossy downloads" + }, + "downloadSelectTidalQobuz": "Select Tidal or Qobuz above to configure quality", + "@downloadSelectTidalQobuz": { + "description": "Info hint when non-Tidal/Qobuz service is selected" + }, + "downloadEmbedLyricsDisabled": "Disabled while Embed Metadata is turned off", + "@downloadEmbedLyricsDisabled": { + "description": "Subtitle for Embed Lyrics when Embed Metadata is disabled" + }, + "downloadNeteaseIncludeTranslation": "Netease: Include Translation", + "@downloadNeteaseIncludeTranslation": { + "description": "Toggle title for including Netease translated lyrics" + }, + "downloadNeteaseIncludeTranslationEnabled": "Append translated lyrics when available", + "@downloadNeteaseIncludeTranslationEnabled": { + "description": "Subtitle when Netease translation is enabled" + }, + "downloadNeteaseIncludeTranslationDisabled": "Use original lyrics only", + "@downloadNeteaseIncludeTranslationDisabled": { + "description": "Subtitle when Netease translation is disabled" + }, + "downloadNeteaseIncludeRomanization": "Netease: Include Romanization", + "@downloadNeteaseIncludeRomanization": { + "description": "Toggle title for including Netease romanized lyrics" + }, + "downloadNeteaseIncludeRomanizationEnabled": "Append romanized lyrics when available", + "@downloadNeteaseIncludeRomanizationEnabled": { + "description": "Subtitle when Netease romanization is enabled" + }, + "downloadNeteaseIncludeRomanizationDisabled": "Disabled", + "@downloadNeteaseIncludeRomanizationDisabled": { + "description": "Subtitle when Netease romanization is disabled" + }, + "downloadAppleQqMultiPerson": "Apple/QQ Multi-Person Word-by-Word", + "@downloadAppleQqMultiPerson": { + "description": "Toggle title for Apple/QQ multi-person word-by-word lyrics" + }, + "downloadAppleQqMultiPersonEnabled": "Enable v1/v2 speaker and [bg:] tags", + "@downloadAppleQqMultiPersonEnabled": { + "description": "Subtitle when multi-person word-by-word is enabled" + }, + "downloadAppleQqMultiPersonDisabled": "Simplified word-by-word formatting", + "@downloadAppleQqMultiPersonDisabled": { + "description": "Subtitle when multi-person word-by-word is disabled" + }, + "downloadMusixmatchLanguage": "Musixmatch Language", + "@downloadMusixmatchLanguage": { + "description": "Setting title for Musixmatch language preference" + }, + "downloadMusixmatchLanguageAuto": "Auto (original)", + "@downloadMusixmatchLanguageAuto": { + "description": "Option label when Musixmatch uses original language" + }, + "downloadFilterContributing": "Filter contributing artists in Album Artist", + "@downloadFilterContributing": { + "description": "Toggle title for filtering contributing artists in Album Artist metadata" + }, + "downloadFilterContributingEnabled": "Album Artist metadata uses primary artist only", + "@downloadFilterContributingEnabled": { + "description": "Subtitle when contributing artist filter is enabled" + }, + "downloadFilterContributingDisabled": "Keep full Album Artist metadata value", + "@downloadFilterContributingDisabled": { + "description": "Subtitle when contributing artist filter is disabled" + }, + + "downloadProvidersNoneEnabled": "None enabled", + "@downloadProvidersNoneEnabled": { + "description": "Subtitle for lyrics providers setting when no providers are enabled" + }, + "downloadMusixmatchLanguageCode": "Language code", + "@downloadMusixmatchLanguageCode": { + "description": "Label for the Musixmatch language code text field" + }, + "downloadMusixmatchLanguageHint": "auto / en / es / ja", + "@downloadMusixmatchLanguageHint": { + "description": "Hint text for the Musixmatch language code field" + }, + "downloadMusixmatchLanguageDesc": "Set preferred language code (example: en, es, ja). Leave empty for auto.", + "@downloadMusixmatchLanguageDesc": { + "description": "Description in the Musixmatch language picker" + }, + "downloadMusixmatchAuto": "Auto", + "@downloadMusixmatchAuto": { + "description": "Button to reset Musixmatch language to automatic" + }, + "downloadLossy320Format": "Lossy 320kbps Format", + "@downloadLossy320Format": { + "description": "Title of the Tidal lossy format picker bottom sheet" + }, + "downloadLossy320FormatDesc": "Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.", + "@downloadLossy320FormatDesc": { + "description": "Description in the Tidal lossy format picker" + }, + "downloadLossyMp3": "MP3 320kbps", + "@downloadLossyMp3": { + "description": "Tidal lossy format option - MP3 320kbps" + }, + "downloadLossyMp3Subtitle": "Best compatibility, ~10MB per track", + "@downloadLossyMp3Subtitle": { + "description": "Subtitle for MP3 320kbps option" + }, + "downloadLossyOpus256": "Opus 256kbps", + "@downloadLossyOpus256": { + "description": "Tidal lossy format option - Opus 256kbps" + }, + "downloadLossyOpus256Subtitle": "Best quality Opus, ~8MB per track", + "@downloadLossyOpus256Subtitle": { + "description": "Subtitle for Opus 256kbps option" + }, + "downloadLossyOpus128": "Opus 128kbps", + "@downloadLossyOpus128": { + "description": "Tidal lossy format option - Opus 128kbps" + }, + "downloadLossyOpus128Subtitle": "Smallest size, ~4MB per track", + "@downloadLossyOpus128Subtitle": { + "description": "Subtitle for Opus 128kbps option" + }, + "downloadNetworkAnySubtitle": "WiFi + Mobile Data", + "@downloadNetworkAnySubtitle": { + "description": "Subtitle for 'Any' network mode option" + }, + "downloadNetworkWifiOnlySubtitle": "Pause downloads on mobile data", + "@downloadNetworkWifiOnlySubtitle": { + "description": "Subtitle for 'WiFi only' network mode option" + }, + "downloadSongLinkRegionDesc": "Used as userCountry for SongLink API lookup.", + "@downloadSongLinkRegionDesc": { + "description": "Description in the SongLink region picker" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Title of the folder organization picker bottom sheet" + }, + "snackbarUnsupportedAudioFormat": "Unsupported audio format", + "@snackbarUnsupportedAudioFormat": { + "description": "Snackbar when the audio format is not supported for the requested operation" + }, + "cacheRefresh": "Refresh", + "@cacheRefresh": { + "description": "Tooltip for refresh button on cache management page" } } diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index f3f22790..2352cfac 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -619,7 +619,7 @@ class _AlbumScreenState extends ConsumerState { size: 22, color: allLoved ? Colors.redAccent : Colors.white, ), - tooltip: allLoved ? 'Remove from Loved' : 'Love All', + tooltip: allLoved ? context.l10n.trackOptionRemoveFromLoved : context.l10n.tooltipLoveAll, padding: EdgeInsets.zero, ), ); @@ -642,7 +642,7 @@ class _AlbumScreenState extends ConsumerState { ? null : () => showAddTracksToPlaylistSheet(context, ref, _tracks!), icon: const Icon(Icons.add, size: 22, color: Colors.white), - tooltip: 'Add to Playlist', + tooltip: context.l10n.tooltipAddToPlaylist, padding: EdgeInsets.zero, ), ); @@ -660,7 +660,11 @@ class _AlbumScreenState extends ConsumerState { } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Removed ${tracks.length} tracks from Loved')), + SnackBar( + content: Text( + context.l10n.snackbarRemovedTracksFromLoved(tracks.length), + ), + ), ); } } else { @@ -673,7 +677,11 @@ class _AlbumScreenState extends ConsumerState { } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Added $addedCount tracks to Loved')), + SnackBar( + content: Text( + context.l10n.snackbarAddedTracksToLoved(addedCount), + ), + ), ); } } diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 468d7aff..04e5dcc0 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -363,7 +363,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { if (tracks.isEmpty) { return Scaffold( appBar: AppBar(title: Text(widget.albumName)), - body: Center(child: Text('No tracks found for this album')), + body: Center(child: Text(context.l10n.noTracksFoundForAlbum)), ); } diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 5663caa3..d7c2a8eb 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -816,7 +816,7 @@ class _HomeTabState extends ConsumerState const SizedBox(height: 12), CheckboxListTile( contentPadding: EdgeInsets.zero, - title: const Text('Skip already downloaded songs'), + title: Text(l10n.homeSkipAlreadyDownloaded), value: skipDownloaded, onChanged: (value) { setDialogState(() { @@ -1750,7 +1750,7 @@ class _HomeTabState extends ConsumerState ), ListTile( leading: Icon(Icons.album, color: colorScheme.onSurfaceVariant), - title: const Text('Go to Album'), + title: Text(context.l10n.homeGoToAlbum), onTap: () { Navigator.pop(context); _navigateToTrackAlbum(item); @@ -1824,7 +1824,7 @@ class _HomeTabState extends ConsumerState } else { ScaffoldMessenger.of( context, - ).showSnackBar(const SnackBar(content: Text('Album info not available'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.homeAlbumInfoUnavailable))); } } diff --git a/lib/screens/library_tracks_folder_screen.dart b/lib/screens/library_tracks_folder_screen.dart index d6d1fc5e..c7ba7768 100644 --- a/lib/screens/library_tracks_folder_screen.dart +++ b/lib/screens/library_tracks_folder_screen.dart @@ -850,8 +850,8 @@ class _LibraryTracksFolderScreenState final colorScheme = Theme.of(dialogContext).colorScheme; return AlertDialog( backgroundColor: colorScheme.surfaceContainerHigh, - title: const Text('Download All'), - content: Text('Download ${tracks.length} tracks?'), + title: Text(context.l10n.dialogDownloadAllTitle), + content: Text(context.l10n.dialogDownloadAllMessage(tracks.length)), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), @@ -862,7 +862,7 @@ class _LibraryTracksFolderScreenState Navigator.pop(dialogContext); _downloadAll(tracks); }, - child: const Text('Download'), + child: Text(context.l10n.dialogDownload), ), ], ); diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 9c560546..e5584b34 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -247,7 +247,7 @@ class _LocalAlbumScreenState extends ConsumerState { if (tracks.isEmpty) { return Scaffold( appBar: AppBar(title: Text(widget.albumName)), - body: const Center(child: Text('No tracks found for this album')), + body: Center(child: Text(context.l10n.noTracksFoundForAlbum)), ); } diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index b77fee99..79cb17a8 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -181,25 +181,20 @@ class _MainShellState extends ConsumerState { size: 32, color: colorScheme.primary, ), - title: const Text('Storage Update Required'), - content: const Column( + title: Text(context.l10n.safMigrationTitle), + content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. ' - 'This fixes "permission denied" errors on Android 10+.', - ), - SizedBox(height: 12), - Text( - 'Please select your download folder again to switch to the new storage system.', - ), + Text(context.l10n.safMigrationMessage1), + const SizedBox(height: 12), + Text(context.l10n.safMigrationMessage2), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), - child: const Text('Later'), + child: Text(context.l10n.updateLater), ), FilledButton( onPressed: () async { @@ -219,15 +214,15 @@ class _MainShellState extends ConsumerState { ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Download folder updated to SAF mode'), + SnackBar( + content: Text(context.l10n.safMigrationSuccess), ), ); } } } }, - child: const Text('Select Folder'), + child: Text(context.l10n.setupSelectFolder), ), ], ), diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 37bf14cf..a87676a1 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -482,7 +482,7 @@ class _PlaylistScreenState extends ConsumerState { size: 22, color: allLoved ? Colors.redAccent : Colors.white, ), - tooltip: allLoved ? 'Remove from Loved' : 'Love All', + tooltip: allLoved ? context.l10n.trackOptionRemoveFromLoved : context.l10n.tooltipLoveAll, padding: EdgeInsets.zero, ), ); @@ -505,7 +505,7 @@ class _PlaylistScreenState extends ConsumerState { Widget _buildAddToPlaylistButton(BuildContext context) { return _buildCircleButton( icon: Icons.playlist_add, - tooltip: 'Add to Playlist', + tooltip: context.l10n.tooltipAddToPlaylist, onPressed: _tracks.isEmpty ? null : () => showAddTracksToPlaylistSheet(context, ref, _tracks), @@ -520,8 +520,8 @@ class _PlaylistScreenState extends ConsumerState { final colorScheme = Theme.of(dialogContext).colorScheme; return AlertDialog( backgroundColor: colorScheme.surfaceContainerHigh, - title: const Text('Download All'), - content: Text('Download ${_tracks.length} tracks?'), + title: Text(context.l10n.dialogDownloadAllTitle), + content: Text(context.l10n.dialogDownloadAllMessage(_tracks.length)), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), @@ -532,7 +532,7 @@ class _PlaylistScreenState extends ConsumerState { Navigator.pop(dialogContext); _downloadAll(context); }, - child: const Text('Download'), + child: Text(context.l10n.dialogDownload), ), ], ); @@ -552,7 +552,11 @@ class _PlaylistScreenState extends ConsumerState { } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Removed ${tracks.length} tracks from Loved')), + SnackBar( + content: Text( + context.l10n.snackbarRemovedTracksFromLoved(tracks.length), + ), + ), ); } } else { @@ -565,7 +569,11 @@ class _PlaylistScreenState extends ConsumerState { } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Added $addedCount tracks to Loved')), + SnackBar( + content: Text( + context.l10n.snackbarAddedTracksToLoved(addedCount), + ), + ), ); } } diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 09b2f656..42118769 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/services/cover_cache_manager.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; @@ -52,7 +53,7 @@ class _SearchScreenState extends ConsumerState { .addToQueue(track, settings.defaultService); ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); } @override diff --git a/lib/screens/settings/cache_management_page.dart b/lib/screens/settings/cache_management_page.dart index edb3eed3..e8b43677 100644 --- a/lib/screens/settings/cache_management_page.dart +++ b/lib/screens/settings/cache_management_page.dart @@ -56,7 +56,7 @@ class _CacheManagementPageState extends ConsumerState { setState(() => _isLoading = false); ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Error: $e'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); } } @@ -282,7 +282,7 @@ class _CacheManagementPageState extends ConsumerState { if (!mounted) return; ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Error: $e'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); } finally { if (mounted) { setState(() => _busyAction = null); @@ -394,7 +394,7 @@ class _CacheManagementPageState extends ConsumerState { ), actions: [ IconButton( - tooltip: 'Refresh', + tooltip: context.l10n.cacheRefresh, onPressed: _isBusy ? null : _refreshOverview, icon: const Icon(Icons.refresh), ), diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index e856c0a2..c502d142 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -376,7 +376,7 @@ class _DownloadSettingsPageState extends ConsumerState { title: context.l10n.downloadAskBeforeDownload, subtitle: isBuiltInService ? context.l10n.downloadAskQualitySubtitle - : 'Select a built-in service to enable', + : context.l10n.downloadSelectServiceToEnable, value: settings.askQualityBeforeDownload, enabled: isBuiltInService, onChanged: (value) => ref @@ -413,7 +413,7 @@ class _DownloadSettingsPageState extends ConsumerState { // Lossy 320kbps option (Tidal only) - downloads M4A, converts to MP3/Opus if (isTidalService) _QualityOption( - title: 'Lossy 320kbps', + title: context.l10n.downloadLossy320, subtitle: _getTidalHighFormatLabel( settings.tidalHighFormat, ), @@ -426,7 +426,7 @@ class _DownloadSettingsPageState extends ConsumerState { if (isTidalService && settings.audioQuality == 'HIGH') SettingsItem( icon: Icons.tune, - title: 'Lossy Format', + title: context.l10n.downloadLossyFormat, subtitle: _getTidalHighFormatLabel( settings.tidalHighFormat, ), @@ -451,7 +451,7 @@ class _DownloadSettingsPageState extends ConsumerState { const SizedBox(width: 8), Expanded( child: Text( - 'Select Tidal or Qobuz above to configure quality', + context.l10n.downloadSelectTidalQobuz, style: Theme.of(context).textTheme.bodySmall ?.copyWith( color: colorScheme.onSurfaceVariant, @@ -504,7 +504,7 @@ class _DownloadSettingsPageState extends ConsumerState { title: context.l10n.optionsEmbedLyrics, subtitle: settings.embedMetadata ? context.l10n.optionsEmbedLyricsSubtitle - : 'Disabled while Embed Metadata is turned off', + : context.l10n.downloadEmbedLyricsDisabled, value: settings.embedLyrics, enabled: settings.embedMetadata, onChanged: (value) => ref @@ -528,7 +528,7 @@ class _DownloadSettingsPageState extends ConsumerState { ), SettingsItem( icon: Icons.source_outlined, - title: 'Lyrics Providers', + title: context.l10n.lyricsProvidersTitle, subtitle: _getLyricsProvidersSubtitle( settings.lyricsProviders, ), @@ -541,10 +541,10 @@ class _DownloadSettingsPageState extends ConsumerState { ), SettingsSwitchItem( icon: Icons.translate_outlined, - title: 'Netease: Include Translation', + title: context.l10n.downloadNeteaseIncludeTranslation, subtitle: settings.lyricsIncludeTranslationNetease - ? 'Append translated lyrics when available' - : 'Use original lyrics only', + ? context.l10n.downloadNeteaseIncludeTranslationEnabled + : context.l10n.downloadNeteaseIncludeTranslationDisabled, value: settings.lyricsIncludeTranslationNetease, onChanged: (value) => ref .read(settingsProvider.notifier) @@ -552,10 +552,10 @@ class _DownloadSettingsPageState extends ConsumerState { ), SettingsSwitchItem( icon: Icons.text_fields_outlined, - title: 'Netease: Include Romanization', + title: context.l10n.downloadNeteaseIncludeRomanization, subtitle: settings.lyricsIncludeRomanizationNetease - ? 'Append romanized lyrics when available' - : 'Disabled', + ? context.l10n.downloadNeteaseIncludeRomanizationEnabled + : context.l10n.downloadNeteaseIncludeRomanizationDisabled, value: settings.lyricsIncludeRomanizationNetease, onChanged: (value) => ref .read(settingsProvider.notifier) @@ -563,10 +563,10 @@ class _DownloadSettingsPageState extends ConsumerState { ), SettingsSwitchItem( icon: Icons.record_voice_over_outlined, - title: 'Apple/QQ Multi-Person Word-by-Word', + title: context.l10n.downloadAppleQqMultiPerson, subtitle: settings.lyricsMultiPersonWordByWord - ? 'Enable v1/v2 speaker and [bg:] tags' - : 'Simplified word-by-word formatting', + ? context.l10n.downloadAppleQqMultiPersonEnabled + : context.l10n.downloadAppleQqMultiPersonDisabled, value: settings.lyricsMultiPersonWordByWord, onChanged: (value) => ref .read(settingsProvider.notifier) @@ -574,9 +574,9 @@ class _DownloadSettingsPageState extends ConsumerState { ), SettingsItem( icon: Icons.language_outlined, - title: 'Musixmatch Language', + title: context.l10n.downloadMusixmatchLanguage, subtitle: settings.musixmatchLanguage.isEmpty - ? 'Auto (original)' + ? context.l10n.downloadMusixmatchLanguageAuto : settings.musixmatchLanguage.toUpperCase(), onTap: () => _showMusixmatchLanguagePicker( context, @@ -622,8 +622,8 @@ class _DownloadSettingsPageState extends ConsumerState { icon: Icons.library_music_outlined, title: context.l10n.downloadSeparateSinglesFolder, subtitle: settings.separateSingles - ? 'Albums/ and Singles/ folders' - : 'All files in same structure', + ? context.l10n.downloadSeparateSinglesEnabled + : context.l10n.downloadSeparateSinglesDisabled, value: settings.separateSingles, onChanged: (value) => ref .read(settingsProvider.notifier) @@ -670,9 +670,9 @@ class _DownloadSettingsPageState extends ConsumerState { .read(settingsProvider.notifier) .setUseAlbumArtistForFolders(value), ), - SettingsItem( + SettingsItem( icon: Icons.filter_alt_outlined, - title: 'Artist Name Filters', + title: context.l10n.downloadArtistNameFilters, subtitle: _getArtistFolderFilterSubtitle( context, usePrimaryArtistOnly: settings.usePrimaryArtistOnly, @@ -707,28 +707,16 @@ class _DownloadSettingsPageState extends ConsumerState { if (_artistFolderFiltersExpanded) SettingsSwitchItem( icon: Icons.group_remove_outlined, - title: 'Filter contributing artists in Album Artist', + title: context.l10n.downloadFilterContributing, subtitle: settings.filterContributingArtistsInAlbumArtist - ? 'Album Artist metadata uses primary artist only' - : 'Keep full Album Artist metadata value', + ? context.l10n.downloadFilterContributingEnabled + : context.l10n.downloadFilterContributingDisabled, value: settings.filterContributingArtistsInAlbumArtist, onChanged: (value) => ref .read(settingsProvider.notifier) .setFilterContributingArtistsInAlbumArtist(value), showDivider: false, ), - SettingsSwitchItem( - icon: Icons.person_outline, - title: context.l10n.downloadUsePrimaryArtistOnly, - subtitle: settings.usePrimaryArtistOnly - ? context.l10n.downloadUsePrimaryArtistOnlyEnabled - : context.l10n.downloadUsePrimaryArtistOnlyDisabled, - value: settings.usePrimaryArtistOnly, - onChanged: (value) => ref - .read(settingsProvider.notifier) - .setUsePrimaryArtistOnly(value), - showDivider: false, - ), ], ), ), @@ -753,7 +741,7 @@ class _DownloadSettingsPageState extends ConsumerState { ), SettingsItem( icon: Icons.public, - title: 'SongLink Region', + title: context.l10n.downloadSongLinkRegion, subtitle: _getSongLinkRegionLabel(settings.songLinkRegion), onTap: () => _showSongLinkRegionPicker( context, @@ -763,10 +751,10 @@ class _DownloadSettingsPageState extends ConsumerState { ), SettingsSwitchItem( icon: Icons.security_outlined, - title: 'Network compatibility mode', + title: context.l10n.downloadNetworkCompatibilityMode, subtitle: settings.networkCompatibilityMode - ? 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)' - : 'Off: strict HTTPS certificate validation (recommended)', + ? context.l10n.downloadNetworkCompatibilityModeEnabled + : context.l10n.downloadNetworkCompatibilityModeDisabled, value: settings.networkCompatibilityMode, onChanged: (value) { ref @@ -1045,7 +1033,7 @@ class _DownloadSettingsPageState extends ConsumerState { ), const SizedBox(height: 8), Text( - 'Customize how your files are named.', + context.l10n.downloadFilenameDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -1070,7 +1058,7 @@ class _DownloadSettingsPageState extends ConsumerState { const SizedBox(height: 24), Text( - 'Tap to insert tag:', + context.l10n.downloadFilenameInsertTag, style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), @@ -1238,7 +1226,7 @@ class _DownloadSettingsPageState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( - 'Download Location', + context.l10n.setupDownloadLocationTitle, style: Theme.of( ctx, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), @@ -1247,7 +1235,7 @@ class _DownloadSettingsPageState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( - 'Choose storage mode for downloaded files.', + context.l10n.downloadLocationSubtitle, style: Theme.of(ctx).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -1255,8 +1243,8 @@ class _DownloadSettingsPageState extends ConsumerState { ), ListTile( leading: Icon(Icons.folder_special, color: colorScheme.primary), - title: const Text('App folder (non-SAF)'), - subtitle: const Text('Use default Music/SpotiFLAC path'), + title: Text(context.l10n.storageModeAppFolder), + subtitle: Text(context.l10n.storageModeAppFolderSubtitle), trailing: !isSafMode ? const Icon(Icons.check) : null, onTap: () async { Navigator.pop(ctx); @@ -1269,10 +1257,8 @@ class _DownloadSettingsPageState extends ConsumerState { ), ListTile( leading: Icon(Icons.folder_open, color: colorScheme.primary), - title: const Text('SAF folder'), - subtitle: const Text( - 'Pick folder via Android Storage Access Framework', - ), + title: Text(context.l10n.storageModeSaf), + subtitle: Text(context.l10n.storageModeSafSubtitle), trailing: isSafMode ? const Icon(Icons.check) : null, onTap: () async { Navigator.pop(ctx); @@ -1546,7 +1532,7 @@ class _DownloadSettingsPageState extends ConsumerState { }; String _getLyricsProvidersSubtitle(List providers) { - if (providers.isEmpty) return 'None enabled'; + if (providers.isEmpty) return context.l10n.downloadProvidersNoneEnabled; return providers.map((p) => _providerDisplayNames[p] ?? p).join(' > '); } @@ -1645,14 +1631,14 @@ class _DownloadSettingsPageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Musixmatch Language', + context.l10n.downloadMusixmatchLanguage, style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( - 'Set preferred language code (example: en, es, ja). Leave empty for auto.', + context.l10n.downloadMusixmatchLanguageDesc, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -1661,9 +1647,9 @@ class _DownloadSettingsPageState extends ConsumerState { TextField( controller: controller, textInputAction: TextInputAction.done, - decoration: const InputDecoration( - labelText: 'Language code', - hintText: 'auto / en / es / ja', + decoration: InputDecoration( + labelText: context.l10n.downloadMusixmatchLanguageCode, + hintText: context.l10n.downloadMusixmatchLanguageHint, ), ), const SizedBox(height: 16), @@ -1682,7 +1668,7 @@ class _DownloadSettingsPageState extends ConsumerState { .setMusixmatchLanguage(''); Navigator.pop(context); }, - child: const Text('Auto'), + child: Text(context.l10n.downloadMusixmatchAuto), ), const SizedBox(width: 8), FilledButton( @@ -1739,7 +1725,7 @@ class _DownloadSettingsPageState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( - 'Lossy 320kbps Format', + context.l10n.downloadLossy320Format, style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), @@ -1748,7 +1734,7 @@ class _DownloadSettingsPageState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.', + context.l10n.downloadLossy320FormatDesc, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -1756,8 +1742,8 @@ class _DownloadSettingsPageState extends ConsumerState { ), ListTile( leading: const Icon(Icons.audiotrack), - title: const Text('MP3 320kbps'), - subtitle: const Text('Best compatibility, ~10MB per track'), + title: Text(context.l10n.downloadLossyMp3), + subtitle: Text(context.l10n.downloadLossyMp3Subtitle), trailing: current == 'mp3_320' ? Icon(Icons.check, color: colorScheme.primary) : null, @@ -1770,8 +1756,8 @@ class _DownloadSettingsPageState extends ConsumerState { ), ListTile( leading: const Icon(Icons.graphic_eq), - title: const Text('Opus 256kbps'), - subtitle: const Text('Best quality Opus, ~8MB per track'), + title: Text(context.l10n.downloadLossyOpus256), + subtitle: Text(context.l10n.downloadLossyOpus256Subtitle), trailing: current == 'opus_256' ? Icon(Icons.check, color: colorScheme.primary) : null, @@ -1784,8 +1770,8 @@ class _DownloadSettingsPageState extends ConsumerState { ), ListTile( leading: const Icon(Icons.graphic_eq), - title: const Text('Opus 128kbps'), - subtitle: const Text('Smallest size, ~4MB per track'), + title: Text(context.l10n.downloadLossyOpus128), + subtitle: Text(context.l10n.downloadLossyOpus128Subtitle), trailing: current == 'opus_128' ? Icon(Icons.check, color: colorScheme.primary) : null, @@ -1842,7 +1828,7 @@ class _DownloadSettingsPageState extends ConsumerState { ListTile( leading: const Icon(Icons.signal_cellular_alt), title: Text(context.l10n.settingsDownloadNetworkAny), - subtitle: const Text('WiFi + Mobile Data'), + subtitle: Text(context.l10n.downloadNetworkAnySubtitle), trailing: current == 'any' ? Icon(Icons.check, color: colorScheme.primary) : null, @@ -1856,7 +1842,7 @@ class _DownloadSettingsPageState extends ConsumerState { ListTile( leading: const Icon(Icons.wifi), title: Text(context.l10n.settingsDownloadNetworkWifiOnly), - subtitle: const Text('Pause downloads on mobile data'), + subtitle: Text(context.l10n.downloadNetworkWifiOnlySubtitle), trailing: current == 'wifi_only' ? Icon(Icons.check, color: colorScheme.primary) : null, @@ -1897,17 +1883,17 @@ class _DownloadSettingsPageState extends ConsumerState { children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - 'SongLink Region', - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), + child: Text( + context.l10n.downloadSongLinkRegion, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - 'Used as userCountry for SongLink API lookup.', + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.downloadSongLinkRegionDesc, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -1968,12 +1954,12 @@ class _DownloadSettingsPageState extends ConsumerState { children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - 'Folder Organization', - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), + child: Text( + context.l10n.downloadFolderOrganization, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), diff --git a/lib/screens/settings/extension_detail_page.dart b/lib/screens/settings/extension_detail_page.dart index 0a702946..c6e00b55 100644 --- a/lib/screens/settings/extension_detail_page.dart +++ b/lib/screens/settings/extension_detail_page.dart @@ -801,7 +801,7 @@ class _SettingItemState extends State<_SettingItem> { Future _invokeAction(BuildContext context) async { if (widget.setting.action == null) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No action defined for this button')), + SnackBar(content: Text(context.l10n.snackbarNoActionDefined)), ); return; } @@ -834,7 +834,7 @@ class _SettingItemState extends State<_SettingItem> { if (context.mounted) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Error: $e'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); } } finally { if (mounted) { diff --git a/lib/screens/settings/lyrics_provider_priority_page.dart b/lib/screens/settings/lyrics_provider_priority_page.dart index 426d82c8..3e3537a2 100644 --- a/lib/screens/settings/lyrics_provider_priority_page.dart +++ b/lib/screens/settings/lyrics_provider_priority_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/widgets/priority_settings_scaffold.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -55,18 +56,18 @@ class _LyricsProviderPriorityPageState return PrioritySettingsScaffold( hasChanges: _hasChanges, - title: 'Lyrics Providers', - description: - 'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.', - infoText: - 'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.', + title: context.l10n.lyricsProvidersTitle, + description: context.l10n.lyricsProvidersDescription, + infoText: context.l10n.lyricsProvidersInfoText, onSave: _saveChanges, onConfirmDiscard: _confirmDiscard, slivers: [ if (_enabledProviders.isNotEmpty) SliverToBoxAdapter( child: SettingsSectionHeader( - title: 'Enabled (${_enabledProviders.length})', + title: context.l10n.lyricsProvidersEnabledSection( + _enabledProviders.length, + ), ), ), if (_enabledProviders.isNotEmpty) @@ -76,7 +77,7 @@ class _LyricsProviderPriorityPageState itemCount: _enabledProviders.length, itemBuilder: (context, index) { final id = _enabledProviders[index]; - final info = _getLyricsProviderInfo(id); + final info = _getLyricsProviderInfo(id, context); return _EnabledProviderItem( key: ValueKey(id), providerId: id, @@ -99,7 +100,9 @@ class _LyricsProviderPriorityPageState if (disabled.isNotEmpty) SliverToBoxAdapter( child: SettingsSectionHeader( - title: 'Disabled (${disabled.length})', + title: context.l10n.lyricsProvidersDisabledSection( + disabled.length, + ), ), ), if (disabled.isNotEmpty) @@ -108,7 +111,7 @@ class _LyricsProviderPriorityPageState sliver: SliverList( delegate: SliverChildBuilderDelegate((context, index) { final id = disabled[index]; - final info = _getLyricsProviderInfo(id); + final info = _getLyricsProviderInfo(id, context); return _DisabledProviderItem( key: ValueKey(id), providerId: id, @@ -130,8 +133,8 @@ class _LyricsProviderPriorityPageState void _disableProvider(String id) { if (_enabledProviders.length <= 1) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('At least one provider must remain enabled'), + SnackBar( + content: Text(context.l10n.lyricsProvidersAtLeastOne), ), ); return; @@ -150,7 +153,7 @@ class _LyricsProviderPriorityPageState }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Lyrics provider priority saved')), + SnackBar(content: Text(context.l10n.lyricsProvidersSaved)), ); } } @@ -159,16 +162,16 @@ class _LyricsProviderPriorityPageState final result = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Discard changes?'), - content: const Text('You have unsaved changes that will be lost.'), + title: Text(context.l10n.dialogDiscardChanges), + content: Text(context.l10n.lyricsProvidersDiscardContent), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Discard'), + child: Text(context.l10n.dialogDiscard), ), ], ), @@ -176,48 +179,51 @@ class _LyricsProviderPriorityPageState return result ?? false; } - static _LyricsProviderInfo _getLyricsProviderInfo(String id) { + static _LyricsProviderInfo _getLyricsProviderInfo( + String id, + BuildContext context, + ) { switch (id) { case 'spotify_api': return _LyricsProviderInfo( name: 'Spotify Lyrics API', - description: 'Spotify-sourced synced lyrics via community API', + description: context.l10n.lyricsProviderSpotifyApiDesc, icon: Icons.music_note_outlined, ); case 'lrclib': return _LyricsProviderInfo( name: 'LRCLIB', - description: 'Open-source synced lyrics database', + description: context.l10n.lyricsProviderLrclibDesc, icon: Icons.subtitles_outlined, ); case 'netease': return _LyricsProviderInfo( name: 'Netease', - description: 'NetEase Cloud Music (good for Asian songs)', + description: context.l10n.lyricsProviderNeteaseDesc, icon: Icons.cloud_outlined, ); case 'musixmatch': return _LyricsProviderInfo( name: 'Musixmatch', - description: 'Largest lyrics database (multi-language)', + description: context.l10n.lyricsProviderMusixmatchDesc, icon: Icons.translate, ); case 'apple_music': return _LyricsProviderInfo( name: 'Apple Music', - description: 'Word-by-word synced lyrics (via proxy)', + description: context.l10n.lyricsProviderAppleMusicDesc, icon: Icons.music_note, ); case 'qqmusic': return _LyricsProviderInfo( name: 'QQ Music', - description: 'QQ Music (good for Chinese songs, via proxy)', + description: context.l10n.lyricsProviderQqMusicDesc, icon: Icons.queue_music, ); default: return _LyricsProviderInfo( name: id, - description: 'Extension provider', + description: context.l10n.lyricsProviderExtensionDesc, icon: Icons.extension, ); } diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index 140f933b..7e66fc62 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -107,8 +107,8 @@ class SettingsTab extends ConsumerWidget { ), SettingsItem( icon: Icons.favorite_outline, - title: 'Donate', - subtitle: 'Support SpotiFLAC-Mobile development', + title: l10n.settingsDonate, + subtitle: l10n.settingsDonateSubtitle, onTap: () => _navigateTo(context, const DonatePage()), showDivider: false, ), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 4394592f..95f65c44 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -1731,13 +1731,13 @@ class _TrackMetadataScreenState extends ConsumerState { ); success = ok; if (!ok) { - error = 'Failed to write back to storage'; + error = context.l10n.snackbarFailedToWriteStorage; } } else { success = true; } } else { - error = result['error']?.toString() ?? 'Failed to embed lyrics'; + error = result['error']?.toString() ?? context.l10n.snackbarFailedToEmbedLyrics; } } else if (isMp3 || isOpus) { final metadata = _buildFallbackMetadata(); @@ -1783,7 +1783,7 @@ class _TrackMetadataScreenState extends ConsumerState { } if (ffmpegResult == null) { - error = 'Failed to embed lyrics'; + error = context.l10n.snackbarFailedToEmbedLyrics; } else if (_isSafFile) { final ok = await PlatformBridge.writeTempToSaf( ffmpegResult, @@ -1791,13 +1791,13 @@ class _TrackMetadataScreenState extends ConsumerState { ); success = ok; if (!ok) { - error = 'Failed to write back to storage'; + error = context.l10n.snackbarFailedToWriteStorage; } } else { success = true; } } else { - error = 'Unsupported audio format'; + error = context.l10n.snackbarUnsupportedAudioFormat; } if (mounted) { @@ -1812,7 +1812,7 @@ class _TrackMetadataScreenState extends ConsumerState { } else { setState(() => _isEmbedding = false); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(error ?? 'Failed to embed lyrics')), + SnackBar(content: Text(error ?? context.l10n.snackbarFailedToEmbedLyrics)), ); } } @@ -1821,7 +1821,7 @@ class _TrackMetadataScreenState extends ConsumerState { setState(() => _isEmbedding = false); ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Error: $e'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); } } finally { if (coverPath != null) { @@ -2271,7 +2271,7 @@ class _TrackMetadataScreenState extends ConsumerState { SnackBar( content: Text( context.l10n.trackSaveFailed( - 'Failed to write back to storage', + context.l10n.snackbarFailedToWriteStorage, ), ), ), @@ -2873,7 +2873,7 @@ class _TrackMetadataScreenState extends ConsumerState { // Show loading indicator ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Loading CUE sheet...')), + SnackBar(content: Text(context.l10n.snackbarLoadingCueSheet)), ); try { @@ -3644,7 +3644,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (saved == true && mounted) { ScaffoldMessenger.of(this.context).showSnackBar( - const SnackBar(content: Text('Metadata saved successfully')), + SnackBar(content: Text(this.context.l10n.snackbarMetadataSaved)), ); // Re-read metadata from file to refresh the display try { @@ -4066,7 +4066,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { if (!mounted) return; ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Failed to pick cover: $e'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); } } @@ -4290,7 +4290,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { if (mounted) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Failed to save metadata: $e'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); } } finally { if (mounted) setState(() => _saving = false); From adc74741ce30be6c8c3233b48433ead2f2b69de1 Mon Sep 17 00:00:00 2001 From: ViscousPot Date: Sat, 14 Mar 2026 01:38:32 +0000 Subject: [PATCH 041/145] Update library_tracks_folder_screen.dart --- lib/screens/library_tracks_folder_screen.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/screens/library_tracks_folder_screen.dart b/lib/screens/library_tracks_folder_screen.dart index d6d1fc5e..4e24fdba 100644 --- a/lib/screens/library_tracks_folder_screen.dart +++ b/lib/screens/library_tracks_folder_screen.dart @@ -39,6 +39,7 @@ class _LibraryTracksFolderScreenState bool _isSelectionMode = false; final Set _selectedKeys = {}; + UserPlaylistCollection? playlist; @override void initState() { @@ -243,7 +244,6 @@ class _LibraryTracksFolderScreenState final colorScheme = Theme.of(context).colorScheme; ref.watch(localLibraryProvider.select((s) => s.items)); final localState = ref.read(localLibraryProvider); - final UserPlaylistCollection? playlist; final List entries; switch (widget.mode) { @@ -873,6 +873,7 @@ class _LibraryTracksFolderScreenState void _downloadAll(List tracks) { if (tracks.isEmpty) return; final settings = ref.read(settingsProvider); + final playlistName = widget.mode == LibraryTracksFolderMode.playlist ? playlist?.name ?? context.l10n.collectionPlaylist : null; if (settings.askQualityBeforeDownload) { DownloadServicePicker.show( context, @@ -885,7 +886,7 @@ class _LibraryTracksFolderScreenState onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, service, qualityOverride: quality); + .addMultipleToQueue(tracks, service, qualityOverride: quality, playlistName: playlistName); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -899,7 +900,7 @@ class _LibraryTracksFolderScreenState } else { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, settings.defaultService); + .addMultipleToQueue(tracks, settings.defaultService, playlistName: playlistName); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), From 34d22f783c4314008c5c13505cf2680c12e48a54 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 14 Mar 2026 13:24:30 +0700 Subject: [PATCH 042/145] feat: add store registry URL management, port iOS handlers, and clean up store UI Add set/get/clear store registry URL method channel handlers on Android, iOS, and Go backend so users can configure a custom extension repository. Store tab now shows a setup screen when no registry URL is configured, with a cleaner layout (removed redundant description and helper text) and visible TextField borders for dark theme. Minor comment and formatting cleanups across several files. --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 19 + go_backend/cover.go | 1 - go_backend/exports.go | 39 ++ go_backend/extension_store.go | 118 ++++- ios/Runner/AppDelegate.swift | 17 + lib/providers/local_library_provider.dart | 1 - lib/providers/store_provider.dart | 82 ++- lib/screens/main_shell.dart | 2 - lib/screens/store_tab.dart | 479 ++++++++++++------ lib/screens/track_metadata_screen.dart | 3 - lib/services/library_database.dart | 2 - lib/services/platform_bridge.dart | 16 + 12 files changed, 614 insertions(+), 165 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 50e6eda0..aca778de 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -3064,6 +3064,25 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } + "setStoreRegistryUrl" -> { + val registryUrl = call.argument("registry_url") ?: "" + withContext(Dispatchers.IO) { + Gobackend.setStoreRegistryURLJSON(registryUrl) + } + result.success(null) + } + "getStoreRegistryUrl" -> { + val response = withContext(Dispatchers.IO) { + Gobackend.getStoreRegistryURLJSON() + } + result.success(response) + } + "clearStoreRegistryUrl" -> { + withContext(Dispatchers.IO) { + Gobackend.clearStoreRegistryURLJSON() + } + result.success(null) + } "getStoreExtensions" -> { val forceRefresh = call.argument("force_refresh") ?: false val response = withContext(Dispatchers.IO) { diff --git a/go_backend/cover.go b/go_backend/cover.go index fd0ed822..10c89963 100644 --- a/go_backend/cover.go +++ b/go_backend/cover.go @@ -104,7 +104,6 @@ func upgradeDeezerCover(coverURL string) string { return coverURL } - // Replace any size pattern with 1800x1800 upgraded := deezerSizeRegex.ReplaceAllString(coverURL, "/1800x1800-000000-80-0-0.jpg") if upgraded != coverURL { GoLog("[Cover] Deezer: upgraded to 1800x1800") diff --git a/go_backend/exports.go b/go_backend/exports.go index ec757a2d..44fa7831 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -2930,6 +2930,45 @@ func InitExtensionStoreJSON(cacheDir string) error { return nil } +func SetStoreRegistryURLJSON(registryURL string) error { + store := GetExtensionStore() + if store == nil { + return fmt.Errorf("extension store not initialized") + } + + resolved, err := ResolveRegistryURL(registryURL) + if err != nil { + return err + } + + if err := requireHTTPSURL(resolved, "registry"); err != nil { + return err + } + + store.SetRegistryURL(resolved) + return nil +} + +func ClearStoreRegistryURLJSON() error { + store := GetExtensionStore() + if store == nil { + return fmt.Errorf("extension store not initialized") + } + + store.SetRegistryURL("") + store.ClearCache() + return nil +} + +func GetStoreRegistryURLJSON() (string, error) { + store := GetExtensionStore() + if store == nil { + return "", fmt.Errorf("extension store not initialized") + } + + return store.GetRegistryURL(), nil +} + func GetStoreExtensionsJSON(forceRefresh bool) (string, error) { store := GetExtensionStore() if store == nil { diff --git a/go_backend/extension_store.go b/go_backend/extension_store.go index 2fa17e84..90dbf61a 100644 --- a/go_backend/extension_store.go +++ b/go_backend/extension_store.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "path/filepath" + "strings" "sync" "time" ) @@ -129,9 +130,8 @@ var ( ) const ( - defaultRegistryURL = "https://raw.githubusercontent.com/zarzet/SpotiFLAC-Extension/main/registry.json" - cacheTTL = 30 * time.Minute - cacheFileName = "store_cache.json" + cacheTTL = 30 * time.Minute + cacheFileName = "store_cache.json" ) func InitExtensionStore(cacheDir string) *ExtensionStore { @@ -140,7 +140,7 @@ func InitExtensionStore(cacheDir string) *ExtensionStore { if extensionStore == nil { extensionStore = &ExtensionStore{ - registryURL: defaultRegistryURL, + registryURL: "", // No default - user must provide a registry URL cacheDir: cacheDir, cacheTTL: cacheTTL, } @@ -149,6 +149,36 @@ func InitExtensionStore(cacheDir string) *ExtensionStore { return extensionStore } +// SetRegistryURL updates the registry URL and clears the in-memory cache +// so the next fetch will use the new URL. Disk cache is also cleared. +func (s *ExtensionStore) SetRegistryURL(registryURL string) { + s.cacheMu.Lock() + defer s.cacheMu.Unlock() + + if s.registryURL == registryURL { + return + } + + s.registryURL = registryURL + s.cache = nil + s.cacheTime = time.Time{} + + // Clear disk cache since it's from a different registry + if s.cacheDir != "" { + cachePath := filepath.Join(s.cacheDir, cacheFileName) + os.Remove(cachePath) + } + + LogInfo("ExtensionStore", "Registry URL updated to: %s", registryURL) +} + +// GetRegistryURL returns the currently configured registry URL. +func (s *ExtensionStore) GetRegistryURL() string { + s.cacheMu.RLock() + defer s.cacheMu.RUnlock() + return s.registryURL +} + func GetExtensionStore() *ExtensionStore { extensionStoreMu.Lock() defer extensionStoreMu.Unlock() @@ -206,6 +236,11 @@ func (s *ExtensionStore) FetchRegistry(forceRefresh bool) (*StoreRegistry, error s.cacheMu.Lock() defer s.cacheMu.Unlock() + // Check if a registry URL has been configured + if s.registryURL == "" { + return nil, fmt.Errorf("no registry URL configured. Please add a repository URL first") + } + if !forceRefresh && s.cache != nil && time.Since(s.cacheTime) < s.cacheTTL { LogDebug("ExtensionStore", "Using cached registry (%d extensions)", len(s.cache.Extensions)) return s.cache, nil @@ -336,6 +371,81 @@ func (s *ExtensionStore) DownloadExtension(extensionID string, destPath string) return nil } +// ResolveRegistryURL normalises a user-supplied URL into a direct registry.json URL. +// +// Accepted formats: +// - https://raw.githubusercontent.com/owner/repo//registry.json → returned as-is +// - https://github.com/owner/repo (with optional trailing path / .git) → resolved via +// the GitHub API to discover the default branch, then converted to the raw URL +// - Any other HTTPS URL → returned as-is (assumed to be a direct link) +func ResolveRegistryURL(input string) (string, error) { + input = strings.TrimSpace(input) + if input == "" { + return "", fmt.Errorf("registry URL is empty") + } + + // Already a fully-qualified raw URL – keep it. + if strings.Contains(input, "raw.githubusercontent.com") { + return input, nil + } + + // Try to match https://github.com//[/...] + const ghPrefix = "https://github.com/" + if !strings.HasPrefix(input, ghPrefix) { + // Also accept http:// and upgrade silently. + const ghPrefixHTTP = "http://github.com/" + if strings.HasPrefix(input, ghPrefixHTTP) { + input = "https://github.com/" + input[len(ghPrefixHTTP):] + } else { + // Not a GitHub URL – return as-is. + return input, nil + } + } + + path := input[len(ghPrefix):] + parts := strings.SplitN(path, "/", 3) // owner, repo, [rest] + if len(parts) < 2 || parts[0] == "" || parts[1] == "" { + return "", fmt.Errorf("invalid GitHub URL: expected github.com//") + } + owner := parts[0] + repo := strings.TrimSuffix(parts[1], ".git") + + branch := resolveGitHubDefaultBranch(owner, repo) + + resolved := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/registry.json", owner, repo, branch) + LogInfo("ExtensionStore", "Resolved %s → %s (branch: %s)", input, resolved, branch) + return resolved, nil +} + +// resolveGitHubDefaultBranch calls the GitHub API to discover the repository's +// default branch. Falls back to "main" on any error. +func resolveGitHubDefaultBranch(owner, repo string) string { + apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s", owner, repo) + client := NewHTTPClientWithTimeout(10 * time.Second) + + resp, err := client.Get(apiURL) + if err != nil { + LogWarn("ExtensionStore", "GitHub API request failed for %s/%s: %v – falling back to main", owner, repo, err) + return "main" + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + LogWarn("ExtensionStore", "GitHub API returned %d for %s/%s – falling back to main", resp.StatusCode, owner, repo) + return "main" + } + + var info struct { + DefaultBranch string `json:"default_branch"` + } + if err := json.NewDecoder(resp.Body).Decode(&info); err != nil || info.DefaultBranch == "" { + LogWarn("ExtensionStore", "Could not parse default_branch for %s/%s – falling back to main", owner, repo) + return "main" + } + + return info.DefaultBranch +} + func requireHTTPSURL(rawURL string, context string) error { if rawURL == "" { return fmt.Errorf("%s URL is empty", context) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 586ffbf6..320efb5a 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -791,6 +791,23 @@ import Gobackend // Import Go framework if let error = error { throw error } return nil + case "setStoreRegistryUrl": + let args = call.arguments as! [String: Any] + let registryUrl = args["registry_url"] as? String ?? "" + GobackendSetStoreRegistryURLJSON(registryUrl, &error) + if let error = error { throw error } + return nil + + case "getStoreRegistryUrl": + let response = GobackendGetStoreRegistryURLJSON(&error) + if let error = error { throw error } + return response + + case "clearStoreRegistryUrl": + GobackendClearStoreRegistryURLJSON(&error) + if let error = error { throw error } + return nil + case "getStoreExtensions": let args = call.arguments as! [String: Any] let forceRefresh = args["force_refresh"] as? Bool ?? false diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index 1dd10dd7..8c36a4f6 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -399,7 +399,6 @@ class LocalLibraryNotifier extends Notifier { return; } - // Parse incremental scan result // SAF returns 'files' and 'removedUris', non-SAF returns 'scanned' and 'deletedPaths' final scannedList = (result['files'] as List?) ?? diff --git a/lib/providers/store_provider.dart b/lib/providers/store_provider.dart index e967825a..bac55c5c 100644 --- a/lib/providers/store_provider.dart +++ b/lib/providers/store_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/logger.dart'; @@ -6,6 +7,7 @@ import 'package:spotiflac_android/providers/extension_provider.dart'; final _log = AppLogger('StoreProvider'); final RegExp _leadingVersionPrefix = RegExp(r'^v'); +const _registryUrlPrefKey = 'store_registry_url'; int compareVersions(String v1, String v2) { final parts1 = v1.replaceAll(_leadingVersionPrefix, '').split('.'); @@ -125,6 +127,7 @@ class StoreState { final String? downloadingId; final String? error; final bool isInitialized; + final String registryUrl; const StoreState({ this.extensions = const [], @@ -135,8 +138,12 @@ class StoreState { this.downloadingId, this.error, this.isInitialized = false, + this.registryUrl = '', }); + /// Whether a registry URL has been configured by the user. + bool get hasRegistryUrl => registryUrl.isNotEmpty; + StoreState copyWith({ List? extensions, String? selectedCategory, @@ -149,6 +156,7 @@ class StoreState { String? error, bool clearError = false, bool? isInitialized, + String? registryUrl, }) { return StoreState( extensions: extensions ?? this.extensions, @@ -159,6 +167,7 @@ class StoreState { downloadingId: clearDownloadingId ? null : (downloadingId ?? this.downloadingId), error: clearError ? null : (error ?? this.error), isInitialized: isInitialized ?? this.isInitialized, + registryUrl: registryUrl ?? this.registryUrl, ); } @@ -201,15 +210,84 @@ class StoreNotifier extends Notifier { try { await PlatformBridge.initExtensionStore(cacheDir); - await refresh(); + + // Load saved registry URL from SharedPreferences + final prefs = await SharedPreferences.getInstance(); + final savedUrl = prefs.getString(_registryUrlPrefKey) ?? ''; + + if (savedUrl.isNotEmpty) { + await PlatformBridge.setStoreRegistryUrl(savedUrl); + state = state.copyWith(registryUrl: savedUrl); + await refresh(); + } + state = state.copyWith(isInitialized: true, isLoading: false); - _log.i('Extension store initialized'); + _log.i('Extension store initialized (registryUrl: ${savedUrl.isEmpty ? "not set" : savedUrl})'); } catch (e) { _log.e('Failed to initialize store: $e'); state = state.copyWith(isLoading: false, error: e.toString()); } } + /// Sets the registry URL, saves it, and refreshes the store. + /// The Go backend handles URL normalisation (GitHub repo → raw URL, branch detection). + Future setRegistryUrl(String url) async { + final trimmed = url.trim(); + if (trimmed.isEmpty) { + state = state.copyWith(error: 'Please enter a valid URL'); + return; + } + + state = state.copyWith(isLoading: true, clearError: true); + + try { + // Go backend resolves GitHub URLs (detects default branch) and validates HTTPS. + await PlatformBridge.setStoreRegistryUrl(trimmed); + + // Read back the resolved URL (may differ from input after normalisation). + final resolvedUrl = await PlatformBridge.getStoreRegistryUrl(); + + // Persist to SharedPreferences + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_registryUrlPrefKey, resolvedUrl); + + state = state.copyWith( + registryUrl: resolvedUrl, + extensions: const [], // Clear old extensions + ); + + _log.i('Registry URL set to: $resolvedUrl'); + await refresh(forceRefresh: true); + } catch (e) { + _log.e('Failed to set registry URL: $e'); + state = state.copyWith(isLoading: false, error: e.toString()); + } + } + + /// Removes the saved registry URL and fully detaches the repo from backend. + Future removeRegistryUrl() async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_registryUrlPrefKey); + + // Reset the URL in Go backend memory AND clear its cache + await PlatformBridge.clearStoreRegistryUrl(); + + state = state.copyWith( + registryUrl: '', + extensions: const [], + clearCategory: true, + searchQuery: '', + clearError: true, + ); + + _log.i('Registry URL removed'); + } catch (e) { + _log.e('Failed to remove registry URL: $e'); + state = state.copyWith(error: e.toString()); + } + } + Future refresh({bool forceRefresh = false}) async { state = state.copyWith(isLoading: true, clearError: true); diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 79cb17a8..cf9ceab5 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -158,12 +158,10 @@ class _MainShellState extends ConsumerState { if (settings.storageMode == 'saf') return; if (settings.downloadDirectory.isEmpty) return; - // Check Android version final deviceInfo = DeviceInfoPlugin(); final androidInfo = await deviceInfo.androidInfo; if (androidInfo.version.sdkInt < 29) return; - // Only show once final prefs = await SharedPreferences.getInstance(); if (prefs.getBool(_safMigrationShownKey) == true) return; await prefs.setBool(_safMigrationShownKey, true); diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index ff97c194..54af17db 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -16,6 +16,7 @@ class StoreTab extends ConsumerStatefulWidget { class _StoreTabState extends ConsumerState { final _searchController = TextEditingController(); + final _repoUrlController = TextEditingController(); bool _isInitialized = false; @override @@ -38,6 +39,7 @@ class _StoreTabState extends ConsumerState { @override void dispose() { _searchController.dispose(); + _repoUrlController.dispose(); super.dispose(); } @@ -56,6 +58,8 @@ class _StoreTabState extends ConsumerState { final downloadingId = ref.watch( storeProvider.select((s) => s.downloadingId), ); + final hasRegistryUrl = ref.watch(storeProvider.select((s) => s.hasRegistryUrl)); + final registryUrl = ref.watch(storeProvider.select((s) => s.registryUrl)); final filteredExtensions = StoreState( extensions: extensions, selectedCategory: selectedCategory, @@ -84,6 +88,14 @@ class _StoreTabState extends ConsumerState { backgroundColor: colorScheme.surface, surfaceTintColor: Colors.transparent, automaticallyImplyLeading: false, + actions: [ + if (hasRegistryUrl) + IconButton( + icon: const Icon(Icons.link), + tooltip: context.l10n.storeChangeRepoTooltip, + onPressed: () => _showChangeRepoDialog(registryUrl), + ), + ], flexibleSpace: LayoutBuilder( builder: (context, constraints) { final maxHeight = 120 + topPadding; @@ -109,169 +121,176 @@ class _StoreTabState extends ConsumerState { ), ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: ValueListenableBuilder( - valueListenable: _searchController, - builder: (context, value, _) { - return TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: context.l10n.storeSearch, - prefixIcon: const Icon(Icons.search), - suffixIcon: value.text.isNotEmpty - ? IconButton( - tooltip: 'Clear search', - icon: const Icon(Icons.clear), - onPressed: () { - _searchController.clear(); - ref - .read(storeProvider.notifier) - .setSearchQuery(''); - }, - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(28), - borderSide: BorderSide.none, - ), - filled: true, - fillColor: - Theme.of(context).brightness == Brightness.dark - ? Color.alphaBlend( - Colors.white.withValues(alpha: 0.08), - colorScheme.surface, - ) - : colorScheme.surfaceContainerHighest, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - ), - onChanged: (value) { - ref.read(storeProvider.notifier).setSearchQuery(value); - }, - ); - }, - ), - ), - ), - - SliverToBoxAdapter( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: Row( - children: [ - _CategoryChip( - label: context.l10n.storeFilterAll, - icon: Icons.apps, - isSelected: selectedCategory == null, - onTap: () => - ref.read(storeProvider.notifier).setCategory(null), - ), - const SizedBox(width: 8), - _CategoryChip( - label: context.l10n.storeFilterMetadata, - icon: Icons.label_outline, - isSelected: selectedCategory == StoreCategory.metadata, - onTap: () => ref - .read(storeProvider.notifier) - .setCategory(StoreCategory.metadata), - ), - const SizedBox(width: 8), - _CategoryChip( - label: context.l10n.storeFilterDownload, - icon: Icons.download_outlined, - isSelected: selectedCategory == StoreCategory.download, - onTap: () => ref - .read(storeProvider.notifier) - .setCategory(StoreCategory.download), - ), - const SizedBox(width: 8), - _CategoryChip( - label: context.l10n.storeFilterUtility, - icon: Icons.build_outlined, - isSelected: selectedCategory == StoreCategory.utility, - onTap: () => ref - .read(storeProvider.notifier) - .setCategory(StoreCategory.utility), - ), - const SizedBox(width: 8), - _CategoryChip( - label: context.l10n.storeFilterLyrics, - icon: Icons.lyrics_outlined, - isSelected: selectedCategory == StoreCategory.lyrics, - onTap: () => ref - .read(storeProvider.notifier) - .setCategory(StoreCategory.lyrics), - ), - const SizedBox(width: 8), - _CategoryChip( - label: context.l10n.storeFilterIntegration, - icon: Icons.link, - isSelected: selectedCategory == StoreCategory.integration, - onTap: () => ref - .read(storeProvider.notifier) - .setCategory(StoreCategory.integration), - ), - ], - ), - ), - ), - - if (isLoading && extensions.isEmpty) - const SliverFillRemaining( - child: Center(child: CircularProgressIndicator()), - ) - else if (error != null && extensions.isEmpty) - SliverFillRemaining(child: _buildErrorState(error, colorScheme)) - else if (filteredExtensions.isEmpty) + // Show setup screen if no registry URL is configured + if (!hasRegistryUrl) SliverFillRemaining( - child: _buildEmptyState( - hasFilters: - searchQuery.isNotEmpty || selectedCategory != null, - colorScheme: colorScheme, - ), + child: _buildSetupRepoState(colorScheme, error), ) else ...[ SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text( - '${filteredExtensions.length} ${filteredExtensions.length == 1 ? 'extension' : 'extensions'}', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + child: ValueListenableBuilder( + valueListenable: _searchController, + builder: (context, value, _) { + return TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: context.l10n.storeSearch, + prefixIcon: const Icon(Icons.search), + suffixIcon: value.text.isNotEmpty + ? IconButton( + tooltip: 'Clear search', + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + ref + .read(storeProvider.notifier) + .setSearchQuery(''); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(28), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: + Theme.of(context).brightness == Brightness.dark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.08), + colorScheme.surface, + ) + : colorScheme.surfaceContainerHighest, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: (value) { + ref.read(storeProvider.notifier).setSearchQuery(value); + }, + ); + }, ), ), ), SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SettingsGroup( - children: filteredExtensions.asMap().entries.map((entry) { - final index = entry.key; - final ext = entry.value; - return _ExtensionItem( - extension: ext, - showDivider: index < filteredExtensions.length - 1, - isDownloading: downloadingId == ext.id, - onInstall: () => _installExtension(ext), - onUpdate: () => _updateExtension(ext), - onTap: () => _showExtensionDetails(ext), - ); - }).toList(), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + children: [ + _CategoryChip( + label: context.l10n.storeFilterAll, + icon: Icons.apps, + isSelected: selectedCategory == null, + onTap: () => + ref.read(storeProvider.notifier).setCategory(null), + ), + const SizedBox(width: 8), + _CategoryChip( + label: context.l10n.storeFilterMetadata, + icon: Icons.label_outline, + isSelected: selectedCategory == StoreCategory.metadata, + onTap: () => ref + .read(storeProvider.notifier) + .setCategory(StoreCategory.metadata), + ), + const SizedBox(width: 8), + _CategoryChip( + label: context.l10n.storeFilterDownload, + icon: Icons.download_outlined, + isSelected: selectedCategory == StoreCategory.download, + onTap: () => ref + .read(storeProvider.notifier) + .setCategory(StoreCategory.download), + ), + const SizedBox(width: 8), + _CategoryChip( + label: context.l10n.storeFilterUtility, + icon: Icons.build_outlined, + isSelected: selectedCategory == StoreCategory.utility, + onTap: () => ref + .read(storeProvider.notifier) + .setCategory(StoreCategory.utility), + ), + const SizedBox(width: 8), + _CategoryChip( + label: context.l10n.storeFilterLyrics, + icon: Icons.lyrics_outlined, + isSelected: selectedCategory == StoreCategory.lyrics, + onTap: () => ref + .read(storeProvider.notifier) + .setCategory(StoreCategory.lyrics), + ), + const SizedBox(width: 8), + _CategoryChip( + label: context.l10n.storeFilterIntegration, + icon: Icons.link, + isSelected: selectedCategory == StoreCategory.integration, + onTap: () => ref + .read(storeProvider.notifier) + .setCategory(StoreCategory.integration), + ), + ], ), ), ), - const SliverToBoxAdapter(child: SizedBox(height: 16)), + if (isLoading && extensions.isEmpty) + const SliverFillRemaining( + child: Center(child: CircularProgressIndicator()), + ) + else if (error != null && extensions.isEmpty) + SliverFillRemaining(child: _buildErrorState(error, colorScheme)) + else if (filteredExtensions.isEmpty) + SliverFillRemaining( + child: _buildEmptyState( + hasFilters: + searchQuery.isNotEmpty || selectedCategory != null, + colorScheme: colorScheme, + ), + ) + else ...[ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Text( + '${filteredExtensions.length} ${filteredExtensions.length == 1 ? 'extension' : 'extensions'}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ), + + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SettingsGroup( + children: filteredExtensions.asMap().entries.map((entry) { + final index = entry.key; + final ext = entry.value; + return _ExtensionItem( + extension: ext, + showDivider: index < filteredExtensions.length - 1, + isDownloading: downloadingId == ext.id, + onInstall: () => _installExtension(ext), + onUpdate: () => _updateExtension(ext), + onTap: () => _showExtensionDetails(ext), + ); + }).toList(), + ), + ), + ), + + const SliverToBoxAdapter(child: SizedBox(height: 16)), + ], ], ], ), @@ -279,6 +298,166 @@ class _StoreTabState extends ConsumerState { ); } + Widget _buildSetupRepoState(ColorScheme colorScheme, String? error) { + return Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.store_outlined, + size: 72, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 24), + Text( + context.l10n.storeAddRepoTitle, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + TextField( + controller: _repoUrlController, + decoration: InputDecoration( + hintText: context.l10n.storeRepoUrlHint, + labelText: context.l10n.storeRepoUrlLabel, + prefixIcon: const Icon(Icons.link), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colorScheme.outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colorScheme.primary, width: 2), + ), + ), + keyboardType: TextInputType.url, + autocorrect: false, + onSubmitted: (_) => _submitRepoUrl(), + ), + if (error != null) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon(Icons.error_outline, size: 20, color: colorScheme.onErrorContainer), + const SizedBox(width: 8), + Expanded( + child: Text( + error, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onErrorContainer, + ), + ), + ), + ], + ), + ), + ], + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: _submitRepoUrl, + icon: const Icon(Icons.add), + label: Text(context.l10n.storeAddRepoButton), + ), + ), + ], + ), + ), + ); + } + + void _submitRepoUrl() { + final url = _repoUrlController.text.trim(); + if (url.isEmpty) return; + ref.read(storeProvider.notifier).setRegistryUrl(url); + } + + void _showChangeRepoDialog(String currentUrl) { + final changeUrlController = TextEditingController(text: currentUrl); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.storeRepoDialogTitle), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.storeRepoDialogCurrent, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + currentUrl, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + fontSize: 11, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 16), + TextField( + controller: changeUrlController, + decoration: InputDecoration( + hintText: context.l10n.storeRepoUrlHint, + labelText: context.l10n.storeNewRepoUrlLabel, + prefixIcon: const Icon(Icons.link), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + keyboardType: TextInputType.url, + autocorrect: false, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + ref.read(storeProvider.notifier).removeRegistryUrl(); + }, + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + ), + child: Text(context.l10n.dialogRemove), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.dialogCancel), + ), + FilledButton( + onPressed: () { + final newUrl = changeUrlController.text.trim(); + Navigator.of(context).pop(); + if (newUrl.isNotEmpty) { + ref.read(storeProvider.notifier).setRegistryUrl(newUrl); + } + }, + child: Text(context.l10n.dialogSave), + ), + ], + ), + ).then((_) => changeUrlController.dispose()); + } + Widget _buildErrorState(String error, ColorScheme colorScheme) { return Center( child: Padding( @@ -289,7 +468,7 @@ class _StoreTabState extends ConsumerState { Icon(Icons.error_outline, size: 64, color: colorScheme.error), const SizedBox(height: 16), Text( - 'Failed to load store', + context.l10n.storeLoadError, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), @@ -328,7 +507,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(height: 16), Text( - hasFilters ? 'No extensions found' : 'No extensions available', + hasFilters ? context.l10n.storeEmptyNoResults : context.l10n.storeEmptyNoExtensions, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 95f65c44..68ecb60b 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -1596,7 +1596,6 @@ class _TrackMetadataScreenState extends ConsumerState { }); try { - // Convert duration from seconds to milliseconds final durationMs = (duration ?? 0) * 1000; // First, check if lyrics are embedded in the file @@ -1617,7 +1616,6 @@ class _TrackMetadataScreenState extends ConsumerState { final embeddedSource = embeddedResult['source']?.toString() ?? ''; if (embeddedLyrics.isNotEmpty) { - // Lyrics found in file if (mounted) { final cleanLyrics = _cleanLrcForDisplay(embeddedLyrics); setState(() { @@ -1650,7 +1648,6 @@ class _TrackMetadataScreenState extends ConsumerState { lrcText == '[instrumental:true]'; if (mounted) { - // Check for instrumental marker if (instrumental) { setState(() { _isInstrumental = true; diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index c92fb7a3..703dfe6d 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -242,8 +242,6 @@ class LibraryDatabase { }; } - // CRUD Operations - Future upsert(Map json) async { final db = await database; await db.insert( diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index c41207b9..8ea1dddc 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1191,6 +1191,22 @@ class PlatformBridge { await _channel.invokeMethod('initExtensionStore', {'cache_dir': cacheDir}); } + static Future setStoreRegistryUrl(String registryUrl) async { + _log.d('setStoreRegistryUrl: $registryUrl'); + await _channel.invokeMethod('setStoreRegistryUrl', {'registry_url': registryUrl}); + } + + static Future getStoreRegistryUrl() async { + _log.d('getStoreRegistryUrl'); + final result = await _channel.invokeMethod('getStoreRegistryUrl'); + return result as String? ?? ''; + } + + static Future clearStoreRegistryUrl() async { + _log.d('clearStoreRegistryUrl'); + await _channel.invokeMethod('clearStoreRegistryUrl'); + } + static Future>> getStoreExtensions({ bool forceRefresh = false, }) async { From 5948e4f1250d96dd037fae14c9bad2a0e76ad35b Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 14 Mar 2026 15:07:15 +0700 Subject: [PATCH 043/145] chore: remove redundant inline comments --- .../src/main/kotlin/com/zarz/spotiflac/DownloadService.kt | 2 -- go_backend/cue_parser.go | 7 ------- go_backend/duplicate.go | 1 - go_backend/extension_manager.go | 2 -- go_backend/extension_store.go | 2 -- go_backend/httputil.go | 4 ++-- go_backend/youtube.go | 3 --- lib/providers/local_library_provider.dart | 2 -- lib/providers/recent_access_provider.dart | 2 +- lib/screens/downloaded_album_screen.dart | 1 - lib/screens/queue_tab.dart | 3 +-- lib/screens/store_tab.dart | 1 - lib/screens/track_metadata_screen.dart | 4 ++-- lib/services/ffmpeg_service.dart | 4 ---- lib/services/library_database.dart | 1 - 15 files changed, 6 insertions(+), 33 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt index c3fd7b46..fe9b8e07 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt @@ -115,10 +115,8 @@ class DownloadService : Service() { * We must call stopSelf() within a few seconds to avoid a crash. */ override fun onTimeout(startId: Int, fgsType: Int) { - // Log the timeout for debugging android.util.Log.w("DownloadService", "Foreground service timeout reached (6 hours limit). Stopping service.") - // Gracefully stop the service stopForegroundService() } diff --git a/go_backend/cue_parser.go b/go_backend/cue_parser.go index 914d3f4d..8cbf1f61 100644 --- a/go_backend/cue_parser.go +++ b/go_backend/cue_parser.go @@ -114,7 +114,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { continue } - // PERFORMER if strings.HasPrefix(upper, "PERFORMER ") { value := unquoteCue(line[len("PERFORMER "):]) if currentTrack != nil { @@ -125,7 +124,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { continue } - // TITLE if strings.HasPrefix(upper, "TITLE ") { value := unquoteCue(line[len("TITLE "):]) if currentTrack != nil { @@ -136,7 +134,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { continue } - // FILE if strings.HasPrefix(upper, "FILE ") { rest := line[len("FILE "):] // Extract filename and type @@ -148,7 +145,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { continue } - // TRACK if strings.HasPrefix(upper, "TRACK ") { // Save previous track if currentTrack != nil { @@ -168,7 +164,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { continue } - // INDEX if strings.HasPrefix(upper, "INDEX ") && currentTrack != nil { parts := strings.Fields(line) if len(parts) >= 3 { @@ -184,7 +179,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { continue } - // ISRC if strings.HasPrefix(upper, "ISRC ") && currentTrack != nil { currentTrack.ISRC = strings.TrimSpace(line[len("ISRC "):]) continue @@ -560,7 +554,6 @@ func scanCueSheetForLibrary(cuePath string, sheet *CueSheet, audioPath, virtualP duration = int(totalDurationSec - track.StartTime) } - // Use a unique ID based on pathBase + track number id := generateLibraryID(fmt.Sprintf("%s#track%d", pathBase, track.Number)) // Use a virtual file path that includes the track number to ensure diff --git a/go_backend/duplicate.go b/go_backend/duplicate.go index cd922285..30b68551 100644 --- a/go_backend/duplicate.go +++ b/go_backend/duplicate.go @@ -34,7 +34,6 @@ func GetISRCIndex(outputDir string) *ISRCIndex { return idx } - // Slow path: need to build index // Use per-directory mutex to prevent multiple goroutines from building simultaneously buildLock, _ := isrcBuildingMu.LoadOrStore(outputDir, &sync.Mutex{}) mu := buildLock.(*sync.Mutex) diff --git a/go_backend/extension_manager.go b/go_backend/extension_manager.go index 5c73e251..d2a1f8d3 100644 --- a/go_backend/extension_manager.go +++ b/go_backend/extension_manager.go @@ -151,7 +151,6 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens if exists { versionCompare := compareVersions(manifest.Version, existingVersion) if versionCompare > 0 { - // This is an upgrade - call UpgradeExtension return m.UpgradeExtension(filePath) } else if versionCompare == 0 { return nil, fmt.Errorf("Extension '%s' v%s is already installed", existingDisplayName, existingVersion) @@ -429,7 +428,6 @@ func (m *ExtensionManager) loadExtensionFromDirectory(dirPath string) (*LoadedEx SourceDir: dirPath, } - // Restore enabled state from settings store store := GetExtensionSettingsStore() if enabledVal, err := store.Get(manifest.Name, "_enabled"); err == nil { if enabled, ok := enabledVal.(bool); ok { diff --git a/go_backend/extension_store.go b/go_backend/extension_store.go index 90dbf61a..22ac2d0a 100644 --- a/go_backend/extension_store.go +++ b/go_backend/extension_store.go @@ -484,12 +484,10 @@ func (s *ExtensionStore) SearchExtensions(query string, category string) ([]Stor queryLower := toLower(query) for _, ext := range extensions { - // Filter by category if category != "" && ext.Category != category { continue } - // Filter by query if query != "" { if !containsIgnoreCase(ext.Name, queryLower) && !containsIgnoreCase(ext.DisplayName, queryLower) && diff --git a/go_backend/httputil.go b/go_backend/httputil.go index 08fac2a8..36433d1e 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -350,7 +350,7 @@ func calculateNextDelay(currentDelay time.Duration, config RetryConfig) time.Dur func getRetryAfterDuration(resp *http.Response) time.Duration { retryAfter := resp.Header.Get("Retry-After") if retryAfter == "" { - return 60 * time.Second // Default wait time + return 60 * time.Second } if seconds, err := strconv.Atoi(retryAfter); err == nil { @@ -364,7 +364,7 @@ func getRetryAfterDuration(resp *http.Response) time.Duration { } } - return 60 * time.Second // Default + return 60 * time.Second } func ReadResponseBody(resp *http.Response) ([]byte, error) { diff --git a/go_backend/youtube.go b/go_backend/youtube.go index b8038032..58e5d751 100644 --- a/go_backend/youtube.go +++ b/go_backend/youtube.go @@ -510,12 +510,10 @@ func ExtractYouTubeVideoID(urlStr string) (string, error) { return "", fmt.Errorf("invalid URL: %w", err) } - // /watch?v= if v := parsed.Query().Get("v"); v != "" { return v, nil } - // /embed/ if strings.Contains(parsed.Path, "/embed/") { parts := strings.Split(parsed.Path, "/embed/") if len(parts) >= 2 { @@ -523,7 +521,6 @@ func ExtractYouTubeVideoID(urlStr string) (string, error) { } } - // /v/ if strings.Contains(parsed.Path, "/v/") { parts := strings.Split(parsed.Path, "/v/") if len(parts) >= 2 { diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index 8c36a4f6..33f1ae6a 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -315,7 +315,6 @@ class LocalLibraryNotifier extends Notifier { int skippedDownloads = 0; for (final json in results) { final filePath = json['filePath'] as String?; - // Skip files that are already in download history if (_isDownloadedPath(filePath, downloadedPathKeys)) { skippedDownloads++; continue; @@ -464,7 +463,6 @@ class LocalLibraryNotifier extends Notifier { } } - // Delete removed items if (deletedPaths.isNotEmpty) { final deleteCount = await _db.deleteByPaths(deletedPaths); for (final path in deletedPaths) { diff --git a/lib/providers/recent_access_provider.dart b/lib/providers/recent_access_provider.dart index 8a2ba671..7b9cc15a 100644 --- a/lib/providers/recent_access_provider.dart +++ b/lib/providers/recent_access_provider.dart @@ -70,7 +70,7 @@ class RecentAccessItem { /// State for recent access history class RecentAccessState { final List items; - final Set hiddenDownloadIds; // IDs of downloads hidden from recents + final Set hiddenDownloadIds; final bool isLoaded; const RecentAccessState({ diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 04e5dcc0..55b1b71c 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -125,7 +125,6 @@ class _DownloadedAlbumScreenState extends ConsumerState { (item.albumArtist != null && item.albumArtist!.isNotEmpty) ? item.albumArtist! : item.artistName; - // Use lowercase for case-insensitive matching final itemKey = '${item.albumName.toLowerCase()}|${itemArtist.toLowerCase()}'; return itemKey == _albumLookupKey; diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index e05429c9..8645950d 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -105,7 +105,7 @@ class UnifiedLibraryItem { artistName: item.artistName, albumName: item.albumName, coverUrl: null, // Local library doesn't have cover URLs - localCoverPath: item.coverPath, // Use extracted cover path + localCoverPath: item.coverPath, filePath: item.filePath, quality: quality, addedAt: item.fileModTime != null @@ -2067,7 +2067,6 @@ class _QueueTabState extends ConsumerState { final albumCounts = {}; final albumMap = >{}; for (final item in items) { - // Use lowercase key for case-insensitive grouping final key = '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; albumCounts[key] = (albumCounts[key] ?? 0) + 1; diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index 54af17db..72c91444 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -121,7 +121,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Show setup screen if no registry URL is configured if (!hasRegistryUrl) SliverFillRemaining( child: _buildSetupRepoState(colorScheme, error), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 68ecb60b..d23d41e1 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -64,9 +64,9 @@ class _TrackMetadataScreenState extends ConsumerState { String? _lyricsError; String? _lyricsSource; bool _showTitleInAppBar = false; - bool _lyricsEmbedded = false; // Track if lyrics are embedded in file + bool _lyricsEmbedded = false; bool _isEmbedding = false; // Track embed operation in progress - bool _isInstrumental = false; // Track if detected as instrumental + bool _isInstrumental = false; bool _isConverting = false; // Track convert operation in progress bool _hasMetadataChanges = false; bool _hasLoadedResolvedAudioMetadata = false; diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index 8154f03c..b9aa6603 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1385,7 +1385,6 @@ class FFmpegService { final track = tracks[i]; onProgress?.call(i + 1, tracks.length); - // Sanitize filename final sanitizedTitle = track.title .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_') .replaceAll(RegExp(r'\s+'), ' ') @@ -1394,11 +1393,9 @@ class FFmpegService { final outputFileName = '$trackNumStr - $sanitizedTitle.$outputExt'; final outputPath = '$outputDir${Platform.pathSeparator}$outputFileName'; - // Build FFmpeg command for this track final StringBuffer cmdBuffer = StringBuffer(); cmdBuffer.write('-i "$audioPath" '); - // Time range final startTime = _formatSecondsForFFmpeg(track.startSec); cmdBuffer.write('-ss $startTime '); @@ -1413,7 +1410,6 @@ class FFmpegService { cmdBuffer.write('-c:a copy '); } - // Metadata final artist = track.artist.isNotEmpty ? track.artist : (albumMetadata['artist'] ?? ''); final album = albumMetadata['album'] ?? ''; final genre = albumMetadata['genre'] ?? ''; diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index 703dfe6d..cf37c308 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -176,7 +176,6 @@ class LibraryDatabase { _log.i('Upgrading library database from v$oldVersion to v$newVersion'); if (oldVersion < 2) { - // Add cover_path column await db.execute('ALTER TABLE library ADD COLUMN cover_path TEXT'); _log.i('Added cover_path column'); } From d89850e8a936fa325e8e4d35389a4ddae9e2aae0 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 14 Mar 2026 15:07:34 +0700 Subject: [PATCH 044/145] feat: add name and images fields to PlaylistInfoMetadata --- go_backend/spotify.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go_backend/spotify.go b/go_backend/spotify.go index 570c4e24..0514c5d4 100644 --- a/go_backend/spotify.go +++ b/go_backend/spotify.go @@ -157,6 +157,8 @@ type AlbumResponsePayload struct { } type PlaylistInfoMetadata struct { + Name string `json:"name,omitempty"` + Images string `json:"images,omitempty"` Tracks struct { Total int `json:"total"` } `json:"tracks"` From ac9141f167a68b017a6a24d671093f0df2d891d6 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 14 Mar 2026 15:09:48 +0700 Subject: [PATCH 045/145] feat: add Qobuz and Tidal metadata API, URL parsers, and full store support --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 27 +- go_backend/exports.go | 80 +- go_backend/qobuz.go | 735 +++++++++++++++++- go_backend/qobuz_test.go | 276 +++++++ go_backend/tidal.go | 681 +++++++++++++++- go_backend/tidal_test.go | 181 +++++ ios/Runner/AppDelegate.swift | 23 + lib/providers/download_queue_provider.dart | 20 +- lib/providers/track_provider.dart | 179 +++-- lib/screens/album_screen.dart | 21 +- lib/screens/artist_screen.dart | 49 +- lib/screens/playlist_screen.dart | 50 +- lib/services/platform_bridge.dart | 41 +- 13 files changed, 2230 insertions(+), 133 deletions(-) create mode 100644 go_backend/tidal_test.go diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index aca778de..51a64a3e 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -1122,7 +1122,6 @@ class MainActivity: FlutterFragmentActivity() { var tempCuePath: String? = null var tempAudioPath: String? = null try { - // Copy CUE to temp tempCuePath = copyUriToTemp(cueDoc.uri, ".cue") if (tempCuePath == null) { errors++ @@ -1131,10 +1130,8 @@ class MainActivity: FlutterFragmentActivity() { continue } - // Extract the audio filename from the CUE sheet text val audioFileName = extractCueAudioFileName(tempCuePath) - // Find the referenced audio file as a sibling in the same SAF directory val audioDoc = resolveCueAudioSibling( parentDir = parentDir, cueName = cueName, @@ -1176,7 +1173,6 @@ class MainActivity: FlutterFragmentActivity() { val cueLastModified = try { cueDoc.lastModified() } catch (_: Exception) { 0L } - // Call Go to produce library scan entries for each CUE track val cueResultsJson = Gobackend.scanCueSheetForLibrary( tempCuePath, tempDir, @@ -2624,6 +2620,22 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } + "getQobuzMetadata" -> { + val resourceType = call.argument("resource_type") ?: "" + val resourceId = call.argument("resource_id") ?: "" + val response = withContext(Dispatchers.IO) { + Gobackend.getQobuzMetadata(resourceType, resourceId) + } + result.success(response) + } + "getTidalMetadata" -> { + val resourceType = call.argument("resource_type") ?: "" + val resourceId = call.argument("resource_id") ?: "" + val response = withContext(Dispatchers.IO) { + Gobackend.getTidalMetadata(resourceType, resourceId) + } + result.success(response) + } "parseDeezerUrl" -> { val url = call.argument("url") ?: "" val response = withContext(Dispatchers.IO) { @@ -2631,6 +2643,13 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } + "parseQobuzUrl" -> { + val url = call.argument("url") ?: "" + val response = withContext(Dispatchers.IO) { + Gobackend.parseQobuzURLExport(url) + } + result.success(response) + } "parseTidalUrl" -> { val url = call.argument("url") ?: "" val response = withContext(Dispatchers.IO) { diff --git a/go_backend/exports.go b/go_backend/exports.go index 44fa7831..447f7685 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1156,6 +1156,66 @@ func GetDeezerMetadata(resourceType, resourceID string) (string, error) { return string(jsonBytes), nil } +func GetQobuzMetadata(resourceType, resourceID string) (string, error) { + downloader := NewQobuzDownloader() + + var data interface{} + var err error + + switch resourceType { + case "track": + data, err = downloader.GetTrackMetadata(resourceID) + case "album": + data, err = downloader.GetAlbumMetadata(resourceID) + case "artist": + data, err = downloader.GetArtistMetadata(resourceID) + case "playlist": + data, err = downloader.GetPlaylistMetadata(resourceID) + default: + return "", fmt.Errorf("unsupported Qobuz resource type: %s", resourceType) + } + if err != nil { + return "", err + } + + jsonBytes, err := json.Marshal(data) + if err != nil { + return "", err + } + + return string(jsonBytes), nil +} + +func GetTidalMetadata(resourceType, resourceID string) (string, error) { + downloader := NewTidalDownloader() + + var data interface{} + var err error + + switch resourceType { + case "track": + data, err = downloader.GetTrackMetadata(resourceID) + case "album": + data, err = downloader.GetAlbumMetadata(resourceID) + case "artist": + data, err = downloader.GetArtistMetadata(resourceID) + case "playlist": + data, err = downloader.GetPlaylistMetadata(resourceID) + default: + return "", fmt.Errorf("unsupported Tidal resource type: %s", resourceType) + } + if err != nil { + return "", err + } + + jsonBytes, err := json.Marshal(data) + if err != nil { + return "", err + } + + return string(jsonBytes), nil +} + func ParseDeezerURLExport(url string) (string, error) { resourceType, resourceID, err := parseDeezerURL(url) if err != nil { @@ -1175,6 +1235,25 @@ func ParseDeezerURLExport(url string) (string, error) { return string(jsonBytes), nil } +func ParseQobuzURLExport(url string) (string, error) { + resourceType, resourceID, err := parseQobuzURL(url) + if err != nil { + return "", err + } + + result := map[string]string{ + "type": resourceType, + "id": resourceID, + } + + jsonBytes, err := json.Marshal(result) + if err != nil { + return "", err + } + + return string(jsonBytes), nil +} + func ParseTidalURLExport(url string) (string, error) { resourceType, resourceID, err := parseTidalURL(url) if err != nil { @@ -1311,7 +1390,6 @@ func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) { return string(jsonBytes), nil } - // For artists/playlists, SongLink doesn't provide direct mapping return "", fmt.Errorf("Spotify to Deezer conversion only supported for tracks and albums. Please search by name for %s", resourceType) } diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 092c0e96..d51e45de 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -28,13 +28,29 @@ type QobuzDownloader struct { var ( globalQobuzDownloader *QobuzDownloader qobuzDownloaderOnce sync.Once + qobuzGetTrackByIDFunc = func(q *QobuzDownloader, trackID int64) (*QobuzTrack, error) { + return q.GetTrackByID(trackID) + } + qobuzSearchTrackByISRCWithDurationFunc = func(q *QobuzDownloader, isrc string, expectedDurationSec int) (*QobuzTrack, error) { + return q.SearchTrackByISRCWithDuration(isrc, expectedDurationSec) + } + qobuzSearchTrackByMetadataWithDurationFunc = func(q *QobuzDownloader, trackName, artistName string, expectedDurationSec int) (*QobuzTrack, error) { + return q.SearchTrackByMetadataWithDuration(trackName, artistName, expectedDurationSec) + } + songLinkCheckTrackAvailabilityFunc = func(client *SongLinkClient, spotifyTrackID string, isrc string) (*TrackAvailability, error) { + return client.CheckTrackAvailability(spotifyTrackID, isrc) + } ) const ( qobuzTrackGetBaseURL = "https://www.qobuz.com/api.json/0.2/track/get?track_id=" qobuzTrackSearchBaseURL = "https://www.qobuz.com/api.json/0.2/track/search?query=" + qobuzAlbumGetBaseURL = "https://www.qobuz.com/api.json/0.2/album/get?album_id=" + qobuzArtistGetBaseURL = "https://www.qobuz.com/api.json/0.2/artist/get?artist_id=" + qobuzPlaylistGetBaseURL = "https://www.qobuz.com/api.json/0.2/playlist/get?playlist_id=" qobuzStoreSearchBaseURL = "https://www.qobuz.com/us-en/search/tracks/" qobuzTrackPlayBaseURL = "https://play.qobuz.com/track/" + qobuzStoreBaseURL = "https://www.qobuz.com/us-en" qobuzDownloadAPIURL = "https://www.musicdl.me/api/qobuz/download" qobuzDabMusicAPIURL = "https://dabmusic.xyz/api/stream?trackId=" qobuzDeebAPIURL = "https://dab.yeet.su/api/stream?trackId=" @@ -43,6 +59,8 @@ const ( ) var qobuzStoreTrackIDRegex = regexp.MustCompile(`/v4/ajax/popin-add-cart/track/([0-9]+)`) +var qobuzArtistAlbumIDRegex = regexp.MustCompile(`data-itemtype="album"\s+data-itemId="([A-Za-z0-9]+)"`) +var qobuzLocaleSegmentRegex = regexp.MustCompile(`^[a-z]{2}-[a-z]{2}$`) var qobuzDebugKeyObfuscated = []byte{ 0x69, 0x3b, 0x38, 0x3e, 0x36, 0x37, 0x35, 0x2f, 0x36, 0x3b, @@ -58,20 +76,406 @@ type QobuzTrack struct { ISRC string `json:"isrc"` Duration int `json:"duration"` TrackNumber int `json:"track_number"` + MediaNumber int `json:"media_number"` MaximumBitDepth int `json:"maximum_bit_depth"` MaximumSamplingRate float64 `json:"maximum_sampling_rate"` + Version string `json:"version"` Album struct { + ID string `json:"id"` + QobuzID int64 `json:"qobuz_id"` + TracksCount int `json:"tracks_count"` Title string `json:"title"` ReleaseDate string `json:"release_date_original"` - Image struct { - Large string `json:"large"` + ProductType string `json:"product_type"` + ReleaseType string `json:"release_type"` + Artist struct { + ID int64 `json:"id"` + Name string `json:"name"` + } `json:"artist"` + Artists []qobuzArtistRef `json:"artists"` + Image struct { + Thumbnail string `json:"thumbnail"` + Small string `json:"small"` + Large string `json:"large"` } `json:"image"` } `json:"album"` Performer struct { + ID int64 `json:"id"` Name string `json:"name"` } `json:"performer"` } +type qobuzImageSet struct { + Thumbnail string `json:"thumbnail"` + Small string `json:"small"` + Large string `json:"large"` +} + +type qobuzArtistRef struct { + ID int64 `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +type qobuzLabelRef struct { + Name string `json:"name"` +} + +type qobuzGenreRef struct { + Name string `json:"name"` +} + +type qobuzAlbumDetails struct { + ID string `json:"id"` + QobuzID int64 `json:"qobuz_id"` + Title string `json:"title"` + ReleaseDateOriginal string `json:"release_date_original"` + TracksCount int `json:"tracks_count"` + ProductType string `json:"product_type"` + ReleaseType string `json:"release_type"` + Image qobuzImageSet `json:"image"` + Artist qobuzArtistRef `json:"artist"` + Artists []qobuzArtistRef `json:"artists"` + Genre qobuzGenreRef `json:"genre"` + Label qobuzLabelRef `json:"label"` + Copyright string `json:"copyright"` + Tracks struct { + Items []QobuzTrack `json:"items"` + } `json:"tracks"` +} + +type qobuzArtistDetails struct { + ID int64 `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Image qobuzImageSet `json:"image"` +} + +type qobuzPlaylistDetails struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ImageRectangle []string `json:"image_rectangle"` + ImageRectangleMini []string `json:"image_rectangle_mini"` + TracksCount int `json:"tracks_count"` + Owner struct { + ID int64 `json:"id"` + Name string `json:"name"` + } `json:"owner"` + Tracks struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Items []QobuzTrack `json:"items"` + } `json:"tracks"` +} + +func qobuzFirstNonEmpty(values ...string) string { + for _, value := range values { + trimmed := strings.TrimSpace(value) + if trimmed != "" { + return trimmed + } + } + return "" +} + +func qobuzPrefixedID(id string) string { + trimmed := strings.TrimSpace(id) + if trimmed == "" { + return "" + } + if strings.HasPrefix(trimmed, "qobuz:") { + return trimmed + } + return "qobuz:" + trimmed +} + +func qobuzPrefixedNumericID(id int64) string { + if id <= 0 { + return "" + } + return fmt.Sprintf("qobuz:%d", id) +} + +func qobuzNormalizeReleaseDate(value string) string { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return "" + } + if _, err := time.Parse("2006-01-02", trimmed); err == nil { + return trimmed + } + if parsed, err := time.Parse("Jan 2, 2006", trimmed); err == nil { + return parsed.Format("2006-01-02") + } + return trimmed +} + +func qobuzNormalizeAlbumType(releaseType, productType string, totalTracks int) string { + kind := strings.ToLower(strings.TrimSpace(releaseType)) + if kind == "" { + kind = strings.ToLower(strings.TrimSpace(productType)) + } + switch kind { + case "album", "single", "ep", "compilation": + return kind + } + if totalTracks > 0 && totalTracks <= 3 { + return "single" + } + return "album" +} + +func qobuzArtistsDisplayName(artists []qobuzArtistRef, fallback string) string { + names := make([]string, 0, len(artists)) + seen := make(map[string]struct{}, len(artists)) + for _, artist := range artists { + name := strings.TrimSpace(artist.Name) + if name == "" { + continue + } + key := strings.ToLower(name) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + names = append(names, name) + } + if len(names) == 0 { + return strings.TrimSpace(fallback) + } + return strings.Join(names, ", ") +} + +func qobuzTrackDisplayTitle(track *QobuzTrack) string { + if track == nil { + return "" + } + title := strings.TrimSpace(track.Title) + version := strings.TrimSpace(track.Version) + if title == "" || version == "" { + return title + } + return fmt.Sprintf("%s (%s)", title, version) +} + +func qobuzTrackAlbumImage(track *QobuzTrack) string { + if track == nil { + return "" + } + return qobuzFirstNonEmpty( + track.Album.Image.Large, + track.Album.Image.Small, + track.Album.Image.Thumbnail, + ) +} + +func qobuzAlbumImage(album *qobuzAlbumDetails) string { + if album == nil { + return "" + } + return qobuzFirstNonEmpty( + album.Image.Large, + album.Image.Small, + album.Image.Thumbnail, + ) +} + +func qobuzTrackArtistID(track *QobuzTrack) string { + if track == nil { + return "" + } + if track.Performer.ID > 0 { + return qobuzPrefixedNumericID(track.Performer.ID) + } + return qobuzPrefixedNumericID(track.Album.Artist.ID) +} + +func qobuzTrackArtistName(track *QobuzTrack) string { + if track == nil { + return "" + } + return strings.TrimSpace(track.Performer.Name) +} + +func qobuzTrackAlbumArtist(track *QobuzTrack) string { + if track == nil { + return "" + } + return qobuzArtistsDisplayName(track.Album.Artists, track.Album.Artist.Name) +} + +func qobuzTrackAlbumType(track *QobuzTrack) string { + if track == nil { + return "album" + } + return qobuzNormalizeAlbumType( + track.Album.ReleaseType, + track.Album.ProductType, + track.Album.TracksCount, + ) +} + +func qobuzTrackToTrackMetadata(track *QobuzTrack) TrackMetadata { + if track == nil { + return TrackMetadata{} + } + return TrackMetadata{ + SpotifyID: qobuzPrefixedNumericID(track.ID), + Artists: qobuzTrackArtistName(track), + Name: qobuzTrackDisplayTitle(track), + AlbumName: strings.TrimSpace(track.Album.Title), + AlbumArtist: qobuzTrackAlbumArtist(track), + DurationMS: track.Duration * 1000, + Images: qobuzTrackAlbumImage(track), + ReleaseDate: qobuzNormalizeReleaseDate(track.Album.ReleaseDate), + TrackNumber: track.TrackNumber, + TotalTracks: track.Album.TracksCount, + DiscNumber: track.MediaNumber, + ExternalURL: fmt.Sprintf("%s%d", qobuzTrackPlayBaseURL, track.ID), + ISRC: strings.TrimSpace(track.ISRC), + AlbumID: qobuzPrefixedID(track.Album.ID), + ArtistID: qobuzTrackArtistID(track), + AlbumType: qobuzTrackAlbumType(track), + } +} + +func qobuzTrackToAlbumTrackMetadata(track *QobuzTrack) AlbumTrackMetadata { + if track == nil { + return AlbumTrackMetadata{} + } + return AlbumTrackMetadata{ + SpotifyID: qobuzPrefixedNumericID(track.ID), + Artists: qobuzTrackArtistName(track), + Name: qobuzTrackDisplayTitle(track), + AlbumName: strings.TrimSpace(track.Album.Title), + AlbumArtist: qobuzTrackAlbumArtist(track), + DurationMS: track.Duration * 1000, + Images: qobuzTrackAlbumImage(track), + ReleaseDate: qobuzNormalizeReleaseDate(track.Album.ReleaseDate), + TrackNumber: track.TrackNumber, + TotalTracks: track.Album.TracksCount, + DiscNumber: track.MediaNumber, + ExternalURL: fmt.Sprintf("%s%d", qobuzTrackPlayBaseURL, track.ID), + ISRC: strings.TrimSpace(track.ISRC), + AlbumID: qobuzPrefixedID(track.Album.ID), + AlbumURL: fmt.Sprintf("https://play.qobuz.com/album/%s", strings.TrimSpace(track.Album.ID)), + AlbumType: qobuzTrackAlbumType(track), + } +} + +func qobuzAlbumToAlbumInfo(album *qobuzAlbumDetails) AlbumInfoMetadata { + if album == nil { + return AlbumInfoMetadata{} + } + return AlbumInfoMetadata{ + TotalTracks: album.TracksCount, + Name: strings.TrimSpace(album.Title), + ReleaseDate: qobuzNormalizeReleaseDate(album.ReleaseDateOriginal), + Artists: qobuzArtistsDisplayName(album.Artists, album.Artist.Name), + ArtistId: qobuzPrefixedNumericID(album.Artist.ID), + Images: qobuzAlbumImage(album), + Genre: strings.TrimSpace(album.Genre.Name), + Label: strings.TrimSpace(album.Label.Name), + Copyright: strings.TrimSpace(album.Copyright), + } +} + +func qobuzAlbumToArtistAlbum(album *qobuzAlbumDetails) ArtistAlbumMetadata { + if album == nil { + return ArtistAlbumMetadata{} + } + return ArtistAlbumMetadata{ + ID: qobuzPrefixedID(album.ID), + Name: strings.TrimSpace(album.Title), + ReleaseDate: qobuzNormalizeReleaseDate(album.ReleaseDateOriginal), + TotalTracks: album.TracksCount, + Images: qobuzAlbumImage(album), + AlbumType: qobuzNormalizeAlbumType(album.ReleaseType, album.ProductType, album.TracksCount), + Artists: qobuzArtistsDisplayName(album.Artists, album.Artist.Name), + } +} + +func qobuzSplitPathSegments(path string) []string { + rawSegments := strings.Split(strings.TrimSpace(path), "/") + segments := make([]string, 0, len(rawSegments)) + for _, segment := range rawSegments { + trimmed := strings.TrimSpace(segment) + if trimmed == "" { + continue + } + segments = append(segments, trimmed) + } + if len(segments) > 0 && qobuzLocaleSegmentRegex.MatchString(strings.ToLower(segments[0])) { + return segments[1:] + } + return segments +} + +func qobuzResourceTypeFromSegment(segment string) string { + switch strings.ToLower(strings.TrimSpace(segment)) { + case "album": + return "album" + case "interpreter", "artist": + return "artist" + case "playlist", "playlists": + return "playlist" + case "track": + return "track" + default: + return "" + } +} + +func parseQobuzURL(input string) (string, string, error) { + raw := strings.TrimSpace(input) + if raw == "" { + return "", "", fmt.Errorf("empty Qobuz URL") + } + + if strings.HasPrefix(strings.ToLower(raw), "qobuzapp://") { + parsed, err := url.Parse(raw) + if err != nil { + return "", "", err + } + resourceType := qobuzResourceTypeFromSegment(parsed.Host) + resourceID := strings.Trim(strings.TrimSpace(parsed.Path), "/") + if resourceType == "" || resourceID == "" { + return "", "", fmt.Errorf("invalid or unsupported Qobuz URL") + } + return resourceType, resourceID, nil + } + + parsed, err := url.Parse(raw) + if err != nil || parsed.Host == "" { + if !strings.Contains(raw, "://") { + parsed, err = url.Parse("https://" + raw) + } + } + if err != nil || parsed == nil || parsed.Host == "" { + return "", "", fmt.Errorf("invalid or unsupported Qobuz URL") + } + + host := strings.ToLower(parsed.Host) + if host != "qobuz.com" && host != "www.qobuz.com" && host != "play.qobuz.com" { + return "", "", fmt.Errorf("invalid or unsupported Qobuz URL") + } + + segments := qobuzSplitPathSegments(parsed.Path) + if len(segments) < 2 { + return "", "", fmt.Errorf("invalid or unsupported Qobuz URL") + } + + resourceType := qobuzResourceTypeFromSegment(segments[0]) + resourceID := strings.TrimSpace(segments[len(segments)-1]) + if resourceType == "" || resourceID == "" { + return "", "", fmt.Errorf("invalid or unsupported Qobuz URL") + } + + return resourceType, resourceID, nil +} + func qobuzArtistsMatch(expectedArtist, foundArtist string) bool { normExpected := strings.ToLower(strings.TrimSpace(expectedArtist)) normFound := strings.ToLower(strings.TrimSpace(foundArtist)) @@ -386,6 +790,232 @@ func (q *QobuzDownloader) GetTrackByID(trackID int64) (*QobuzTrack, error) { return &track, nil } +func (q *QobuzDownloader) getQobuzJSON(requestURL string, target interface{}) error { + req, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + return err + } + + resp, err := DoRequestWithUserAgent(q.client, req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) + return fmt.Errorf("qobuz request failed: HTTP %d (%s)", resp.StatusCode, strings.TrimSpace(string(body))) + } + + return json.NewDecoder(resp.Body).Decode(target) +} + +func (q *QobuzDownloader) getQobuzBody(requestURL string) ([]byte, error) { + req, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + return nil, err + } + + resp, err := DoRequestWithUserAgent(q.client, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) + return nil, fmt.Errorf("qobuz request failed: HTTP %d (%s)", resp.StatusCode, strings.TrimSpace(string(body))) + } + + return io.ReadAll(resp.Body) +} + +func extractQobuzAlbumIDsFromArtistHTML(body []byte) []string { + matches := qobuzArtistAlbumIDRegex.FindAllSubmatch(body, -1) + if len(matches) == 0 { + return nil + } + + albumIDs := make([]string, 0, len(matches)) + seen := make(map[string]struct{}, len(matches)) + for _, match := range matches { + if len(match) < 2 { + continue + } + albumID := strings.TrimSpace(string(match[1])) + if albumID == "" { + continue + } + if _, ok := seen[albumID]; ok { + continue + } + seen[albumID] = struct{}{} + albumIDs = append(albumIDs, albumID) + } + return albumIDs +} + +func (q *QobuzDownloader) getAlbumDetails(albumID string) (*qobuzAlbumDetails, error) { + requestURL := fmt.Sprintf("%s%s&app_id=%s", qobuzAlbumGetBaseURL, url.QueryEscape(strings.TrimSpace(albumID)), q.appID) + var album qobuzAlbumDetails + if err := q.getQobuzJSON(requestURL, &album); err != nil { + return nil, err + } + return &album, nil +} + +func (q *QobuzDownloader) getArtistDetails(artistID string) (*qobuzArtistDetails, error) { + requestURL := fmt.Sprintf("%s%s&app_id=%s", qobuzArtistGetBaseURL, url.QueryEscape(strings.TrimSpace(artistID)), q.appID) + var artist qobuzArtistDetails + if err := q.getQobuzJSON(requestURL, &artist); err != nil { + return nil, err + } + return &artist, nil +} + +func (q *QobuzDownloader) getPlaylistDetailsPage(playlistID string, limit, offset int) (*qobuzPlaylistDetails, error) { + requestURL := fmt.Sprintf( + "%s%s&extra=tracks&limit=%d&offset=%d&app_id=%s", + qobuzPlaylistGetBaseURL, + url.QueryEscape(strings.TrimSpace(playlistID)), + limit, + offset, + q.appID, + ) + var playlist qobuzPlaylistDetails + if err := q.getQobuzJSON(requestURL, &playlist); err != nil { + return nil, err + } + return &playlist, nil +} + +func (q *QobuzDownloader) getArtistAlbumIDs(artistID string) ([]string, error) { + artist, err := q.getArtistDetails(artistID) + if err != nil { + return nil, err + } + + slug := strings.TrimSpace(artist.Slug) + if slug == "" { + slug = "artist" + } + requestURL := fmt.Sprintf("%s/interpreter/%s/%d", qobuzStoreBaseURL, url.PathEscape(slug), artist.ID) + body, err := q.getQobuzBody(requestURL) + if err != nil { + return nil, err + } + + albumIDs := extractQobuzAlbumIDsFromArtistHTML(body) + if len(albumIDs) == 0 { + return nil, fmt.Errorf("artist page did not contain album IDs") + } + return albumIDs, nil +} + +func (q *QobuzDownloader) GetTrackMetadata(resourceID string) (*TrackResponse, error) { + trackID, err := strconv.ParseInt(strings.TrimSpace(resourceID), 10, 64) + if err != nil || trackID <= 0 { + return nil, fmt.Errorf("invalid Qobuz track ID: %s", resourceID) + } + + track, err := q.GetTrackByID(trackID) + if err != nil { + return nil, err + } + + return &TrackResponse{Track: qobuzTrackToTrackMetadata(track)}, nil +} + +func (q *QobuzDownloader) GetAlbumMetadata(resourceID string) (*AlbumResponsePayload, error) { + album, err := q.getAlbumDetails(resourceID) + if err != nil { + return nil, err + } + + tracks := make([]AlbumTrackMetadata, 0, len(album.Tracks.Items)) + for i := range album.Tracks.Items { + tracks = append(tracks, qobuzTrackToAlbumTrackMetadata(&album.Tracks.Items[i])) + } + + return &AlbumResponsePayload{ + AlbumInfo: qobuzAlbumToAlbumInfo(album), + TrackList: tracks, + }, nil +} + +func (q *QobuzDownloader) GetPlaylistMetadata(resourceID string) (*PlaylistResponsePayload, error) { + const pageSize = 50 + + offset := 0 + var playlistInfo PlaylistInfoMetadata + tracks := make([]AlbumTrackMetadata, 0, pageSize) + + for { + page, err := q.getPlaylistDetailsPage(resourceID, pageSize, offset) + if err != nil { + return nil, err + } + + if offset == 0 { + total := page.Tracks.Total + if total == 0 { + total = page.TracksCount + } + playlistInfo.Tracks.Total = total + playlistInfo.Owner.DisplayName = strings.TrimSpace(page.Owner.Name) + playlistInfo.Owner.Name = strings.TrimSpace(page.Name) + playlistInfo.Owner.Images = qobuzFirstNonEmpty(page.ImageRectangle...) + } + + for i := range page.Tracks.Items { + tracks = append(tracks, qobuzTrackToAlbumTrackMetadata(&page.Tracks.Items[i])) + } + + if len(page.Tracks.Items) == 0 || + offset+len(page.Tracks.Items) >= playlistInfo.Tracks.Total || + len(page.Tracks.Items) < pageSize { + break + } + offset += len(page.Tracks.Items) + } + + return &PlaylistResponsePayload{ + PlaylistInfo: playlistInfo, + TrackList: tracks, + }, nil +} + +func (q *QobuzDownloader) GetArtistMetadata(resourceID string) (*ArtistResponsePayload, error) { + artist, err := q.getArtistDetails(resourceID) + if err != nil { + return nil, err + } + + albumIDs, err := q.getArtistAlbumIDs(resourceID) + if err != nil { + return nil, err + } + + albums := make([]ArtistAlbumMetadata, 0, len(albumIDs)) + for _, albumID := range albumIDs { + album, albumErr := q.getAlbumDetails(albumID) + if albumErr != nil { + GoLog("[Qobuz] Skipping artist album %s: %v\n", albumID, albumErr) + continue + } + albums = append(albums, qobuzAlbumToArtistAlbum(album)) + } + + return &ArtistResponsePayload{ + ArtistInfo: ArtistInfoMetadata{ + ID: qobuzPrefixedNumericID(artist.ID), + Name: strings.TrimSpace(artist.Name), + Images: qobuzFirstNonEmpty(artist.Image.Large, artist.Image.Small, artist.Image.Thumbnail), + }, + Albums: albums, + }, nil +} + func (q *QobuzDownloader) GetAvailableAPIs() []string { return []string{ qobuzDownloadAPIURL, @@ -791,6 +1421,39 @@ func (q *QobuzDownloader) SearchTrackByMetadataWithDuration(trackName, artistNam return nil, fmt.Errorf("no matching track found for: %s - %s", artistName, trackName) } +func qobuzTrackMatchesRequest(req DownloadRequest, track *QobuzTrack, logPrefix, source string) bool { + if track == nil { + return false + } + + if req.ArtistName != "" && !qobuzArtistsMatch(req.ArtistName, track.Performer.Name) { + GoLog("[%s] Artist mismatch from %s: expected '%s', got '%s'. Rejecting.\n", + logPrefix, source, req.ArtistName, track.Performer.Name) + return false + } + + if req.TrackName != "" && !qobuzTitlesMatch(req.TrackName, track.Title) { + GoLog("[%s] Title mismatch from %s: expected '%s', got '%s'. Rejecting.\n", + logPrefix, source, req.TrackName, track.Title) + return false + } + + expectedDurationSec := req.DurationMS / 1000 + if expectedDurationSec > 0 && track.Duration > 0 { + durationDiff := track.Duration - expectedDurationSec + if durationDiff < 0 { + durationDiff = -durationDiff + } + if durationDiff > 10 { + GoLog("[%s] Duration mismatch from %s: expected %ds, got %ds. Rejecting.\n", + logPrefix, source, expectedDurationSec, track.Duration) + return false + } + } + + return true +} + func (q *QobuzDownloader) searchQobuzTracksViaAPI(query string, limit int) ([]QobuzTrack, error) { searchURL := fmt.Sprintf("%s%s&limit=%d&app_id=%s", qobuzTrackSearchBaseURL, url.QueryEscape(query), limit, q.appID) req, err := http.NewRequest("GET", searchURL, nil) @@ -1247,6 +1910,19 @@ type QobuzDownloadResult struct { LyricsLRC string } +func parseQobuzRequestTrackID(raw string) int64 { + trimmed := strings.TrimSpace(raw) + trimmed = strings.TrimPrefix(trimmed, "qobuz:") + if trimmed == "" { + return 0 + } + var trackID int64 + if _, err := fmt.Sscanf(trimmed, "%d", &trackID); err != nil || trackID <= 0 { + return 0 + } + return trackID +} + func resolveQobuzTrackForRequest(req DownloadRequest, downloader *QobuzDownloader, logPrefix string) (*QobuzTrack, error) { if downloader == nil { downloader = NewQobuzDownloader() @@ -1260,17 +1936,20 @@ func resolveQobuzTrackForRequest(req DownloadRequest, downloader *QobuzDownloade var track *QobuzTrack var err error - // Strategy 1: Use Qobuz ID from Odesli enrichment (fastest, most accurate) + // Strategy 1: Use Qobuz ID from request payload (fastest, most accurate) if req.QobuzID != "" { - GoLog("[%s] Using Qobuz ID from Odesli enrichment: %s\n", logPrefix, req.QobuzID) - var trackID int64 - if _, parseErr := fmt.Sscanf(req.QobuzID, "%d", &trackID); parseErr == nil && trackID > 0 { - track, err = downloader.GetTrackByID(trackID) + GoLog("[%s] Using Qobuz ID from request payload: %s\n", logPrefix, req.QobuzID) + if trackID := parseQobuzRequestTrackID(req.QobuzID); trackID > 0 { + track, err = qobuzGetTrackByIDFunc(downloader, trackID) if err != nil { - GoLog("[%s] Failed to get track by Odesli ID %d: %v\n", logPrefix, trackID, err) + GoLog("[%s] Failed to get track by request Qobuz ID %d: %v\n", logPrefix, trackID, err) track = nil } else if track != nil { - GoLog("[%s] Successfully found track via Odesli ID: '%s' by '%s'\n", logPrefix, track.Title, track.Performer.Name) + if qobuzTrackMatchesRequest(req, track, logPrefix, "request Qobuz ID") { + GoLog("[%s] Successfully found track via request Qobuz ID: '%s' by '%s'\n", logPrefix, track.Title, track.Performer.Name) + } else { + track = nil + } } } } @@ -1279,10 +1958,12 @@ func resolveQobuzTrackForRequest(req DownloadRequest, downloader *QobuzDownloade if track == nil && req.ISRC != "" { if cached := GetTrackIDCache().Get(req.ISRC); cached != nil && cached.QobuzTrackID > 0 { GoLog("[%s] Cache hit! Using cached track ID: %d\n", logPrefix, cached.QobuzTrackID) - track, err = downloader.GetTrackByID(cached.QobuzTrackID) + track, err = qobuzGetTrackByIDFunc(downloader, cached.QobuzTrackID) if err != nil { GoLog("[%s] Cache hit but GetTrackByID failed: %v\n", logPrefix, err) track = nil + } else if track != nil && !qobuzTrackMatchesRequest(req, track, logPrefix, "cached Qobuz ID") { + track = nil } } } @@ -1291,19 +1972,23 @@ func resolveQobuzTrackForRequest(req DownloadRequest, downloader *QobuzDownloade if track == nil && req.SpotifyID != "" && req.QobuzID == "" { GoLog("[%s] Trying to get Qobuz ID from SongLink for Spotify ID: %s\n", logPrefix, req.SpotifyID) songLinkClient := NewSongLinkClient() - availability, slErr := songLinkClient.CheckTrackAvailability(req.SpotifyID, req.ISRC) + availability, slErr := songLinkCheckTrackAvailabilityFunc(songLinkClient, req.SpotifyID, req.ISRC) if slErr == nil && availability != nil && availability.QobuzID != "" { var trackID int64 if _, parseErr := fmt.Sscanf(availability.QobuzID, "%d", &trackID); parseErr == nil && trackID > 0 { GoLog("[%s] Got Qobuz ID %d from SongLink\n", logPrefix, trackID) - track, err = downloader.GetTrackByID(trackID) + track, err = qobuzGetTrackByIDFunc(downloader, trackID) if err != nil { GoLog("[%s] Failed to get track by SongLink ID %d: %v\n", logPrefix, trackID, err) track = nil } else if track != nil { - GoLog("[%s] Successfully found track via SongLink ID: '%s' by '%s'\n", logPrefix, track.Title, track.Performer.Name) - if req.ISRC != "" { - GetTrackIDCache().SetQobuz(req.ISRC, track.ID) + if qobuzTrackMatchesRequest(req, track, logPrefix, "SongLink Qobuz ID") { + GoLog("[%s] Successfully found track via SongLink ID: '%s' by '%s'\n", logPrefix, track.Title, track.Performer.Name) + if req.ISRC != "" { + GetTrackIDCache().SetQobuz(req.ISRC, track.ID) + } + } else { + track = nil } } } @@ -1313,27 +1998,17 @@ func resolveQobuzTrackForRequest(req DownloadRequest, downloader *QobuzDownloade // Strategy 4: ISRC search with duration verification if track == nil && req.ISRC != "" { GoLog("[%s] Trying ISRC search: %s\n", logPrefix, req.ISRC) - track, err = downloader.SearchTrackByISRCWithDuration(req.ISRC, expectedDurationSec) - if track != nil { - if !qobuzArtistsMatch(req.ArtistName, track.Performer.Name) { - GoLog("[%s] Artist mismatch from ISRC search: expected '%s', got '%s'. Rejecting.\n", - logPrefix, req.ArtistName, track.Performer.Name) - track = nil - } else if !qobuzTitlesMatch(req.TrackName, track.Title) { - GoLog("[%s] Title mismatch from ISRC search: expected '%s', got '%s'. Rejecting.\n", - logPrefix, req.TrackName, track.Title) - track = nil - } + track, err = qobuzSearchTrackByISRCWithDurationFunc(downloader, req.ISRC, expectedDurationSec) + if track != nil && !qobuzTrackMatchesRequest(req, track, logPrefix, "ISRC search") { + track = nil } } // Strategy 5: Metadata search with strict matching (duration tolerance: 10 seconds) if track == nil { GoLog("[%s] Trying metadata search: '%s' by '%s'\n", logPrefix, req.TrackName, req.ArtistName) - track, err = downloader.SearchTrackByMetadataWithDuration(req.TrackName, req.ArtistName, expectedDurationSec) - if track != nil && !qobuzArtistsMatch(req.ArtistName, track.Performer.Name) { - GoLog("[%s] Artist mismatch from metadata search: expected '%s', got '%s'. Rejecting.\n", - logPrefix, req.ArtistName, track.Performer.Name) + track, err = qobuzSearchTrackByMetadataWithDurationFunc(downloader, req.TrackName, req.ArtistName, expectedDurationSec) + if track != nil && !qobuzTrackMatchesRequest(req, track, logPrefix, "metadata search") { track = nil } } diff --git a/go_backend/qobuz_test.go b/go_backend/qobuz_test.go index 0382b62f..d19629b6 100644 --- a/go_backend/qobuz_test.go +++ b/go_backend/qobuz_test.go @@ -2,6 +2,95 @@ package gobackend import "testing" +func TestParseQobuzURL(t *testing.T) { + tests := []struct { + name string + input string + wantType string + wantID string + expectErr bool + }{ + { + name: "store album url", + input: "https://www.qobuz.com/us-en/album/harry-styles-harry-styles/0886446451985", + wantType: "album", + wantID: "0886446451985", + }, + { + name: "store playlist url", + input: "https://www.qobuz.com/us-en/playlists/new-releases/2049430", + wantType: "playlist", + wantID: "2049430", + }, + { + name: "store artist url", + input: "https://www.qobuz.com/us-en/interpreter/harry-styles/729886", + wantType: "artist", + wantID: "729886", + }, + { + name: "play track url", + input: "https://play.qobuz.com/track/40681594", + wantType: "track", + wantID: "40681594", + }, + { + name: "custom scheme playlist url", + input: "qobuzapp://playlist/2049430", + wantType: "playlist", + wantID: "2049430", + }, + { + name: "unsupported url", + input: "https://example.com/not-qobuz", + expectErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotType, gotID, err := parseQobuzURL(test.input) + if test.expectErr { + if err == nil { + t.Fatalf("expected error, got none") + } + return + } + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if gotType != test.wantType || gotID != test.wantID { + t.Fatalf("parseQobuzURL(%q) = (%q, %q), want (%q, %q)", test.input, gotType, gotID, test.wantType, test.wantID) + } + }) + } +} + +func TestExtractQobuzArtistAlbumIDs(t *testing.T) { + body := []byte(` +
    + +
    +
    + +
    +
    + +
    +`) + + matches := qobuzArtistAlbumIDRegex.FindAllSubmatch(body, -1) + if len(matches) != 3 { + t.Fatalf("expected 3 regex matches, got %d", len(matches)) + } + if string(matches[0][1]) != "yrpbt0lwm3g0y" { + t.Fatalf("unexpected first album id: %q", matches[0][1]) + } + if string(matches[2][1]) != "0886446451985" { + t.Fatalf("unexpected last album id: %q", matches[2][1]) + } +} + func TestExtractQobuzDownloadURLFromBody(t *testing.T) { t.Run("reads top-level download_url and quality metadata", func(t *testing.T) { body := []byte(`{"success":true,"download_url":"https://example.test/new.flac","bit_depth":24,"sampling_rate":96}`) @@ -106,6 +195,22 @@ func TestGetQobuzDebugKey(t *testing.T) { } } +func TestExtractQobuzAlbumIDsFromArtistHTML(t *testing.T) { + body := []byte(` + + + + `) + + got := extractQobuzAlbumIDsFromArtistHTML(body) + if len(got) != 2 { + t.Fatalf("expected 2 unique album IDs, got %d (%v)", len(got), got) + } + if got[0] != "0886446451985" || got[1] != "pvv406bth40ya" { + t.Fatalf("unexpected album IDs: %v", got) + } +} + func TestQobuzAvailableProviders(t *testing.T) { providers := NewQobuzDownloader().GetAvailableProviders() if len(providers) != 3 { @@ -133,3 +238,174 @@ func TestQobuzAvailableProviders(t *testing.T) { t.Fatalf("missing providers: %v", want) } } + +func testQobuzTrack(id int64, title, artist string, duration int) *QobuzTrack { + track := &QobuzTrack{ + ID: id, + Title: title, + Duration: duration, + } + track.Performer.Name = artist + return track +} + +func TestResolveQobuzTrackForRequestRejectsSongLinkMismatch(t *testing.T) { + origGetTrackByID := qobuzGetTrackByIDFunc + origSearchISRC := qobuzSearchTrackByISRCWithDurationFunc + origSearchMetadata := qobuzSearchTrackByMetadataWithDurationFunc + origSongLinkCheck := songLinkCheckTrackAvailabilityFunc + t.Cleanup(func() { + qobuzGetTrackByIDFunc = origGetTrackByID + qobuzSearchTrackByISRCWithDurationFunc = origSearchISRC + qobuzSearchTrackByMetadataWithDurationFunc = origSearchMetadata + songLinkCheckTrackAvailabilityFunc = origSongLinkCheck + GetTrackIDCache().Clear() + }) + GetTrackIDCache().Clear() + + qobuzGetTrackByIDFunc = func(_ *QobuzDownloader, trackID int64) (*QobuzTrack, error) { + if trackID != 111 { + t.Fatalf("unexpected track ID lookup: %d", trackID) + } + return testQobuzTrack(111, "Aperture", "Harry Styles", 180), nil + } + qobuzSearchTrackByISRCWithDurationFunc = func(_ *QobuzDownloader, isrc string, expectedDurationSec int) (*QobuzTrack, error) { + if isrc != "TESTISRC1" { + t.Fatalf("unexpected ISRC lookup: %q", isrc) + } + if expectedDurationSec != 180 { + t.Fatalf("unexpected duration: %d", expectedDurationSec) + } + return testQobuzTrack(222, "Taste Back", "Harry Styles", 180), nil + } + qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, _, _ string, _ int) (*QobuzTrack, error) { + t.Fatal("metadata fallback should not run when ISRC fallback succeeds") + return nil, nil + } + songLinkCheckTrackAvailabilityFunc = func(_ *SongLinkClient, spotifyTrackID string, isrc string) (*TrackAvailability, error) { + if spotifyTrackID != "spotify-track-id" { + t.Fatalf("unexpected spotify ID: %q", spotifyTrackID) + } + if isrc != "TESTISRC1" { + t.Fatalf("unexpected SongLink ISRC: %q", isrc) + } + return &TrackAvailability{QobuzID: "111"}, nil + } + + req := DownloadRequest{ + ISRC: "TESTISRC1", + SpotifyID: "spotify-track-id", + TrackName: "Taste Back", + ArtistName: "Harry Styles", + DurationMS: 180000, + } + + track, err := resolveQobuzTrackForRequest(req, &QobuzDownloader{}, "Test") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if track == nil || track.ID != 222 || track.Title != "Taste Back" { + t.Fatalf("unexpected resolved track: %+v", track) + } + + cached := GetTrackIDCache().Get(req.ISRC) + if cached == nil || cached.QobuzTrackID != 222 { + t.Fatalf("expected validated fallback track to be cached, got %+v", cached) + } +} + +func TestResolveQobuzTrackForRequestRejectsOdesliMismatch(t *testing.T) { + origGetTrackByID := qobuzGetTrackByIDFunc + origSearchISRC := qobuzSearchTrackByISRCWithDurationFunc + origSearchMetadata := qobuzSearchTrackByMetadataWithDurationFunc + origSongLinkCheck := songLinkCheckTrackAvailabilityFunc + t.Cleanup(func() { + qobuzGetTrackByIDFunc = origGetTrackByID + qobuzSearchTrackByISRCWithDurationFunc = origSearchISRC + qobuzSearchTrackByMetadataWithDurationFunc = origSearchMetadata + songLinkCheckTrackAvailabilityFunc = origSongLinkCheck + }) + + qobuzGetTrackByIDFunc = func(_ *QobuzDownloader, trackID int64) (*QobuzTrack, error) { + if trackID != 333 { + t.Fatalf("unexpected track ID lookup: %d", trackID) + } + return testQobuzTrack(333, "American Girls", "Harry Styles", 181), nil + } + qobuzSearchTrackByISRCWithDurationFunc = func(_ *QobuzDownloader, _ string, _ int) (*QobuzTrack, error) { + t.Fatal("ISRC fallback should not run without an ISRC") + return nil, nil + } + qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, trackName, artistName string, expectedDurationSec int) (*QobuzTrack, error) { + if trackName != "Taste Back" || artistName != "Harry Styles" || expectedDurationSec != 181 { + t.Fatalf("unexpected metadata fallback arguments: %q / %q / %d", trackName, artistName, expectedDurationSec) + } + return testQobuzTrack(444, "Taste Back", "Harry Styles", 181), nil + } + songLinkCheckTrackAvailabilityFunc = func(_ *SongLinkClient, _, _ string) (*TrackAvailability, error) { + t.Fatal("SongLink should not run when Odesli QobuzID is provided") + return nil, nil + } + + req := DownloadRequest{ + QobuzID: "333", + TrackName: "Taste Back", + ArtistName: "Harry Styles", + DurationMS: 181000, + } + + track, err := resolveQobuzTrackForRequest(req, &QobuzDownloader{}, "Test") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if track == nil || track.ID != 444 || track.Title != "Taste Back" { + t.Fatalf("unexpected resolved track: %+v", track) + } +} + +func TestResolveQobuzTrackForRequestUsesPrefixedQobuzIDWithoutSongLink(t *testing.T) { + origGetTrackByID := qobuzGetTrackByIDFunc + origSearchISRC := qobuzSearchTrackByISRCWithDurationFunc + origSearchMetadata := qobuzSearchTrackByMetadataWithDurationFunc + origSongLinkCheck := songLinkCheckTrackAvailabilityFunc + t.Cleanup(func() { + qobuzGetTrackByIDFunc = origGetTrackByID + qobuzSearchTrackByISRCWithDurationFunc = origSearchISRC + qobuzSearchTrackByMetadataWithDurationFunc = origSearchMetadata + songLinkCheckTrackAvailabilityFunc = origSongLinkCheck + }) + + qobuzGetTrackByIDFunc = func(_ *QobuzDownloader, trackID int64) (*QobuzTrack, error) { + if trackID != 40681594 { + t.Fatalf("unexpected track ID lookup: %d", trackID) + } + return testQobuzTrack(40681594, "Sign of the Times", "Harry Styles", 341), nil + } + qobuzSearchTrackByISRCWithDurationFunc = func(_ *QobuzDownloader, _ string, _ int) (*QobuzTrack, error) { + t.Fatal("ISRC fallback should not run when request qobuz id succeeds") + return nil, nil + } + qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, _, _ string, _ int) (*QobuzTrack, error) { + t.Fatal("metadata fallback should not run when request qobuz id succeeds") + return nil, nil + } + songLinkCheckTrackAvailabilityFunc = func(_ *SongLinkClient, _, _ string) (*TrackAvailability, error) { + t.Fatal("SongLink should not run when request qobuz id is provided") + return nil, nil + } + + req := DownloadRequest{ + QobuzID: "qobuz:40681594", + TrackName: "Sign of the Times", + ArtistName: "Harry Styles", + DurationMS: 341000, + } + + track, err := resolveQobuzTrackForRequest(req, &QobuzDownloader{}, "Test") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if track == nil || track.ID != 40681594 { + t.Fatalf("unexpected resolved track: %+v", track) + } +} diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 1e540110..1ede9dba 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -14,6 +14,7 @@ import ( "os" "path/filepath" "regexp" + "strconv" "strings" "sync" "time" @@ -32,6 +33,12 @@ var ( const ( spotifyTrackBaseURL = "https://open.spotify.com/track/" songLinkLookupBaseURL = "https://api.song.link/v1-alpha.1/links?url=" + tidalPublicAPIBaseURL = "https://tidal.com/v1" + tidalPublicToken = "txNoH4kkV41MfH25" + tidalResourceBaseURL = "https://resources.tidal.com" + tidalCountryCode = "US" + tidalLocale = "en_US" + tidalDeviceType = "BROWSER" ) type TidalTrack struct { @@ -43,19 +50,28 @@ type TidalTrack struct { VolumeNumber int `json:"volumeNumber"` Duration int `json:"duration"` Album struct { + ID int64 `json:"id"` Title string `json:"title"` Cover string `json:"cover"` ReleaseDate string `json:"releaseDate"` + URL string `json:"url"` } `json:"album"` Artists []struct { - Name string `json:"name"` + ID int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Picture string `json:"picture"` } `json:"artists"` Artist struct { - Name string `json:"name"` + ID int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Picture string `json:"picture"` } `json:"artist"` MediaMetadata struct { Tags []string `json:"tags"` } `json:"mediaMetadata"` + URL string `json:"url"` } type TidalAPIResponseV2 struct { @@ -100,6 +116,97 @@ type MPD struct { } `xml:"Period"` } +type tidalPublicArtist struct { + ID int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Picture string `json:"picture"` +} + +type tidalPublicAlbum struct { + ID int64 `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + Cover string `json:"cover"` + ReleaseDate string `json:"releaseDate"` + URL string `json:"url"` + NumberOfTracks int `json:"numberOfTracks"` + Explicit bool `json:"explicit"` + Artists []tidalPublicArtist `json:"artists"` +} + +type tidalPublicAlbumPage struct { + Rows []struct { + Modules []struct { + Type string `json:"type"` + Album tidalPublicAlbum `json:"album"` + PagedList struct { + DataAPIPath string `json:"dataApiPath"` + Limit int `json:"limit"` + Offset int `json:"offset"` + TotalNumberOfItems int `json:"totalNumberOfItems"` + Items []struct { + Item TidalTrack `json:"item"` + Type string `json:"type"` + } `json:"items"` + } `json:"pagedList"` + } `json:"modules"` + } `json:"rows"` +} + +type tidalPublicArtistPage struct { + Rows []struct { + Modules []struct { + Type string `json:"type"` + Artist struct { + ID int64 `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Picture string `json:"picture"` + } `json:"artist"` + PagedList struct { + DataAPIPath string `json:"dataApiPath"` + Limit int `json:"limit"` + Offset int `json:"offset"` + TotalNumberOfItems int `json:"totalNumberOfItems"` + Items []tidalPublicAlbum `json:"items"` + } `json:"pagedList"` + } `json:"modules"` + } `json:"rows"` +} + +type tidalPublicArtistAlbumsPage struct { + Limit int `json:"limit"` + Offset int `json:"offset"` + TotalNumberOfItems int `json:"totalNumberOfItems"` + Items []tidalPublicAlbum `json:"items"` +} + +type tidalPublicPlaylist struct { + UUID string `json:"uuid"` + Title string `json:"title"` + Description string `json:"description"` + Type string `json:"type"` + URL string `json:"url"` + Image string `json:"image"` + SquareImage string `json:"squareImage"` + NumberOfTracks int `json:"numberOfTracks"` + Creator struct { + ID int64 `json:"id"` + Name string `json:"name"` + } `json:"creator"` +} + +type tidalPublicPlaylistItemsPage struct { + Limit int `json:"limit"` + Offset int `json:"offset"` + TotalNumberOfItems int `json:"totalNumberOfItems"` + Items []struct { + Item TidalTrack `json:"item"` + Type string `json:"type"` + } `json:"items"` +} + func NewTidalDownloader() *TidalDownloader { tidalDownloaderOnce.Do(func() { globalTidalDownloader = &TidalDownloader{ @@ -114,6 +221,408 @@ func NewTidalDownloader() *TidalDownloader { return globalTidalDownloader } +func tidalPrefixedID(id string) string { + trimmed := strings.TrimSpace(id) + if trimmed == "" { + return "" + } + return "tidal:" + trimmed +} + +func tidalPrefixedNumericID(id int64) string { + if id <= 0 { + return "" + } + return fmt.Sprintf("tidal:%d", id) +} + +func tidalImageURL(imageID, size string) string { + normalizedID := strings.TrimSpace(imageID) + if normalizedID == "" || strings.TrimSpace(size) == "" { + return "" + } + return fmt.Sprintf( + "%s/images/%s/%s.jpg", + tidalResourceBaseURL, + strings.ReplaceAll(normalizedID, "-", "/"), + size, + ) +} + +func tidalFirstNonEmpty(values ...string) string { + for _, value := range values { + if trimmed := strings.TrimSpace(value); trimmed != "" { + return trimmed + } + } + return "" +} + +func tidalJoinArtistNames(artists []tidalPublicArtist) string { + if len(artists) == 0 { + return "" + } + + names := make([]string, 0, len(artists)) + for _, artist := range artists { + if trimmed := strings.TrimSpace(artist.Name); trimmed != "" { + names = append(names, trimmed) + } + } + return strings.Join(names, ", ") +} + +func tidalTrackArtistsDisplay(track *TidalTrack) string { + if track == nil { + return "" + } + + if len(track.Artists) > 0 { + names := make([]string, 0, len(track.Artists)) + for _, artist := range track.Artists { + if trimmed := strings.TrimSpace(artist.Name); trimmed != "" { + names = append(names, trimmed) + } + } + if len(names) > 0 { + return strings.Join(names, ", ") + } + } + + return strings.TrimSpace(track.Artist.Name) +} + +func tidalAlbumArtistsDisplay(album *tidalPublicAlbum) string { + if album == nil { + return "" + } + return tidalJoinArtistNames(album.Artists) +} + +func tidalTrackExternalURL(track *TidalTrack) string { + if track == nil { + return "" + } + if trimmed := strings.TrimSpace(track.URL); trimmed != "" { + return strings.Replace(trimmed, "http://", "https://", 1) + } + if track.ID > 0 { + return fmt.Sprintf("https://tidal.com/browse/track/%d", track.ID) + } + return "" +} + +func tidalAlbumExternalURL(album *tidalPublicAlbum) string { + if album == nil { + return "" + } + if trimmed := strings.TrimSpace(album.URL); trimmed != "" { + return strings.Replace(trimmed, "http://", "https://", 1) + } + if album.ID > 0 { + return fmt.Sprintf("https://tidal.com/browse/album/%d", album.ID) + } + return "" +} + +func tidalTrackToTrackMetadata(track *TidalTrack) TrackMetadata { + if track == nil { + return TrackMetadata{} + } + + artistID := tidalPrefixedNumericID(track.Artist.ID) + if artistID == "" && len(track.Artists) > 0 { + artistID = tidalPrefixedNumericID(track.Artists[0].ID) + } + + return TrackMetadata{ + SpotifyID: tidalPrefixedNumericID(track.ID), + Artists: tidalTrackArtistsDisplay(track), + Name: strings.TrimSpace(track.Title), + AlbumName: strings.TrimSpace(track.Album.Title), + AlbumArtist: strings.TrimSpace(track.Artist.Name), + DurationMS: track.Duration * 1000, + Images: tidalImageURL(track.Album.Cover, "1280x1280"), + ReleaseDate: strings.TrimSpace(track.Album.ReleaseDate), + TrackNumber: track.TrackNumber, + DiscNumber: track.VolumeNumber, + ExternalURL: tidalTrackExternalURL(track), + ISRC: strings.TrimSpace(track.ISRC), + AlbumID: tidalPrefixedNumericID(track.Album.ID), + ArtistID: artistID, + } +} + +func tidalTrackToAlbumTrackMetadata(track *TidalTrack) AlbumTrackMetadata { + if track == nil { + return AlbumTrackMetadata{} + } + + return AlbumTrackMetadata{ + SpotifyID: tidalPrefixedNumericID(track.ID), + Artists: tidalTrackArtistsDisplay(track), + Name: strings.TrimSpace(track.Title), + AlbumName: strings.TrimSpace(track.Album.Title), + AlbumArtist: strings.TrimSpace(track.Artist.Name), + DurationMS: track.Duration * 1000, + Images: tidalImageURL(track.Album.Cover, "1280x1280"), + ReleaseDate: strings.TrimSpace(track.Album.ReleaseDate), + TrackNumber: track.TrackNumber, + DiscNumber: track.VolumeNumber, + ExternalURL: tidalTrackExternalURL(track), + ISRC: strings.TrimSpace(track.ISRC), + AlbumID: tidalPrefixedNumericID(track.Album.ID), + AlbumURL: strings.Replace(strings.TrimSpace(track.Album.URL), "http://", "https://", 1), + } +} + +func tidalAlbumToAlbumInfo(album *tidalPublicAlbum) AlbumInfoMetadata { + if album == nil { + return AlbumInfoMetadata{} + } + + artistID := "" + if len(album.Artists) > 0 { + artistID = tidalPrefixedNumericID(album.Artists[0].ID) + } + + return AlbumInfoMetadata{ + TotalTracks: album.NumberOfTracks, + Name: strings.TrimSpace(album.Title), + ReleaseDate: strings.TrimSpace(album.ReleaseDate), + Artists: tidalAlbumArtistsDisplay(album), + ArtistId: artistID, + Images: tidalImageURL(album.Cover, "1280x1280"), + } +} + +func tidalAlbumToArtistAlbum(album *tidalPublicAlbum) ArtistAlbumMetadata { + if album == nil { + return ArtistAlbumMetadata{} + } + + return ArtistAlbumMetadata{ + ID: tidalPrefixedNumericID(album.ID), + Name: strings.TrimSpace(album.Title), + ReleaseDate: strings.TrimSpace(album.ReleaseDate), + TotalTracks: album.NumberOfTracks, + Images: tidalImageURL(album.Cover, "1280x1280"), + AlbumType: strings.ToLower(strings.TrimSpace(album.Type)), + Artists: tidalAlbumArtistsDisplay(album), + } +} + +func tidalPlaylistOwnerName(playlist *tidalPublicPlaylist) string { + if playlist == nil { + return "" + } + if trimmed := strings.TrimSpace(playlist.Creator.Name); trimmed != "" { + return trimmed + } + if strings.EqualFold(strings.TrimSpace(playlist.Type), "ARTIST") { + return "Artist" + } + return "TIDAL" +} + +func tidalBuildMetadataURL(path string, extraQuery url.Values) string { + trimmedPath := strings.TrimLeft(strings.TrimSpace(path), "/") + if trimmedPath == "" { + return tidalPublicAPIBaseURL + } + + baseURL, err := url.Parse(tidalPublicAPIBaseURL + "/" + trimmedPath) + if err != nil { + return tidalPublicAPIBaseURL + "/" + trimmedPath + } + + query := baseURL.Query() + query.Set("countryCode", tidalCountryCode) + query.Set("locale", tidalLocale) + query.Set("deviceType", tidalDeviceType) + for key, values := range extraQuery { + query.Del(key) + for _, value := range values { + query.Add(key, value) + } + } + baseURL.RawQuery = query.Encode() + return baseURL.String() +} + +func (t *TidalDownloader) getTidalMetadataJSON(requestURL string, target interface{}) error { + req, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + return err + } + req.Header.Set("Accept", "application/json") + req.Header.Set("x-tidal-token", tidalPublicToken) + + resp, err := DoRequestWithUserAgent(t.client, req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) + return fmt.Errorf("tidal metadata request failed: HTTP %d (%s)", resp.StatusCode, strings.TrimSpace(string(body))) + } + + return json.NewDecoder(resp.Body).Decode(target) +} + +func (t *TidalDownloader) getPublicTrack(resourceID string) (*TidalTrack, error) { + trackID, err := strconv.ParseInt(strings.TrimSpace(resourceID), 10, 64) + if err != nil || trackID <= 0 { + return nil, fmt.Errorf("invalid tidal track ID: %s", resourceID) + } + + requestURL := tidalBuildMetadataURL(fmt.Sprintf("tracks/%d", trackID), nil) + var track TidalTrack + if err := t.getTidalMetadataJSON(requestURL, &track); err != nil { + return nil, err + } + return &track, nil +} + +func (t *TidalDownloader) getAlbumPage(resourceID string) (*tidalPublicAlbumPage, error) { + albumID := strings.TrimSpace(resourceID) + if albumID == "" { + return nil, fmt.Errorf("invalid tidal album ID") + } + + requestURL := tidalBuildMetadataURL("pages/album", url.Values{"albumId": {albumID}}) + var page tidalPublicAlbumPage + if err := t.getTidalMetadataJSON(requestURL, &page); err != nil { + return nil, err + } + return &page, nil +} + +func (t *TidalDownloader) getArtistPage(resourceID string) (*tidalPublicArtistPage, error) { + artistID := strings.TrimSpace(resourceID) + if artistID == "" { + return nil, fmt.Errorf("invalid tidal artist ID") + } + + requestURL := tidalBuildMetadataURL("pages/artist", url.Values{"artistId": {artistID}}) + var page tidalPublicArtistPage + if err := t.getTidalMetadataJSON(requestURL, &page); err != nil { + return nil, err + } + return &page, nil +} + +func (t *TidalDownloader) getArtistAlbumsPage(dataAPIPath string, offset, limit int) (*tidalPublicArtistAlbumsPage, error) { + extraQuery := url.Values{} + if offset >= 0 { + extraQuery.Set("offset", strconv.Itoa(offset)) + } + if limit > 0 { + extraQuery.Set("limit", strconv.Itoa(limit)) + } + + requestURL := tidalBuildMetadataURL(dataAPIPath, extraQuery) + var page tidalPublicArtistAlbumsPage + if err := t.getTidalMetadataJSON(requestURL, &page); err != nil { + return nil, err + } + return &page, nil +} + +func (t *TidalDownloader) getPlaylist(resourceID string) (*tidalPublicPlaylist, error) { + playlistID := strings.TrimSpace(resourceID) + if playlistID == "" { + return nil, fmt.Errorf("invalid tidal playlist ID") + } + + requestURL := tidalBuildMetadataURL("playlists/"+url.PathEscape(playlistID), nil) + var playlist tidalPublicPlaylist + if err := t.getTidalMetadataJSON(requestURL, &playlist); err != nil { + return nil, err + } + return &playlist, nil +} + +func (t *TidalDownloader) getPlaylistItemsPage(resourceID string, offset, limit int) (*tidalPublicPlaylistItemsPage, error) { + playlistID := strings.TrimSpace(resourceID) + if playlistID == "" { + return nil, fmt.Errorf("invalid tidal playlist ID") + } + + requestURL := tidalBuildMetadataURL( + "playlists/"+url.PathEscape(playlistID)+"/items", + url.Values{ + "offset": {strconv.Itoa(offset)}, + "limit": {strconv.Itoa(limit)}, + }, + ) + var page tidalPublicPlaylistItemsPage + if err := t.getTidalMetadataJSON(requestURL, &page); err != nil { + return nil, err + } + return &page, nil +} + +func findTidalAlbumPageModule(page *tidalPublicAlbumPage, moduleType string) *struct { + Type string `json:"type"` + Album tidalPublicAlbum `json:"album"` + PagedList struct { + DataAPIPath string `json:"dataApiPath"` + Limit int `json:"limit"` + Offset int `json:"offset"` + TotalNumberOfItems int `json:"totalNumberOfItems"` + Items []struct { + Item TidalTrack `json:"item"` + Type string `json:"type"` + } `json:"items"` + } `json:"pagedList"` +} { + if page == nil { + return nil + } + for rowIndex := range page.Rows { + for moduleIndex := range page.Rows[rowIndex].Modules { + module := &page.Rows[rowIndex].Modules[moduleIndex] + if module.Type == moduleType { + return module + } + } + } + return nil +} + +func findTidalArtistPageModule(page *tidalPublicArtistPage, moduleType string) *struct { + Type string `json:"type"` + Artist struct { + ID int64 `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Picture string `json:"picture"` + } `json:"artist"` + PagedList struct { + DataAPIPath string `json:"dataApiPath"` + Limit int `json:"limit"` + Offset int `json:"offset"` + TotalNumberOfItems int `json:"totalNumberOfItems"` + Items []tidalPublicAlbum `json:"items"` + } `json:"pagedList"` +} { + if page == nil { + return nil + } + for rowIndex := range page.Rows { + for moduleIndex := range page.Rows[rowIndex].Modules { + module := &page.Rows[rowIndex].Modules[moduleIndex] + if module.Type == moduleType { + return module + } + } + } + return nil +} + func (t *TidalDownloader) GetAvailableAPIs() []string { return []string{ "https://tidal-api.binimum.org", @@ -203,6 +712,146 @@ func (t *TidalDownloader) SearchTrackByMetadata(trackName, artistName string) (* return nil, fmt.Errorf("tidal metadata search API disabled: no client credentials mode") } +func (t *TidalDownloader) GetTrackMetadata(resourceID string) (*TrackResponse, error) { + track, err := t.getPublicTrack(resourceID) + if err != nil { + return nil, err + } + return &TrackResponse{Track: tidalTrackToTrackMetadata(track)}, nil +} + +func (t *TidalDownloader) GetAlbumMetadata(resourceID string) (*AlbumResponsePayload, error) { + page, err := t.getAlbumPage(resourceID) + if err != nil { + return nil, err + } + + headerModule := findTidalAlbumPageModule(page, "ALBUM_HEADER") + itemsModule := findTidalAlbumPageModule(page, "ALBUM_ITEMS") + if headerModule == nil { + return nil, fmt.Errorf("tidal album page missing album header") + } + if itemsModule == nil { + return nil, fmt.Errorf("tidal album page missing track list") + } + + tracks := make([]AlbumTrackMetadata, 0, len(itemsModule.PagedList.Items)) + for _, item := range itemsModule.PagedList.Items { + track := item.Item + if track.Album.ID == 0 { + track.Album.ID = headerModule.Album.ID + track.Album.Title = headerModule.Album.Title + track.Album.Cover = headerModule.Album.Cover + track.Album.ReleaseDate = headerModule.Album.ReleaseDate + track.Album.URL = headerModule.Album.URL + } + tracks = append(tracks, tidalTrackToAlbumTrackMetadata(&track)) + } + + return &AlbumResponsePayload{ + AlbumInfo: tidalAlbumToAlbumInfo(&headerModule.Album), + TrackList: tracks, + }, nil +} + +func (t *TidalDownloader) GetPlaylistMetadata(resourceID string) (*PlaylistResponsePayload, error) { + playlist, err := t.getPlaylist(resourceID) + if err != nil { + return nil, err + } + + const pageSize = 50 + offset := 0 + totalTracks := playlist.NumberOfTracks + tracks := make([]AlbumTrackMetadata, 0, totalTracks) + + for { + page, pageErr := t.getPlaylistItemsPage(resourceID, offset, pageSize) + if pageErr != nil { + return nil, pageErr + } + if totalTracks == 0 && page.TotalNumberOfItems > 0 { + totalTracks = page.TotalNumberOfItems + } + + for _, item := range page.Items { + if item.Type != "track" { + continue + } + tracks = append(tracks, tidalTrackToAlbumTrackMetadata(&item.Item)) + } + + if len(page.Items) == 0 || offset+len(page.Items) >= totalTracks || len(page.Items) < pageSize { + break + } + offset += len(page.Items) + } + + var info PlaylistInfoMetadata + info.Tracks.Total = totalTracks + info.Name = strings.TrimSpace(playlist.Title) + info.Images = tidalImageURL(tidalFirstNonEmpty(playlist.SquareImage, playlist.Image), "1280x1280") + info.Owner.DisplayName = tidalPlaylistOwnerName(playlist) + info.Owner.Name = strings.TrimSpace(playlist.Title) + info.Owner.Images = info.Images + + return &PlaylistResponsePayload{ + PlaylistInfo: info, + TrackList: tracks, + }, nil +} + +func (t *TidalDownloader) GetArtistMetadata(resourceID string) (*ArtistResponsePayload, error) { + page, err := t.getArtistPage(resourceID) + if err != nil { + return nil, err + } + + headerModule := findTidalArtistPageModule(page, "ARTIST_HEADER") + albumsModule := findTidalArtistPageModule(page, "ALBUM_LIST") + if headerModule == nil { + return nil, fmt.Errorf("tidal artist page missing artist header") + } + if albumsModule == nil { + return nil, fmt.Errorf("tidal artist page missing albums list") + } + + albums := make([]ArtistAlbumMetadata, 0, albumsModule.PagedList.TotalNumberOfItems) + for _, album := range albumsModule.PagedList.Items { + albums = append(albums, tidalAlbumToArtistAlbum(&album)) + } + + pageSize := albumsModule.PagedList.Limit + if pageSize <= 0 { + pageSize = 50 + } + offset := len(albumsModule.PagedList.Items) + for offset < albumsModule.PagedList.TotalNumberOfItems && strings.TrimSpace(albumsModule.PagedList.DataAPIPath) != "" { + albumsPage, pageErr := t.getArtistAlbumsPage(albumsModule.PagedList.DataAPIPath, offset, pageSize) + if pageErr != nil { + return nil, pageErr + } + + for _, album := range albumsPage.Items { + albums = append(albums, tidalAlbumToArtistAlbum(&album)) + } + + if len(albumsPage.Items) == 0 || offset+len(albumsPage.Items) >= albumsPage.TotalNumberOfItems { + break + } + offset += len(albumsPage.Items) + } + + return &ArtistResponsePayload{ + ArtistInfo: ArtistInfoMetadata{ + ID: tidalPrefixedNumericID(headerModule.Artist.ID), + Name: strings.TrimSpace(headerModule.Artist.Name), + Images: tidalImageURL(headerModule.Artist.Picture, "750x750"), + }, + Albums: albums, + }, nil +} + type TidalDownloadInfo struct { URL string BitDepth int @@ -1062,20 +1711,18 @@ func isLatinScript(s string) bool { return true } -func tidalTrackArtistsDisplay(track *TidalTrack) string { - if track == nil { - return "" +func parseTidalRequestTrackID(raw string) (int64, bool) { + trimmed := strings.TrimSpace(raw) + trimmed = strings.TrimPrefix(trimmed, "tidal:") + if trimmed == "" { + return 0, false } - tidalArtist := track.Artist.Name - if len(track.Artists) > 0 { - var artistNames []string - for _, a := range track.Artists { - artistNames = append(artistNames, a.Name) - } - tidalArtist = strings.Join(artistNames, ", ") + trackID, err := strconv.ParseInt(trimmed, 10, 64) + if err != nil || trackID <= 0 { + return 0, false } - return tidalArtist + return trackID, true } func resolveTidalTrackForRequest(req DownloadRequest, downloader *TidalDownloader, logPrefix string) (*TidalTrack, error) { @@ -1091,8 +1738,9 @@ func resolveTidalTrackForRequest(req DownloadRequest, downloader *TidalDownloade var gotTidalID bool if req.TidalID != "" { - GoLog("[%s] Using Tidal ID from Odesli enrichment: %s\n", logPrefix, req.TidalID) - if _, parseErr := fmt.Sscanf(req.TidalID, "%d", &trackID); parseErr == nil && trackID > 0 { + GoLog("[%s] Using Tidal ID from request payload: %s\n", logPrefix, req.TidalID) + if parsedTrackID, ok := parseTidalRequestTrackID(req.TidalID); ok { + trackID = parsedTrackID gotTidalID = true } } @@ -1113,7 +1761,8 @@ func resolveTidalTrackForRequest(req DownloadRequest, downloader *TidalDownloade return } if availability.TidalID != "" { - if _, parseErr := fmt.Sscanf(availability.TidalID, "%d", &trackID); parseErr == nil && trackID > 0 { + if parsedTrackID, ok := parseTidalRequestTrackID(availability.TidalID); ok { + trackID = parsedTrackID GoLog("[%s] Got Tidal ID %d directly from SongLink\n", logPrefix, trackID) gotTidalID = true return diff --git a/go_backend/tidal_test.go b/go_backend/tidal_test.go new file mode 100644 index 00000000..647ebaf0 --- /dev/null +++ b/go_backend/tidal_test.go @@ -0,0 +1,181 @@ +package gobackend + +import "testing" + +func TestParseTidalURL(t *testing.T) { + tests := []struct { + name string + input string + wantType string + wantID string + expectErr bool + }{ + { + name: "track url", + input: "https://tidal.com/track/77616174", + wantType: "track", + wantID: "77616174", + }, + { + name: "browse album url", + input: "https://listen.tidal.com/browse/album/77616169", + wantType: "album", + wantID: "77616169", + }, + { + name: "artist url", + input: "https://www.tidal.com/artist/3852143", + wantType: "artist", + wantID: "3852143", + }, + { + name: "playlist url", + input: "https://tidal.com/playlist/edf3b7d2-cb42-41d7-93c0-afa2a395521b", + wantType: "playlist", + wantID: "edf3b7d2-cb42-41d7-93c0-afa2a395521b", + }, + { + name: "unsupported host", + input: "https://example.com/track/123", + expectErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotType, gotID, err := parseTidalURL(test.input) + if test.expectErr { + if err == nil { + t.Fatalf("expected error, got none") + } + return + } + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if gotType != test.wantType || gotID != test.wantID { + t.Fatalf("parseTidalURL(%q) = (%q, %q), want (%q, %q)", test.input, gotType, gotID, test.wantType, test.wantID) + } + }) + } +} + +func TestParseTidalRequestTrackID(t *testing.T) { + tests := []struct { + input string + want int64 + ok bool + }{ + {input: "40681594", want: 40681594, ok: true}, + {input: "tidal:40681594", want: 40681594, ok: true}, + {input: " tidal:40681594 ", want: 40681594, ok: true}, + {input: "", want: 0, ok: false}, + {input: "tidal:not-a-number", want: 0, ok: false}, + } + + for _, test := range tests { + got, ok := parseTidalRequestTrackID(test.input) + if got != test.want || ok != test.ok { + t.Fatalf("parseTidalRequestTrackID(%q) = (%d, %v), want (%d, %v)", test.input, got, ok, test.want, test.ok) + } + } +} + +func TestTidalImageURL(t *testing.T) { + got := tidalImageURL("fc18a64b-d76b-4582-962a-224cb05193f3", "1280x1280") + want := "https://resources.tidal.com/images/fc18a64b/d76b/4582/962a/224cb05193f3/1280x1280.jpg" + if got != want { + t.Fatalf("tidalImageURL() = %q, want %q", got, want) + } +} + +func TestTidalTrackToTrackMetadata(t *testing.T) { + track := &TidalTrack{ + ID: 77616174, + Title: "Bruckner: Symphony No. 5", + ISRC: "GBUM71507433", + Duration: 1172, + TrackNumber: 5, + VolumeNumber: 1, + URL: "http://www.tidal.com/track/77616174", + } + track.Artist.ID = 3852143 + track.Artist.Name = "Staatskapelle Berlin" + track.Artists = []struct { + ID int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Picture string `json:"picture"` + }{ + {ID: 3852143, Name: "Staatskapelle Berlin", Type: "MAIN"}, + {ID: 12430, Name: "Daniel Barenboim", Type: "FEATURED"}, + } + track.Album.ID = 77616169 + track.Album.Title = "Bruckner: Symphonies 4-9" + track.Album.Cover = "fc18a64b-d76b-4582-962a-224cb05193f3" + track.Album.ReleaseDate = "2016-02-26" + + got := tidalTrackToTrackMetadata(track) + if got.SpotifyID != "tidal:77616174" { + t.Fatalf("unexpected track ID: %q", got.SpotifyID) + } + if got.Artists != "Staatskapelle Berlin, Daniel Barenboim" { + t.Fatalf("unexpected artists: %q", got.Artists) + } + if got.AlbumID != "tidal:77616169" { + t.Fatalf("unexpected album ID: %q", got.AlbumID) + } + if got.ArtistID != "tidal:3852143" { + t.Fatalf("unexpected artist ID: %q", got.ArtistID) + } + if got.Images == "" || got.ExternalURL != "https://www.tidal.com/track/77616174" { + t.Fatalf("unexpected image/url: %q / %q", got.Images, got.ExternalURL) + } +} + +func TestTidalAlbumToArtistAlbum(t *testing.T) { + album := &tidalPublicAlbum{ + ID: 77616169, + Title: "Bruckner: Symphonies 4-9", + Type: "ALBUM", + Cover: "fc18a64b-d76b-4582-962a-224cb05193f3", + ReleaseDate: "2016-02-26", + NumberOfTracks: 23, + Artists: []tidalPublicArtist{ + {ID: 3852143, Name: "Staatskapelle Berlin", Type: "MAIN"}, + {ID: 12430, Name: "Daniel Barenboim", Type: "FEATURED"}, + }, + } + + got := tidalAlbumToArtistAlbum(album) + if got.ID != "tidal:77616169" { + t.Fatalf("unexpected album ID: %q", got.ID) + } + if got.AlbumType != "album" { + t.Fatalf("unexpected album type: %q", got.AlbumType) + } + if got.Artists != "Staatskapelle Berlin, Daniel Barenboim" { + t.Fatalf("unexpected artists: %q", got.Artists) + } + if got.Images == "" { + t.Fatalf("expected image URL, got empty string") + } +} + +func TestTidalPlaylistOwnerName(t *testing.T) { + editorial := &tidalPublicPlaylist{Type: "EDITORIAL"} + if got := tidalPlaylistOwnerName(editorial); got != "TIDAL" { + t.Fatalf("unexpected editorial owner: %q", got) + } + + artist := &tidalPublicPlaylist{Type: "ARTIST"} + if got := tidalPlaylistOwnerName(artist); got != "Artist" { + t.Fatalf("unexpected artist owner: %q", got) + } + + user := &tidalPublicPlaylist{} + user.Creator.Name = "djtest" + if got := tidalPlaylistOwnerName(user); got != "djtest" { + t.Fatalf("unexpected creator owner: %q", got) + } +} diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 320efb5a..d72d27f2 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -383,6 +383,22 @@ import Gobackend // Import Go framework if let error = error { throw error } return response + case "getQobuzMetadata": + let args = call.arguments as! [String: Any] + let resourceType = args["resource_type"] as! String + let resourceId = args["resource_id"] as! String + let response = GobackendGetQobuzMetadata(resourceType, resourceId, &error) + if let error = error { throw error } + return response + + case "getTidalMetadata": + let args = call.arguments as! [String: Any] + let resourceType = args["resource_type"] as! String + let resourceId = args["resource_id"] as! String + let response = GobackendGetTidalMetadata(resourceType, resourceId, &error) + if let error = error { throw error } + return response + case "parseDeezerUrl": let args = call.arguments as! [String: Any] let url = args["url"] as! String @@ -390,6 +406,13 @@ import Gobackend // Import Go framework if let error = error { throw error } return response + case "parseQobuzUrl": + let args = call.arguments as! [String: Any] + let url = args["url"] as! String + let response = GobackendParseQobuzURLExport(url, &error) + if let error = error { throw error } + return response + case "parseTidalUrl": let args = call.arguments as! [String: Any] let url = args["url"] as! String diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 7dc37dd3..9df46a5b 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -3605,10 +3605,26 @@ class DownloadQueueNotifier extends Notifier { ? trackToDownload.discNumber! : 1; + String payloadSpotifyId = trackToDownload.id; + String payloadQobuzId = ''; + String payloadTidalId = ''; + if (trackToDownload.id.startsWith('qobuz:')) { + payloadQobuzId = trackToDownload.id.substring(6); + if (item.service == 'qobuz') { + payloadSpotifyId = ''; + } + } + if (trackToDownload.id.startsWith('tidal:')) { + payloadTidalId = trackToDownload.id.substring(6); + if (item.service == 'tidal') { + payloadSpotifyId = ''; + } + } + final payload = DownloadRequestPayload( isrc: trackToDownload.isrc ?? '', service: item.service, - spotifyId: trackToDownload.id, + spotifyId: payloadSpotifyId, trackName: trackToDownload.name, artistName: trackToDownload.artistName, albumName: trackToDownload.albumName, @@ -3632,6 +3648,8 @@ class DownloadQueueNotifier extends Notifier { genre: genre ?? '', label: label ?? '', copyright: copyright ?? '', + qobuzId: payloadQobuzId, + tidalId: payloadTidalId, deezerId: deezerTrackId ?? '', lyricsMode: settings.lyricsMode, storageMode: storageMode, diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 1c3974c2..2a36676c 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -18,7 +18,7 @@ class TrackState { final String? artistName; final String? coverUrl; final String? headerImageUrl; // Artist header image for background - final int? monthlyListeners; // Artist monthly listeners + final int? monthlyListeners; final List? artistAlbums; // For artist page final List? artistTopTracks; // Artist's popular tracks final List? searchArtists; // For search results @@ -384,6 +384,72 @@ class TrackNotifier extends Notifier { return; } + if (url.contains('qobuz.com') || url.startsWith('qobuzapp://')) { + _log.i('Detected Qobuz URL, parsing...'); + final parsed = await PlatformBridge.parseQobuzUrl(url); + if (!_isRequestValid(requestId)) return; + + final type = parsed['type'] as String; + final id = parsed['id'] as String; + + final metadata = await PlatformBridge.getQobuzMetadata(type, id); + if (!_isRequestValid(requestId)) return; + + if (type == 'track') { + final trackData = metadata['track'] as Map; + final track = _parseTrack(trackData); + state = TrackState( + tracks: [track], + isLoading: false, + coverUrl: track.coverUrl, + ); + } else if (type == 'album') { + final albumInfo = metadata['album_info'] as Map; + final trackList = metadata['track_list'] as List; + final tracks = trackList + .map((t) => _parseTrack(t as Map)) + .toList(); + state = TrackState( + tracks: tracks, + isLoading: false, + albumId: 'qobuz:$id', + albumName: albumInfo['name'] as String?, + coverUrl: albumInfo['images'] as String?, + ); + _preWarmCacheForTracks(tracks); + } else if (type == 'playlist') { + final playlistInfo = + metadata['playlist_info'] as Map; + final trackList = metadata['track_list'] as List; + final tracks = trackList + .map((t) => _parseTrack(t as Map)) + .toList(); + final owner = playlistInfo['owner'] as Map?; + state = TrackState( + tracks: tracks, + isLoading: false, + playlistName: owner?['name'] as String?, + coverUrl: owner?['images'] as String?, + ); + _preWarmCacheForTracks(tracks); + } else if (type == 'artist') { + final artistInfo = metadata['artist_info'] as Map; + final albumsList = metadata['albums'] as List; + final albums = albumsList + .map((a) => _parseArtistAlbum(a as Map)) + .toList(); + state = TrackState( + tracks: [], + isLoading: false, + artistId: artistInfo['id'] as String?, + artistName: artistInfo['name'] as String?, + coverUrl: artistInfo['images'] as String?, + artistAlbums: albums, + ); + } + return; + } + if (url.contains('tidal.com')) { _log.i('Detected Tidal URL, parsing...'); final parsed = await PlatformBridge.parseTidalUrl(url); @@ -392,68 +458,61 @@ class TrackNotifier extends Notifier { final type = parsed['type'] as String; final id = parsed['id'] as String; - _log.i('Tidal URL parsed: type=$type, id=$id'); + final metadata = await PlatformBridge.getTidalMetadata(type, id); + if (!_isRequestValid(requestId)) return; - // For track URLs, convert to Spotify/Deezer and fetch metadata from there if (type == 'track') { - try { - _log.i('Converting Tidal track to Spotify/Deezer via SongLink...'); - final conversion = await PlatformBridge.convertTidalToSpotifyDeezer( - url, - ); - if (!_isRequestValid(requestId)) return; - - final spotifyUrl = conversion['spotify_url'] as String?; - final deezerUrl = conversion['deezer_url'] as String?; - - if (spotifyUrl != null && spotifyUrl.isNotEmpty) { - _log.i('Found Spotify URL: $spotifyUrl, fetching metadata...'); - final metadata = - await PlatformBridge.getSpotifyMetadataWithFallback( - spotifyUrl, - ); - if (!_isRequestValid(requestId)) return; - - final trackData = metadata['track'] as Map; - final track = _parseTrack(trackData); - state = TrackState( - tracks: [track], - isLoading: false, - coverUrl: track.coverUrl, - ); - return; - } else if (deezerUrl != null && deezerUrl.isNotEmpty) { - _log.i('Found Deezer URL: $deezerUrl, fetching metadata...'); - final deezerParsed = await PlatformBridge.parseDeezerUrl( - deezerUrl, - ); - final metadata = await PlatformBridge.getDeezerMetadata( - 'track', - deezerParsed['id'] as String, - ); - if (!_isRequestValid(requestId)) return; - - final trackData = metadata['track'] as Map; - final track = _parseTrack(trackData); - state = TrackState( - tracks: [track], - isLoading: false, - coverUrl: track.coverUrl, - ); - return; - } - } catch (e) { - _log.w('Failed to convert Tidal URL via SongLink: $e'); - } + final trackData = metadata['track'] as Map; + final track = _parseTrack(trackData); + state = TrackState( + tracks: [track], + isLoading: false, + coverUrl: track.coverUrl, + ); + } else if (type == 'album') { + final albumInfo = metadata['album_info'] as Map; + final trackList = metadata['track_list'] as List; + final tracks = trackList + .map((t) => _parseTrack(t as Map)) + .toList(); + state = TrackState( + tracks: tracks, + isLoading: false, + albumId: 'tidal:$id', + albumName: albumInfo['name'] as String?, + coverUrl: albumInfo['images'] as String?, + ); + _preWarmCacheForTracks(tracks); + } else if (type == 'playlist') { + final playlistInfo = + metadata['playlist_info'] as Map; + final trackList = metadata['track_list'] as List; + final tracks = trackList + .map((t) => _parseTrack(t as Map)) + .toList(); + final owner = playlistInfo['owner'] as Map?; + state = TrackState( + tracks: tracks, + isLoading: false, + playlistName: owner?['name'] as String?, + coverUrl: owner?['images'] as String?, + ); + _preWarmCacheForTracks(tracks); + } else if (type == 'artist') { + final artistInfo = metadata['artist_info'] as Map; + final albumsList = metadata['albums'] as List; + final albums = albumsList + .map((a) => _parseArtistAlbum(a as Map)) + .toList(); + state = TrackState( + tracks: [], + isLoading: false, + artistId: artistInfo['id'] as String?, + artistName: artistInfo['name'] as String?, + coverUrl: artistInfo['images'] as String?, + artistAlbums: albums, + ); } - - // For album/artist/playlist, not yet supported - state = TrackState( - isLoading: false, - error: - 'Tidal $type links are not fully supported yet. Only track links work via SongLink conversion.', - hasSearchText: state.hasSearchText, - ); return; } diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index 2352cfac..f498d834 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -84,7 +84,12 @@ class _AlbumScreenState extends ConsumerState { // Use extensionId if available, otherwise detect from albumId prefix final providerId = widget.extensionId ?? - (widget.albumId.startsWith('deezer:') ? 'deezer' : 'spotify'); + (() { + if (widget.albumId.startsWith('deezer:')) return 'deezer'; + if (widget.albumId.startsWith('qobuz:')) return 'qobuz'; + if (widget.albumId.startsWith('tidal:')) return 'tidal'; + return 'spotify'; + })(); ref .read(recentAccessProvider.notifier) .recordAlbumAccess( @@ -175,6 +180,12 @@ class _AlbumScreenState extends ConsumerState { 'album', deezerAlbumId, ); + } else if (widget.albumId.startsWith('qobuz:')) { + final qobuzAlbumId = widget.albumId.replaceFirst('qobuz:', ''); + metadata = await PlatformBridge.getQobuzMetadata('album', qobuzAlbumId); + } else if (widget.albumId.startsWith('tidal:')) { + final tidalAlbumId = widget.albumId.replaceFirst('tidal:', ''); + metadata = await PlatformBridge.getTidalMetadata('album', tidalAlbumId); } else { final url = 'https://open.spotify.com/album/${widget.albumId}'; metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url); @@ -619,7 +630,9 @@ class _AlbumScreenState extends ConsumerState { size: 22, color: allLoved ? Colors.redAccent : Colors.white, ), - tooltip: allLoved ? context.l10n.trackOptionRemoveFromLoved : context.l10n.tooltipLoveAll, + tooltip: allLoved + ? context.l10n.trackOptionRemoveFromLoved + : context.l10n.tooltipLoveAll, padding: EdgeInsets.zero, ), ); @@ -678,9 +691,7 @@ class _AlbumScreenState extends ConsumerState { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.snackbarAddedTracksToLoved(addedCount), - ), + content: Text(context.l10n.snackbarAddedTracksToLoved(addedCount)), ), ); } diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 06fc5063..31ba546e 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -153,7 +153,12 @@ class _ArtistScreenState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback((_) { final providerId = widget.extensionId ?? - (widget.artistId.startsWith('deezer:') ? 'deezer' : 'spotify'); + (() { + if (widget.artistId.startsWith('deezer:')) return 'deezer'; + if (widget.artistId.startsWith('qobuz:')) return 'qobuz'; + if (widget.artistId.startsWith('tidal:')) return 'tidal'; + return 'spotify'; + })(); ref .read(recentAccessProvider.notifier) .recordArtistAccess( @@ -230,6 +235,30 @@ class _ArtistScreenState extends ConsumerState { albums = albumsList .map((a) => _parseArtistAlbum(a as Map)) .toList(); + } else if (widget.artistId.startsWith('qobuz:')) { + final qobuzArtistId = widget.artistId.replaceFirst('qobuz:', ''); + final metadata = await PlatformBridge.getQobuzMetadata( + 'artist', + qobuzArtistId, + ); + final albumsList = metadata['albums'] as List; + albums = albumsList + .map((a) => _parseArtistAlbum(a as Map)) + .toList(); + final artistInfo = metadata['artist_info'] as Map?; + headerImage = artistInfo?['images'] as String?; + } else if (widget.artistId.startsWith('tidal:')) { + final tidalArtistId = widget.artistId.replaceFirst('tidal:', ''); + final metadata = await PlatformBridge.getTidalMetadata( + 'artist', + tidalArtistId, + ); + final albumsList = metadata['albums'] as List; + albums = albumsList + .map((a) => _parseArtistAlbum(a as Map)) + .toList(); + final artistInfo = metadata['artist_info'] as Map?; + headerImage = artistInfo?['images'] as String?; } else { final url = 'https://open.spotify.com/artist/${widget.artistId}'; final result = await PlatformBridge.handleURLWithExtension(url); @@ -961,6 +990,24 @@ class _ArtistScreenState extends ConsumerState { .map((t) => _parseTrackFromDeezer(t as Map, album)) .toList(); } + } else if (album.id.startsWith('qobuz:')) { + final qobuzId = album.id.replaceFirst('qobuz:', ''); + final metadata = await PlatformBridge.getQobuzMetadata('album', qobuzId); + if (metadata['track_list'] != null) { + final tracksList = metadata['track_list'] as List; + return tracksList + .map((t) => _parseTrack(t as Map, album: album)) + .toList(); + } + } else if (album.id.startsWith('tidal:')) { + final tidalId = album.id.replaceFirst('tidal:', ''); + final metadata = await PlatformBridge.getTidalMetadata('album', tidalId); + if (metadata['track_list'] != null) { + final tracksList = metadata['track_list'] as List; + return tracksList + .map((t) => _parseTrack(t as Map, album: album)) + .toList(); + } } else { final url = 'https://open.spotify.com/album/${album.id}'; final result = await PlatformBridge.handleURLWithExtension(url); diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index a87676a1..b76c0886 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -65,16 +65,20 @@ class _PlaylistScreenState extends ConsumerState { }); try { - // Extract numeric ID from "deezer:123" format String playlistId = widget.playlistId!; + late final Map result; if (playlistId.startsWith('deezer:')) { playlistId = playlistId.substring(7); + result = await PlatformBridge.getDeezerMetadata('playlist', playlistId); + } else if (playlistId.startsWith('qobuz:')) { + playlistId = playlistId.substring(6); + result = await PlatformBridge.getQobuzMetadata('playlist', playlistId); + } else if (playlistId.startsWith('tidal:')) { + playlistId = playlistId.substring(6); + result = await PlatformBridge.getTidalMetadata('playlist', playlistId); + } else { + result = await PlatformBridge.getDeezerMetadata('playlist', playlistId); } - - final result = await PlatformBridge.getDeezerMetadata( - 'playlist', - playlistId, - ); if (!mounted) return; // Go backend returns 'track_list' not 'tracks' @@ -416,7 +420,12 @@ class _PlaylistScreenState extends ConsumerState { onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addToQueue(track, service, qualityOverride: quality, playlistName: widget.playlistName); + .addToQueue( + track, + service, + qualityOverride: quality, + playlistName: widget.playlistName, + ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(context.l10n.snackbarAddedToQueue(track.name)), @@ -427,7 +436,11 @@ class _PlaylistScreenState extends ConsumerState { } else { ref .read(downloadQueueProvider.notifier) - .addToQueue(track, settings.defaultService, playlistName: widget.playlistName); + .addToQueue( + track, + settings.defaultService, + playlistName: widget.playlistName, + ); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))), ); @@ -482,7 +495,9 @@ class _PlaylistScreenState extends ConsumerState { size: 22, color: allLoved ? Colors.redAccent : Colors.white, ), - tooltip: allLoved ? context.l10n.trackOptionRemoveFromLoved : context.l10n.tooltipLoveAll, + tooltip: allLoved + ? context.l10n.trackOptionRemoveFromLoved + : context.l10n.tooltipLoveAll, padding: EdgeInsets.zero, ), ); @@ -570,9 +585,7 @@ class _PlaylistScreenState extends ConsumerState { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.snackbarAddedTracksToLoved(addedCount), - ), + content: Text(context.l10n.snackbarAddedTracksToLoved(addedCount)), ), ); } @@ -594,7 +607,12 @@ class _PlaylistScreenState extends ConsumerState { onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, service, qualityOverride: quality, playlistName: widget.playlistName); + .addMultipleToQueue( + tracks, + service, + qualityOverride: quality, + playlistName: widget.playlistName, + ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -607,7 +625,11 @@ class _PlaylistScreenState extends ConsumerState { } else { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, settings.defaultService, playlistName: widget.playlistName); + .addMultipleToQueue( + tracks, + settings.defaultService, + playlistName: widget.playlistName, + ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 8ea1dddc..f3601165 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -535,11 +535,48 @@ class PlatformBridge { return jsonDecode(result as String) as Map; } + static Future> getQobuzMetadata( + String resourceType, + String resourceId, + ) async { + final result = await _channel.invokeMethod('getQobuzMetadata', { + 'resource_type': resourceType, + 'resource_id': resourceId, + }); + if (result == null) { + throw Exception( + 'getQobuzMetadata returned null for $resourceType:$resourceId', + ); + } + return jsonDecode(result as String) as Map; + } + + static Future> parseQobuzUrl(String url) async { + final result = await _channel.invokeMethod('parseQobuzUrl', {'url': url}); + return jsonDecode(result as String) as Map; + } + static Future> parseTidalUrl(String url) async { final result = await _channel.invokeMethod('parseTidalUrl', {'url': url}); return jsonDecode(result as String) as Map; } + static Future> getTidalMetadata( + String resourceType, + String resourceId, + ) async { + final result = await _channel.invokeMethod('getTidalMetadata', { + 'resource_type': resourceType, + 'resource_id': resourceId, + }); + if (result == null) { + throw Exception( + 'getTidalMetadata returned null for $resourceType:$resourceId', + ); + } + return jsonDecode(result as String) as Map; + } + static Future> convertTidalToSpotifyDeezer( String tidalUrl, ) async { @@ -1193,7 +1230,9 @@ class PlatformBridge { static Future setStoreRegistryUrl(String registryUrl) async { _log.d('setStoreRegistryUrl: $registryUrl'); - await _channel.invokeMethod('setStoreRegistryUrl', {'registry_url': registryUrl}); + await _channel.invokeMethod('setStoreRegistryUrl', { + 'registry_url': registryUrl, + }); } static Future getStoreRegistryUrl() async { From 733efce1616560042bd7e2f56ce749ec0fb45415 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 14 Mar 2026 15:41:57 +0700 Subject: [PATCH 046/145] fix: fix Tidal track resolution, playlist owner info, and improve track provider state --- go_backend/tidal.go | 90 ++++++++++++++++++++++++------- go_backend/tidal_test.go | 41 ++++++++++++++ lib/providers/track_provider.dart | 24 ++++++--- lib/screens/playlist_screen.dart | 30 +++++++---- 4 files changed, 149 insertions(+), 36 deletions(-) diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 1ede9dba..42295eb1 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -158,6 +158,7 @@ type tidalPublicArtistPage struct { Rows []struct { Modules []struct { Type string `json:"type"` + Title string `json:"title"` Artist struct { ID int64 `json:"id"` Name string `json:"name"` @@ -397,17 +398,29 @@ func tidalAlbumToAlbumInfo(album *tidalPublicAlbum) AlbumInfoMetadata { } func tidalAlbumToArtistAlbum(album *tidalPublicAlbum) ArtistAlbumMetadata { + return tidalAlbumToArtistAlbumWithType(album, "") +} + +func tidalAlbumToArtistAlbumWithType(album *tidalPublicAlbum, fallbackType string) ArtistAlbumMetadata { if album == nil { return ArtistAlbumMetadata{} } + albumType := strings.ToLower(strings.TrimSpace(album.Type)) + if albumType == "" { + albumType = strings.ToLower(strings.TrimSpace(fallbackType)) + } + if albumType == "" { + albumType = "album" + } + return ArtistAlbumMetadata{ ID: tidalPrefixedNumericID(album.ID), Name: strings.TrimSpace(album.Title), ReleaseDate: strings.TrimSpace(album.ReleaseDate), TotalTracks: album.NumberOfTracks, Images: tidalImageURL(album.Cover, "1280x1280"), - AlbumType: strings.ToLower(strings.TrimSpace(album.Type)), + AlbumType: albumType, Artists: tidalAlbumArtistsDisplay(album), } } @@ -425,6 +438,18 @@ func tidalPlaylistOwnerName(playlist *tidalPublicPlaylist) string { return "TIDAL" } +func tidalArtistAlbumTypeFromModuleTitle(title string) string { + normalized := strings.ToLower(strings.TrimSpace(title)) + switch normalized { + case "albums", "compilations", "appears on": + return "album" + case "ep & singles", "eps & singles", "singles", "ep", "eps": + return "single" + default: + return "" + } +} + func tidalBuildMetadataURL(path string, extraQuery url.Values) string { trimmedPath := strings.TrimLeft(strings.TrimSpace(path), "/") if trimmedPath == "" { @@ -595,6 +620,7 @@ func findTidalAlbumPageModule(page *tidalPublicAlbumPage, moduleType string) *st func findTidalArtistPageModule(page *tidalPublicArtistPage, moduleType string) *struct { Type string `json:"type"` + Title string `json:"title"` Artist struct { ID int64 `json:"id"` Name string `json:"name"` @@ -790,7 +816,7 @@ func (t *TidalDownloader) GetPlaylistMetadata(resourceID string) (*PlaylistRespo var info PlaylistInfoMetadata info.Tracks.Total = totalTracks info.Name = strings.TrimSpace(playlist.Title) - info.Images = tidalImageURL(tidalFirstNonEmpty(playlist.SquareImage, playlist.Image), "1280x1280") + info.Images = tidalImageURL(tidalFirstNonEmpty(playlist.SquareImage, playlist.Image), "origin") info.Owner.DisplayName = tidalPlaylistOwnerName(playlist) info.Owner.Name = strings.TrimSpace(playlist.Title) info.Owner.Images = info.Images @@ -817,29 +843,53 @@ func (t *TidalDownloader) GetArtistMetadata(resourceID string) (*ArtistResponseP } albums := make([]ArtistAlbumMetadata, 0, albumsModule.PagedList.TotalNumberOfItems) - for _, album := range albumsModule.PagedList.Items { - albums = append(albums, tidalAlbumToArtistAlbum(&album)) + seenAlbumIDs := make(map[string]struct{}) + + appendArtistAlbum := func(album tidalPublicAlbum, fallbackType string) { + mapped := tidalAlbumToArtistAlbumWithType(&album, fallbackType) + if mapped.ID == "" { + return + } + if _, exists := seenAlbumIDs[mapped.ID]; exists { + return + } + seenAlbumIDs[mapped.ID] = struct{}{} + albums = append(albums, mapped) } - pageSize := albumsModule.PagedList.Limit - if pageSize <= 0 { - pageSize = 50 - } - offset := len(albumsModule.PagedList.Items) - for offset < albumsModule.PagedList.TotalNumberOfItems && strings.TrimSpace(albumsModule.PagedList.DataAPIPath) != "" { - albumsPage, pageErr := t.getArtistAlbumsPage(albumsModule.PagedList.DataAPIPath, offset, pageSize) - if pageErr != nil { - return nil, pageErr - } + for rowIndex := range page.Rows { + for moduleIndex := range page.Rows[rowIndex].Modules { + module := &page.Rows[rowIndex].Modules[moduleIndex] + if module.Type != "ALBUM_LIST" { + continue + } - for _, album := range albumsPage.Items { - albums = append(albums, tidalAlbumToArtistAlbum(&album)) - } + fallbackType := tidalArtistAlbumTypeFromModuleTitle(module.Title) + for _, album := range module.PagedList.Items { + appendArtistAlbum(album, fallbackType) + } - if len(albumsPage.Items) == 0 || offset+len(albumsPage.Items) >= albumsPage.TotalNumberOfItems { - break + pageSize := module.PagedList.Limit + if pageSize <= 0 { + pageSize = 50 + } + offset := len(module.PagedList.Items) + for offset < module.PagedList.TotalNumberOfItems && strings.TrimSpace(module.PagedList.DataAPIPath) != "" { + albumsPage, pageErr := t.getArtistAlbumsPage(module.PagedList.DataAPIPath, offset, pageSize) + if pageErr != nil { + return nil, pageErr + } + + for _, album := range albumsPage.Items { + appendArtistAlbum(album, fallbackType) + } + + if len(albumsPage.Items) == 0 || offset+len(albumsPage.Items) >= albumsPage.TotalNumberOfItems { + break + } + offset += len(albumsPage.Items) + } } - offset += len(albumsPage.Items) } return &ArtistResponsePayload{ diff --git a/go_backend/tidal_test.go b/go_backend/tidal_test.go index 647ebaf0..bc8a56dc 100644 --- a/go_backend/tidal_test.go +++ b/go_backend/tidal_test.go @@ -162,6 +162,47 @@ func TestTidalAlbumToArtistAlbum(t *testing.T) { } } +func TestTidalAlbumToArtistAlbumWithFallbackType(t *testing.T) { + album := &tidalPublicAlbum{ + ID: 490623904, + Title: "LET 'EM KNOW", + Cover: "fc18a64b-d76b-4582-962a-224cb05193f3", + NumberOfTracks: 1, + } + + got := tidalAlbumToArtistAlbumWithType(album, "single") + if got.AlbumType != "single" { + t.Fatalf("unexpected fallback album type: %q", got.AlbumType) + } +} + +func TestTidalArtistAlbumTypeFromModuleTitle(t *testing.T) { + tests := []struct { + title string + want string + }{ + {title: "Albums", want: "album"}, + {title: "EP & Singles", want: "single"}, + {title: "Compilations", want: "album"}, + {title: "Appears On", want: "album"}, + {title: "Unknown", want: ""}, + } + + for _, test := range tests { + if got := tidalArtistAlbumTypeFromModuleTitle(test.title); got != test.want { + t.Fatalf("tidalArtistAlbumTypeFromModuleTitle(%q) = %q, want %q", test.title, got, test.want) + } + } +} + +func TestTidalPlaylistImageUsesOrigin(t *testing.T) { + got := tidalImageURL("e6b59fd3-6995-40f0-8a32-174db3a8f4f2", "origin") + want := "https://resources.tidal.com/images/e6b59fd3/6995/40f0/8a32/174db3a8f4f2/origin.jpg" + if got != want { + t.Fatalf("unexpected origin playlist image URL: %q", got) + } +} + func TestTidalPlaylistOwnerName(t *testing.T) { editorial := &tidalPublicPlaylist{Type: "EDITORIAL"} if got := tidalPlaylistOwnerName(editorial); got != "TIDAL" { diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 2a36676c..30511899 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -425,11 +425,15 @@ class TrackNotifier extends Notifier { .map((t) => _parseTrack(t as Map)) .toList(); final owner = playlistInfo['owner'] as Map?; + final playlistName = + (playlistInfo['name'] ?? owner?['name']) as String?; + final coverUrl = + (playlistInfo['images'] ?? owner?['images']) as String?; state = TrackState( tracks: tracks, isLoading: false, - playlistName: owner?['name'] as String?, - coverUrl: owner?['images'] as String?, + playlistName: playlistName, + coverUrl: coverUrl, ); _preWarmCacheForTracks(tracks); } else if (type == 'artist') { @@ -491,11 +495,15 @@ class TrackNotifier extends Notifier { .map((t) => _parseTrack(t as Map)) .toList(); final owner = playlistInfo['owner'] as Map?; + final playlistName = + (playlistInfo['name'] ?? owner?['name']) as String?; + final coverUrl = + (playlistInfo['images'] ?? owner?['images']) as String?; state = TrackState( tracks: tracks, isLoading: false, - playlistName: owner?['name'] as String?, - coverUrl: owner?['images'] as String?, + playlistName: playlistName, + coverUrl: coverUrl, ); _preWarmCacheForTracks(tracks); } else if (type == 'artist') { @@ -574,11 +582,15 @@ class TrackNotifier extends Notifier { .map((t) => _parseTrack(t as Map)) .toList(); final owner = playlistInfo['owner'] as Map?; + final playlistName = + (playlistInfo['name'] ?? owner?['name']) as String?; + final coverUrl = + (playlistInfo['images'] ?? owner?['images']) as String?; state = TrackState( tracks: tracks, isLoading: false, - playlistName: owner?['name'] as String?, - coverUrl: owner?['images'] as String?, + playlistName: playlistName, + coverUrl: coverUrl, ); _preWarmCacheForTracks(tracks); } else if (type == 'artist') { diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index b76c0886..ba16a01f 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -39,8 +39,12 @@ class _PlaylistScreenState extends ConsumerState { List? _fetchedTracks; bool _isLoading = false; String? _error; + String? _resolvedPlaylistName; + String? _resolvedCoverUrl; List get _tracks => _fetchedTracks ?? widget.tracks; + String get _playlistName => _resolvedPlaylistName ?? widget.playlistName; + String? get _coverUrl => _resolvedCoverUrl ?? widget.coverUrl; @override void initState() { @@ -81,6 +85,9 @@ class _PlaylistScreenState extends ConsumerState { } if (!mounted) return; + final playlistInfo = result['playlist_info'] as Map?; + final owner = playlistInfo?['owner'] as Map?; + // Go backend returns 'track_list' not 'tracks' final trackList = result['track_list'] as List? ?? []; final tracks = trackList @@ -89,6 +96,10 @@ class _PlaylistScreenState extends ConsumerState { setState(() { _fetchedTracks = tracks; + _resolvedPlaylistName = (playlistInfo?['name'] ?? owner?['name']) + ?.toString(); + _resolvedCoverUrl = (playlistInfo?['images'] ?? owner?['images']) + ?.toString(); _isLoading = false; }); } catch (e) { @@ -188,7 +199,7 @@ class _PlaylistScreenState extends ConsumerState { duration: const Duration(milliseconds: 200), opacity: _showTitleInAppBar ? 1.0 : 0.0, child: Text( - widget.playlistName, + _playlistName, style: TextStyle( color: colorScheme.onSurface, fontWeight: FontWeight.w600, @@ -210,10 +221,9 @@ class _PlaylistScreenState extends ConsumerState { background: Stack( fit: StackFit.expand, children: [ - if (widget.coverUrl != null) + if (_coverUrl != null) CachedNetworkImage( - imageUrl: - _highResCoverUrl(widget.coverUrl) ?? widget.coverUrl!, + imageUrl: _highResCoverUrl(_coverUrl) ?? _coverUrl!, fit: BoxFit.cover, cacheManager: CoverCacheManager.instance, placeholder: (_, _) => @@ -260,7 +270,7 @@ class _PlaylistScreenState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Text( - widget.playlistName, + _playlistName, style: const TextStyle( color: Colors.white, fontSize: 24, @@ -424,7 +434,7 @@ class _PlaylistScreenState extends ConsumerState { track, service, qualityOverride: quality, - playlistName: widget.playlistName, + playlistName: _playlistName, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -439,7 +449,7 @@ class _PlaylistScreenState extends ConsumerState { .addToQueue( track, settings.defaultService, - playlistName: widget.playlistName, + playlistName: _playlistName, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))), @@ -603,7 +613,7 @@ class _PlaylistScreenState extends ConsumerState { DownloadServicePicker.show( context, trackName: '${tracks.length} tracks', - artistName: widget.playlistName, + artistName: _playlistName, onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) @@ -611,7 +621,7 @@ class _PlaylistScreenState extends ConsumerState { tracks, service, qualityOverride: quality, - playlistName: widget.playlistName, + playlistName: _playlistName, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -628,7 +638,7 @@ class _PlaylistScreenState extends ConsumerState { .addMultipleToQueue( tracks, settings.defaultService, - playlistName: widget.playlistName, + playlistName: _playlistName, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( From 10bc29e3479406651862b4839c75140cdcc81c9c Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 14 Mar 2026 16:07:41 +0700 Subject: [PATCH 047/145] feat: add Qobuz and Tidal as built-in metadata search providers with priority-based unified search --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 9 + go_backend/exports.go | 120 +++++------ go_backend/extension_providers.go | 197 +++++++++++++++++- go_backend/extension_providers_test.go | 68 ++++++ go_backend/qobuz.go | 21 ++ go_backend/tidal.go | 44 ++++ ios/Runner/AppDelegate.swift | 14 ++ lib/providers/extension_provider.dart | 8 +- lib/providers/track_provider.dart | 76 +++---- .../metadata_provider_priority_page.dart | 14 ++ lib/services/platform_bridge.dart | 16 ++ 11 files changed, 465 insertions(+), 122 deletions(-) create mode 100644 go_backend/extension_providers_test.go diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 51a64a3e..9108e5de 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -2878,6 +2878,15 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } + "searchTracksWithMetadataProviders" -> { + val query = call.argument("query") ?: "" + val limit = call.argument("limit") ?: 20 + val includeExtensions = call.argument("include_extensions") ?: true + val response = withContext(Dispatchers.IO) { + Gobackend.searchTracksWithMetadataProvidersJSON(query, limit.toLong(), includeExtensions) + } + result.success(response) + } "enrichTrackWithExtension" -> { val extensionId = call.argument("extension_id") ?: "" val trackJson = call.argument("track") ?: "{}" diff --git a/go_backend/exports.go b/go_backend/exports.go index 447f7685..674cc01d 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1753,79 +1753,54 @@ func ReEnrichFile(requestJSON string) (string, error) { GoLog("[ReEnrich] Starting re-enrichment for: %s\n", req.FilePath) - // When search_online is true, search for metadata from internet. - // Priority: 1) Deezer (reliable, no credentials) 2) Extension providers (spotify-web etc) + // When search_online is true, search for metadata from internet using the + // configured metadata-provider priority. if req.SearchOnline && req.TrackName != "" && req.ArtistName != "" { GoLog("[ReEnrich] Searching online metadata for: %s - %s\n", req.TrackName, req.ArtistName) searchQuery := req.TrackName + " " + req.ArtistName found := false - // 1) Try Deezer first (reliable, no credentials needed) - GoLog("[ReEnrich] Trying Deezer search...\n") deezerClient := GetDeezerClient() - { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - deezerResults, err := deezerClient.SearchAll(ctx, searchQuery, 5, 0, "track") - cancel() - if err == nil && len(deezerResults.Tracks) > 0 { - track := deezerResults.Tracks[0] - GoLog("[ReEnrich] Deezer match: %s - %s (album: %s)\n", track.Name, track.Artists, track.AlbumName) - req.SpotifyID = "deezer:" + track.SpotifyID - req.AlbumName = track.AlbumName - req.AlbumArtist = track.AlbumArtist - req.TrackNumber = track.TrackNumber - req.DiscNumber = track.DiscNumber - req.ReleaseDate = track.ReleaseDate - req.ISRC = track.ISRC - if track.Images != "" { - req.CoverURL = track.Images - } - req.DurationMs = int64(track.DurationMS) - found = true - } else if err != nil { - GoLog("[ReEnrich] Deezer search failed: %v\n", err) + GoLog("[ReEnrich] Trying metadata providers in configured priority...\n") + manager := GetExtensionManager() + tracks, searchErr := manager.SearchTracksWithMetadataProviders(searchQuery, 5, true) + if searchErr == nil && len(tracks) > 0 { + track := tracks[0] + GoLog("[ReEnrich] Metadata match (%s): %s - %s (album: %s)\n", track.ProviderID, track.Name, track.Artists, track.AlbumName) + if track.SpotifyID != "" { + req.SpotifyID = track.SpotifyID + } else if track.DeezerID != "" { + req.SpotifyID = "deezer:" + track.DeezerID + } else if track.QobuzID != "" { + req.SpotifyID = "qobuz:" + track.QobuzID + } else if track.TidalID != "" { + req.SpotifyID = "tidal:" + track.TidalID + } else { + req.SpotifyID = track.ID } - } - - // 2) Try extension metadata providers (spotify-web etc) if Deezer failed - if !found { - GoLog("[ReEnrich] Trying extension metadata providers...\n") - manager := GetExtensionManager() - extTracks, extErr := manager.SearchTracksWithExtensions(searchQuery, 5) - if extErr == nil && len(extTracks) > 0 { - track := extTracks[0] - GoLog("[ReEnrich] Extension match (%s): %s - %s (album: %s)\n", track.ProviderID, track.Name, track.Artists, track.AlbumName) - if track.SpotifyID != "" { - req.SpotifyID = track.SpotifyID - } else if track.DeezerID != "" { - req.SpotifyID = "deezer:" + track.DeezerID - } else { - req.SpotifyID = track.ID - } - req.AlbumName = track.AlbumName - req.AlbumArtist = track.AlbumArtist - req.TrackNumber = track.TrackNumber - req.DiscNumber = track.DiscNumber - req.ReleaseDate = track.ReleaseDate - req.ISRC = track.ISRC - coverURL := track.ResolvedCoverURL() - if coverURL != "" { - req.CoverURL = coverURL - } - req.DurationMs = int64(track.DurationMS) - if track.Genre != "" { - req.Genre = track.Genre - } - if track.Label != "" { - req.Label = track.Label - } - if track.Copyright != "" { - req.Copyright = track.Copyright - } - found = true - } else if extErr != nil { - GoLog("[ReEnrich] Extension search failed: %v\n", extErr) + req.AlbumName = track.AlbumName + req.AlbumArtist = track.AlbumArtist + req.TrackNumber = track.TrackNumber + req.DiscNumber = track.DiscNumber + req.ReleaseDate = track.ReleaseDate + req.ISRC = track.ISRC + coverURL := track.ResolvedCoverURL() + if coverURL != "" { + req.CoverURL = coverURL } + req.DurationMs = int64(track.DurationMS) + if track.Genre != "" { + req.Genre = track.Genre + } + if track.Label != "" { + req.Label = track.Label + } + if track.Copyright != "" { + req.Copyright = track.Copyright + } + found = true + } else if searchErr != nil { + GoLog("[ReEnrich] Metadata provider search failed: %v\n", searchErr) } // Try to get extended metadata (genre, label) from Deezer if not already set @@ -2227,6 +2202,21 @@ func SearchTracksWithExtensionsJSON(query string, limit int) (string, error) { return string(jsonBytes), nil } +func SearchTracksWithMetadataProvidersJSON(query string, limit int, includeExtensions bool) (string, error) { + manager := GetExtensionManager() + tracks, err := manager.SearchTracksWithMetadataProviders(query, limit, includeExtensions) + if err != nil { + return "", err + } + + jsonBytes, err := json.Marshal(tracks) + if err != nil { + return "", err + } + + return string(jsonBytes), nil +} + func DownloadWithExtensionsJSON(requestJSON string) (string, error) { var req DownloadRequest if err := json.Unmarshal([]byte(requestJSON), &req); err != nil { diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 9da7c344..13d759af 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -600,8 +600,30 @@ func (m *ExtensionManager) SearchTracksWithExtensions(query string, limit int) ( return nil, nil } - var allTracks []ExtTrackMetadata + providerByID := make(map[string]*ExtensionProviderWrapper, len(providers)) + orderedProviders := make([]*ExtensionProviderWrapper, 0, len(providers)) for _, provider := range providers { + providerByID[provider.extension.ID] = provider + } + for _, providerID := range GetMetadataProviderPriority() { + if provider := providerByID[providerID]; provider != nil { + orderedProviders = append(orderedProviders, provider) + delete(providerByID, providerID) + } + } + if len(providerByID) > 0 { + remainingIDs := make([]string, 0, len(providerByID)) + for providerID := range providerByID { + remainingIDs = append(remainingIDs, providerID) + } + sort.Strings(remainingIDs) + for _, providerID := range remainingIDs { + orderedProviders = append(orderedProviders, providerByID[providerID]) + } + } + + var allTracks []ExtTrackMetadata + for _, provider := range orderedProviders { result, err := provider.SearchTracks(query, limit) if err != nil { GoLog("[Extension] Search error from %s: %v\n", provider.extension.ID, err) @@ -621,6 +643,8 @@ var providerPriorityMu sync.RWMutex var metadataProviderPriority []string var metadataProviderPriorityMu sync.RWMutex +var searchBuiltInMetadataTracksFunc = searchBuiltInMetadataTracks + func SetProviderPriority(providerIDs []string) { providerPriorityMu.Lock() defer providerPriorityMu.Unlock() @@ -645,7 +669,7 @@ func SetMetadataProviderPriority(providerIDs []string) { metadataProviderPriorityMu.Lock() defer metadataProviderPriorityMu.Unlock() - sanitized := make([]string, 0, len(providerIDs)+1) + sanitized := make([]string, 0, len(providerIDs)+3) seen := map[string]struct{}{} for _, providerID := range providerIDs { providerID = strings.TrimSpace(providerID) @@ -658,8 +682,12 @@ func SetMetadataProviderPriority(providerIDs []string) { seen[providerID] = struct{}{} sanitized = append(sanitized, providerID) } - if _, exists := seen["deezer"]; !exists { - sanitized = append([]string{"deezer"}, sanitized...) + for _, providerID := range []string{"deezer", "qobuz", "tidal"} { + if _, exists := seen[providerID]; exists { + continue + } + seen[providerID] = struct{}{} + sanitized = append(sanitized, providerID) } metadataProviderPriority = sanitized @@ -671,7 +699,7 @@ func GetMetadataProviderPriority() []string { defer metadataProviderPriorityMu.RUnlock() if len(metadataProviderPriority) == 0 { - return []string{"deezer"} + return []string{"deezer", "qobuz", "tidal"} } result := make([]string, len(metadataProviderPriority)) @@ -688,6 +716,165 @@ func isBuiltInProvider(providerID string) bool { } } +func normalizeBuiltInMetadataTrack(track TrackMetadata, providerID string) ExtTrackMetadata { + deezerID := "" + tidalID := "" + qobuzID := "" + prefixedID := strings.TrimSpace(track.SpotifyID) + + switch providerID { + case "deezer": + deezerID = strings.TrimPrefix(prefixedID, "deezer:") + case "tidal": + tidalID = strings.TrimPrefix(prefixedID, "tidal:") + case "qobuz": + qobuzID = strings.TrimPrefix(prefixedID, "qobuz:") + } + + return ExtTrackMetadata{ + ID: prefixedID, + Name: track.Name, + Artists: track.Artists, + AlbumName: track.AlbumName, + AlbumArtist: track.AlbumArtist, + DurationMS: track.DurationMS, + CoverURL: track.Images, + Images: track.Images, + ReleaseDate: track.ReleaseDate, + TrackNumber: track.TrackNumber, + DiscNumber: track.DiscNumber, + ISRC: track.ISRC, + ProviderID: providerID, + SpotifyID: prefixedID, + DeezerID: deezerID, + TidalID: tidalID, + QobuzID: qobuzID, + AlbumType: track.AlbumType, + } +} + +func metadataTrackDedupKey(track ExtTrackMetadata) string { + if isrc := strings.TrimSpace(track.ISRC); isrc != "" { + return "isrc:" + strings.ToUpper(isrc) + } + if spotifyID := strings.TrimSpace(track.SpotifyID); spotifyID != "" { + return "spotify:" + spotifyID + } + if providerID := strings.TrimSpace(track.ProviderID); providerID != "" && strings.TrimSpace(track.ID) != "" { + return providerID + ":" + strings.TrimSpace(track.ID) + } + return strings.TrimSpace(track.Name) + "|" + strings.TrimSpace(track.Artists) +} + +func searchBuiltInMetadataTracks(providerID, query string, limit int) ([]ExtTrackMetadata, error) { + switch providerID { + case "deezer": + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + results, err := GetDeezerClient().SearchAll(ctx, query, limit, 0, "track") + if err != nil { + return nil, err + } + + tracks := make([]ExtTrackMetadata, 0, len(results.Tracks)) + for _, track := range results.Tracks { + tracks = append(tracks, normalizeBuiltInMetadataTrack(track, "deezer")) + } + return tracks, nil + case "qobuz": + return NewQobuzDownloader().SearchTracks(query, limit) + case "tidal": + return NewTidalDownloader().SearchTracks(query, limit) + default: + return nil, fmt.Errorf("unsupported built-in metadata provider: %s", providerID) + } +} + +func (m *ExtensionManager) SearchTracksWithMetadataProviders(query string, limit int, includeExtensions bool) ([]ExtTrackMetadata, error) { + priority := GetMetadataProviderPriority() + if limit <= 0 { + limit = 20 + } + + extensionProviders := make(map[string]*ExtensionProviderWrapper) + if includeExtensions { + for _, provider := range m.GetMetadataProviders() { + extensionProviders[provider.extension.ID] = provider + } + } + + orderedProviderIDs := make([]string, 0, len(priority)+len(extensionProviders)) + seenProviderIDs := make(map[string]struct{}, len(priority)+len(extensionProviders)) + for _, providerID := range priority { + providerID = strings.TrimSpace(providerID) + if providerID == "" { + continue + } + orderedProviderIDs = append(orderedProviderIDs, providerID) + seenProviderIDs[providerID] = struct{}{} + } + if includeExtensions { + remainingIDs := make([]string, 0, len(extensionProviders)) + for providerID := range extensionProviders { + if _, exists := seenProviderIDs[providerID]; exists { + continue + } + remainingIDs = append(remainingIDs, providerID) + } + sort.Strings(remainingIDs) + orderedProviderIDs = append(orderedProviderIDs, remainingIDs...) + } + + tracks := make([]ExtTrackMetadata, 0, limit) + seenTracks := make(map[string]struct{}) + for _, providerID := range orderedProviderIDs { + var ( + providerTracks []ExtTrackMetadata + err error + ) + + if isBuiltInProvider(providerID) { + providerTracks, err = searchBuiltInMetadataTracksFunc(providerID, query, limit) + } else { + if !includeExtensions { + continue + } + provider := extensionProviders[providerID] + if provider == nil { + continue + } + var result *ExtSearchResult + result, err = provider.SearchTracks(query, limit) + if result != nil { + providerTracks = result.Tracks + } + } + + if err != nil { + GoLog("[MetadataSearch] Search error from %s: %v\n", providerID, err) + continue + } + + for _, track := range providerTracks { + key := metadataTrackDedupKey(track) + if key == "" { + continue + } + if _, exists := seenTracks[key]; exists { + continue + } + seenTracks[key] = struct{}{} + tracks = append(tracks, track) + if len(tracks) >= limit { + return tracks, nil + } + } + } + + return tracks, nil +} + func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, error) { priority := GetProviderPriority() extManager := GetExtensionManager() diff --git a/go_backend/extension_providers_test.go b/go_backend/extension_providers_test.go new file mode 100644 index 00000000..3112de44 --- /dev/null +++ b/go_backend/extension_providers_test.go @@ -0,0 +1,68 @@ +package gobackend + +import "testing" + +func TestSetMetadataProviderPriorityAddsBuiltIns(t *testing.T) { + original := GetMetadataProviderPriority() + defer SetMetadataProviderPriority(original) + + SetMetadataProviderPriority([]string{"tidal"}) + got := GetMetadataProviderPriority() + want := []string{"tidal", "deezer", "qobuz"} + if len(got) != len(want) { + t.Fatalf("unexpected priority length: got %v want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("unexpected priority at %d: got %v want %v", i, got, want) + } + } +} + +func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) { + originalPriority := GetMetadataProviderPriority() + originalSearch := searchBuiltInMetadataTracksFunc + defer func() { + SetMetadataProviderPriority(originalPriority) + searchBuiltInMetadataTracksFunc = originalSearch + }() + + SetMetadataProviderPriority([]string{"qobuz", "tidal", "deezer"}) + + var calls []string + searchBuiltInMetadataTracksFunc = func(providerID, query string, limit int) ([]ExtTrackMetadata, error) { + calls = append(calls, providerID) + switch providerID { + case "qobuz": + return []ExtTrackMetadata{ + {ProviderID: "qobuz", SpotifyID: "qobuz:1", ISRC: "AAA111", Name: "First"}, + }, nil + case "tidal": + return []ExtTrackMetadata{ + {ProviderID: "tidal", SpotifyID: "tidal:2", ISRC: "AAA111", Name: "Duplicate"}, + {ProviderID: "tidal", SpotifyID: "tidal:3", ISRC: "BBB222", Name: "Second"}, + }, nil + case "deezer": + return []ExtTrackMetadata{ + {ProviderID: "deezer", SpotifyID: "deezer:4", ISRC: "CCC333", Name: "Third"}, + }, nil + default: + return nil, nil + } + } + + manager := GetExtensionManager() + tracks, err := manager.SearchTracksWithMetadataProviders("query", 3, false) + if err != nil { + t.Fatalf("SearchTracksWithMetadataProviders returned error: %v", err) + } + if len(tracks) != 3 { + t.Fatalf("unexpected track count: got %d want 3", len(tracks)) + } + if tracks[0].ProviderID != "qobuz" || tracks[1].ProviderID != "tidal" || tracks[2].ProviderID != "deezer" { + t.Fatalf("unexpected track provider order: %+v", tracks) + } + if len(calls) != 3 || calls[0] != "qobuz" || calls[1] != "tidal" || calls[2] != "deezer" { + t.Fatalf("unexpected provider call order: %v", calls) + } +} diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index d51e45de..788f816f 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -1278,6 +1278,27 @@ func (q *QobuzDownloader) SearchTrackByMetadata(trackName, artistName string) (* return q.SearchTrackByMetadataWithDuration(trackName, artistName, 0) } +func (q *QobuzDownloader) SearchTracks(query string, limit int) ([]ExtTrackMetadata, error) { + cleanQuery := strings.TrimSpace(query) + if cleanQuery == "" { + return nil, fmt.Errorf("empty qobuz search query") + } + if limit <= 0 { + limit = 20 + } + + tracks, err := q.searchQobuzTracksWithFallback(cleanQuery, limit) + if err != nil { + return nil, err + } + + results := make([]ExtTrackMetadata, 0, len(tracks)) + for i := range tracks { + results = append(results, normalizeBuiltInMetadataTrack(qobuzTrackToTrackMetadata(&tracks[i]), "qobuz")) + } + return results, nil +} + func (q *QobuzDownloader) SearchTrackByMetadataWithDuration(trackName, artistName string, expectedDurationSec int) (*QobuzTrack, error) { queries := []string{} diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 42295eb1..8a68826d 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -208,6 +208,13 @@ type tidalPublicPlaylistItemsPage struct { } `json:"items"` } +type tidalPublicTrackSearchResponse struct { + Limit int `json:"limit"` + Offset int `json:"offset"` + TotalNumberOfItems int `json:"totalNumberOfItems"` + Items []TidalTrack `json:"items"` +} + func NewTidalDownloader() *TidalDownloader { tidalDownloaderOnce.Do(func() { globalTidalDownloader = &TidalDownloader{ @@ -590,6 +597,30 @@ func (t *TidalDownloader) getPlaylistItemsPage(resourceID string, offset, limit return &page, nil } +func (t *TidalDownloader) getTrackSearchPage(query string, limit int) (*tidalPublicTrackSearchResponse, error) { + cleanQuery := strings.TrimSpace(query) + if cleanQuery == "" { + return nil, fmt.Errorf("empty tidal search query") + } + if limit <= 0 { + limit = 20 + } + + requestURL := tidalBuildMetadataURL( + "search/tracks", + url.Values{ + "query": {cleanQuery}, + "limit": {strconv.Itoa(limit)}, + "offset": {"0"}, + }, + ) + var page tidalPublicTrackSearchResponse + if err := t.getTidalMetadataJSON(requestURL, &page); err != nil { + return nil, err + } + return &page, nil +} + func findTidalAlbumPageModule(page *tidalPublicAlbumPage, moduleType string) *struct { Type string `json:"type"` Album tidalPublicAlbum `json:"album"` @@ -738,6 +769,19 @@ func (t *TidalDownloader) SearchTrackByMetadata(trackName, artistName string) (* return nil, fmt.Errorf("tidal metadata search API disabled: no client credentials mode") } +func (t *TidalDownloader) SearchTracks(query string, limit int) ([]ExtTrackMetadata, error) { + page, err := t.getTrackSearchPage(query, limit) + if err != nil { + return nil, err + } + + results := make([]ExtTrackMetadata, 0, len(page.Items)) + for i := range page.Items { + results = append(results, normalizeBuiltInMetadataTrack(tidalTrackToTrackMetadata(&page.Items[i]), "tidal")) + } + return results, nil +} + func (t *TidalDownloader) GetTrackMetadata(resourceID string) (*TrackResponse, error) { track, err := t.getPublicTrack(resourceID) if err != nil { diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index d72d27f2..5bdc9792 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -623,6 +623,20 @@ import Gobackend // Import Go framework let response = GobackendSearchTracksWithExtensionsJSON(query, Int(limit), &error) if let error = error { throw error } return response + + case "searchTracksWithMetadataProviders": + let args = call.arguments as! [String: Any] + let query = args["query"] as! String + let limit = args["limit"] as? Int ?? 20 + let includeExtensions = args["include_extensions"] as? Bool ?? true + let response = GobackendSearchTracksWithMetadataProvidersJSON( + query, + Int(limit), + includeExtensions, + &error + ) + if let error = error { throw error } + return response case "enrichTrackWithExtension": let args = call.arguments as! [String: Any] diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index 58d14ee5..19eafa75 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -892,7 +892,7 @@ class ExtensionNotifier extends Notifier { } List getAllMetadataProviders() { - final providers = ['deezer']; + final providers = ['deezer', 'qobuz', 'tidal']; for (final ext in state.extensions) { if (ext.enabled && ext.hasMetadataProvider) { providers.add(ext.id); @@ -911,8 +911,10 @@ class ExtensionNotifier extends Notifier { } } - if (!result.contains('deezer')) { - result.insert(0, 'deezer'); + for (final provider in const ['deezer', 'qobuz', 'tidal']) { + if (!result.contains(provider)) { + result.add(provider); + } } return result; diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 30511899..8c1dd6a8 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -637,41 +637,31 @@ class TrackNotifier extends Notifier { final hasActiveMetadataExtensions = extensionState.extensions.any( (e) => e.enabled && e.hasMetadataProvider, ); - final searchProvider = settings.searchProvider; - final useExtensions = - settings.useExtensionProviders && - hasActiveMetadataExtensions && - searchProvider != null && - searchProvider.isNotEmpty; - - const source = 'deezer'; + final includeExtensions = + settings.useExtensionProviders && hasActiveMetadataExtensions; _log.i( - 'Search started: source=$source, query="$query", useExtensions=$useExtensions, filter=$currentFilter', + 'Search started: metadataProviders, query="$query", includeExtensions=$includeExtensions, filter=$currentFilter', ); Map results; - List extensionTracks = []; + List> metadataTrackResults = []; - if (useExtensions) { - try { - _log.d('Calling extension search API...'); - final extResults = await PlatformBridge.searchTracksWithExtensions( - query, - limit: 20, - ); - _log.i('Extensions returned ${extResults.length} tracks'); - - for (final t in extResults) { - try { - extensionTracks.add(_parseSearchTrack(t)); - } catch (e) { - _log.e('Failed to parse extension track: $e', e); - } - } - } catch (e) { - _log.w('Extension search failed, falling back to Deezer: $e'); - } + try { + _log.d('Calling metadata provider search API...'); + metadataTrackResults = + await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 20, + includeExtensions: includeExtensions, + ); + _log.i( + 'Metadata providers returned ${metadataTrackResults.length} tracks', + ); + } catch (e) { + _log.w( + 'Metadata provider search failed, falling back to Deezer tracks: $e', + ); } _log.d('Calling Deezer search API...'); @@ -693,32 +683,20 @@ class TrackNotifier extends Notifier { final trackList = results['tracks'] as List? ?? []; final artistList = results['artists'] as List? ?? []; final albumList = results['albums'] as List? ?? []; + final trackSearchResults = metadataTrackResults.isNotEmpty + ? metadataTrackResults + : trackList.whereType>().toList(); _log.d( - 'Raw results: ${trackList.length} tracks, ${artistList.length} artists, ${albumList.length} albums', + 'Raw results: ${trackSearchResults.length} tracks, ${artistList.length} artists, ${albumList.length} albums', ); final tracks = []; - tracks.addAll(extensionTracks); - - final existingIsrcs = extensionTracks - .where((t) => t.isrc != null && t.isrc!.isNotEmpty) - .map((t) => t.isrc!) - .toSet(); - - for (int i = 0; i < trackList.length; i++) { - final t = trackList[i]; + for (int i = 0; i < trackSearchResults.length; i++) { + final t = trackSearchResults[i]; try { - if (t is Map) { - final track = _parseSearchTrack(t); - if (track.isrc != null && existingIsrcs.contains(track.isrc)) { - continue; - } - tracks.add(track); - } else { - _log.w('Track[$i] is not a Map: ${t.runtimeType}'); - } + tracks.add(_parseSearchTrack(t)); } catch (e) { _log.e('Failed to parse track[$i]: $e', e); } @@ -768,7 +746,7 @@ class TrackNotifier extends Notifier { } _log.i( - 'Search complete: ${tracks.length} tracks (${extensionTracks.length} from extensions), ${artists.length} artists, ${albums.length} albums, ${playlists.length} playlists parsed successfully', + 'Search complete: ${tracks.length} tracks, ${artists.length} artists, ${albums.length} albums, ${playlists.length} playlists parsed successfully', ); state = TrackState( diff --git a/lib/screens/settings/metadata_provider_priority_page.dart b/lib/screens/settings/metadata_provider_priority_page.dart index 3c614784..ba66489c 100644 --- a/lib/screens/settings/metadata_provider_priority_page.dart +++ b/lib/screens/settings/metadata_provider_priority_page.dart @@ -228,6 +228,20 @@ class _MetadataProviderItem extends StatelessWidget { description: context.l10n.metadataNoRateLimits, isBuiltIn: true, ); + case 'qobuz': + return _MetadataProviderInfo( + name: 'Qobuz', + icon: Icons.library_music, + description: context.l10n.providerBuiltIn, + isBuiltIn: true, + ); + case 'tidal': + return _MetadataProviderInfo( + name: 'Tidal', + icon: Icons.music_note, + description: context.l10n.providerBuiltIn, + isBuiltIn: true, + ); default: return _MetadataProviderInfo( name: provider, diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index f3601165..d5e5c46f 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -816,6 +816,22 @@ class PlatformBridge { return list.map((e) => e as Map).toList(); } + static Future>> searchTracksWithMetadataProviders( + String query, { + int limit = 20, + bool includeExtensions = true, + }) async { + _log.d( + 'searchTracksWithMetadataProviders: "$query", includeExtensions=$includeExtensions', + ); + final result = await _channel.invokeMethod( + 'searchTracksWithMetadataProviders', + {'query': query, 'limit': limit, 'include_extensions': includeExtensions}, + ); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + static Future cleanupExtensions() async { _log.d('cleanupExtensions'); await _channel.invokeMethod('cleanupExtensions'); From aa9854fc0a972e3b9773d19457f9d968fef10901 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 14 Mar 2026 16:52:33 +0700 Subject: [PATCH 048/145] perf: optimize polling, progress caching, staggered warmup, and snapshot-based library scan - Reduce polling interval from 800ms to 1200ms across download progress, library scan, and Android native stream - Add dirty-flag caching to Go GetMultiProgress() to skip redundant JSON marshaling - Replace eager provider initialization with staggered Timer-based warmup (400/900/1600ms) - Add snapshot-based incremental library scan to avoid large MethodChannel payloads - Move history stats and grouped album filtering to Riverpod providers for better cache invalidation - Cap home tab history preview to 48 items with deep equality wrapper to reduce rebuilds - Throttle foreground service notification updates to 2% progress buckets - Migrate PageView to PageView.builder with AutomaticKeepAliveClientMixin - Add comparison table to README --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 50 +- go_backend/exports.go | 4 + go_backend/library_scan.go | 68 +- go_backend/progress.go | 35 +- lib/main.dart | 48 +- lib/providers/download_queue_provider.dart | 12 +- lib/providers/local_library_provider.dart | 49 +- lib/screens/home_tab.dart | 46 +- lib/screens/main_shell.dart | 53 +- lib/screens/queue_tab.dart | 686 +++++++++--------- lib/services/library_database.dart | 30 + lib/services/platform_bridge.dart | 22 + 12 files changed, 711 insertions(+), 392 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 9108e5de..5ffe598f 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -38,7 +38,7 @@ class MainActivity: FlutterFragmentActivity() { "com.zarz.spotiflac/download_progress_stream" private val LIBRARY_SCAN_PROGRESS_STREAM_CHANNEL = "com.zarz.spotiflac/library_scan_progress_stream" - private val STREAM_POLLING_INTERVAL_MS = 800L + private val STREAM_POLLING_INTERVAL_MS = 1200L private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var pendingSafTreeResult: MethodChannel.Result? = null private val safScanLock = Any() @@ -469,6 +469,32 @@ class MainActivity: FlutterFragmentActivity() { lastLibraryScanProgressPayload = null } + private fun loadExistingFilesJsonFromSnapshot(snapshotPath: String): String { + if (snapshotPath.isBlank()) { + return "{}" + } + + val snapshotFile = File(snapshotPath) + if (!snapshotFile.exists()) { + return "{}" + } + + val result = JSONObject() + snapshotFile.forEachLine { line -> + if (line.isBlank()) return@forEachLine + val separatorIndex = line.indexOf('\t') + if (separatorIndex <= 0 || separatorIndex >= line.length - 1) { + return@forEachLine + } + val modTime = line.substring(0, separatorIndex).toLongOrNull() ?: 0L + val filePath = line.substring(separatorIndex + 1) + if (filePath.isNotEmpty()) { + result.put(filePath, modTime) + } + } + return result.toString() + } + private fun resolveSafFile(treeUriStr: String, relativeDir: String, fileName: String): String { val obj = JSONObject() if (treeUriStr.isBlank() || fileName.isBlank()) { @@ -3186,6 +3212,18 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } + "scanLibraryFolderIncrementalFromSnapshot" -> { + val folderPath = call.argument("folder_path") ?: "" + val snapshotPath = call.argument("snapshot_path") ?: "" + val response = withContext(Dispatchers.IO) { + safScanActive = false + Gobackend.scanLibraryFolderIncrementalFromSnapshotJSON( + folderPath, + snapshotPath, + ) + } + result.success(response) + } "scanSafTree" -> { val treeUri = call.argument("tree_uri") ?: "" val response = withContext(Dispatchers.IO) { @@ -3201,6 +3239,16 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } + "scanSafTreeIncrementalFromSnapshot" -> { + val treeUri = call.argument("tree_uri") ?: "" + val snapshotPath = call.argument("snapshot_path") ?: "" + val response = withContext(Dispatchers.IO) { + val existingFilesJson = + loadExistingFilesJsonFromSnapshot(snapshotPath) + scanSafTreeIncremental(treeUri, existingFilesJson) + } + result.success(response) + } "getSafFileModTimes" -> { val uris = call.argument("uris") ?: "[]" val response = withContext(Dispatchers.IO) { diff --git a/go_backend/exports.go b/go_backend/exports.go index 674cc01d..2e833c01 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -3194,6 +3194,10 @@ func ScanLibraryFolderIncrementalJSON(folderPath, existingFilesJSON string) (str return ScanLibraryFolderIncremental(folderPath, existingFilesJSON) } +func ScanLibraryFolderIncrementalFromSnapshotJSON(folderPath, snapshotPath string) (string, error) { + return ScanLibraryFolderIncrementalFromSnapshot(folderPath, snapshotPath) +} + func GetLibraryScanProgressJSON() string { return GetLibraryScanProgress() } diff --git a/go_backend/library_scan.go b/go_backend/library_scan.go index cb831364..20cfd626 100644 --- a/go_backend/library_scan.go +++ b/go_backend/library_scan.go @@ -1,10 +1,12 @@ package gobackend import ( + "bufio" "encoding/json" "fmt" "os" "path/filepath" + "strconv" "strings" "sync" "time" @@ -541,7 +543,43 @@ func ReadAudioMetadataWithDisplayName(filePath, displayNameHint string) (string, // ScanLibraryFolderIncremental performs an incremental scan of the library folder // existingFilesJSON is a JSON object mapping filePath -> modTime (unix millis) // Only files that are new or have changed modification time will be scanned -func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, error) { +func loadExistingFilesSnapshot(snapshotPath string) (map[string]int64, error) { + existingFiles := make(map[string]int64) + if snapshotPath == "" { + return existingFiles, nil + } + + file, err := os.Open(snapshotPath) + if err != nil { + return nil, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if line == "" { + continue + } + parts := strings.SplitN(line, "\t", 2) + if len(parts) != 2 { + continue + } + modTime, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + continue + } + existingFiles[parts[1]] = modTime + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return existingFiles, nil +} + +func scanLibraryFolderIncrementalWithExistingFiles(folderPath string, existingFiles map[string]int64) (string, error) { if folderPath == "" { return "{}", fmt.Errorf("folder path is empty") } @@ -554,13 +592,6 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, return "{}", fmt.Errorf("path is not a folder: %s", folderPath) } - existingFiles := make(map[string]int64) - if existingFilesJSON != "" && existingFilesJSON != "{}" { - if err := json.Unmarshal([]byte(existingFilesJSON), &existingFiles); err != nil { - GoLog("[LibraryScan] Warning: failed to parse existing files JSON: %v\n", err) - } - } - GoLog("[LibraryScan] Incremental scan starting, %d existing files in database\n", len(existingFiles)) libraryScanProgressMu.Lock() @@ -764,3 +795,24 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, return string(jsonBytes), nil } + +// ScanLibraryFolderIncremental performs an incremental scan of the library folder +// existingFilesJSON is a JSON object mapping filePath -> modTime (unix millis) +// Only files that are new or have changed modification time will be scanned +func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, error) { + existingFiles := make(map[string]int64) + if existingFilesJSON != "" && existingFilesJSON != "{}" { + if err := json.Unmarshal([]byte(existingFilesJSON), &existingFiles); err != nil { + GoLog("[LibraryScan] Warning: failed to parse existing files JSON: %v\n", err) + } + } + return scanLibraryFolderIncrementalWithExistingFiles(folderPath, existingFiles) +} + +func ScanLibraryFolderIncrementalFromSnapshot(folderPath, snapshotPath string) (string, error) { + existingFiles, err := loadExistingFilesSnapshot(snapshotPath) + if err != nil { + return "{}", fmt.Errorf("failed to load incremental snapshot: %w", err) + } + return scanLibraryFolderIncrementalWithExistingFiles(folderPath, existingFiles) +} diff --git a/go_backend/progress.go b/go_backend/progress.go index 95ba3fde..2117d242 100644 --- a/go_backend/progress.go +++ b/go_backend/progress.go @@ -34,10 +34,16 @@ var ( downloadDir string downloadDirMu sync.RWMutex - multiProgress = MultiProgress{Items: make(map[string]*ItemProgress)} - multiMu sync.RWMutex + multiProgress = MultiProgress{Items: make(map[string]*ItemProgress)} + multiMu sync.RWMutex + multiProgressDirty = true + cachedMultiProgress = "{\"items\":{}}" ) +func markMultiProgressDirtyLocked() { + multiProgressDirty = true +} + func getProgress() DownloadProgress { multiMu.RLock() defer multiMu.RUnlock() @@ -58,13 +64,25 @@ func getProgress() DownloadProgress { func GetMultiProgress() string { multiMu.RLock() - defer multiMu.RUnlock() + if !multiProgressDirty { + cached := cachedMultiProgress + multiMu.RUnlock() + return cached + } + multiMu.RUnlock() + multiMu.Lock() + defer multiMu.Unlock() + if !multiProgressDirty { + return cachedMultiProgress + } jsonBytes, err := json.Marshal(multiProgress) if err != nil { return "{\"items\":{}}" } - return string(jsonBytes) + cachedMultiProgress = string(jsonBytes) + multiProgressDirty = false + return cachedMultiProgress } func GetItemProgress(itemID string) string { @@ -90,6 +108,7 @@ func StartItemProgress(itemID string) { IsDownloading: true, Status: "downloading", } + markMultiProgressDirtyLocked() } func SetItemBytesTotal(itemID string, total int64) { @@ -98,6 +117,7 @@ func SetItemBytesTotal(itemID string, total int64) { if item, ok := multiProgress.Items[itemID]; ok { item.BytesTotal = total + markMultiProgressDirtyLocked() } } @@ -110,6 +130,7 @@ func SetItemBytesReceived(itemID string, received int64) { if item.BytesTotal > 0 { item.Progress = float64(received) / float64(item.BytesTotal) } + markMultiProgressDirtyLocked() } } @@ -123,6 +144,7 @@ func SetItemBytesReceivedWithSpeed(itemID string, received int64, speedMBps floa if item.BytesTotal > 0 { item.Progress = float64(received) / float64(item.BytesTotal) } + markMultiProgressDirtyLocked() } } @@ -134,6 +156,7 @@ func CompleteItemProgress(itemID string) { item.Progress = 1.0 item.IsDownloading = false item.Status = "completed" + markMultiProgressDirtyLocked() } } @@ -149,6 +172,7 @@ func SetItemProgress(itemID string, progress float64, bytesReceived, bytesTotal if bytesTotal > 0 { item.BytesTotal = bytesTotal } + markMultiProgressDirtyLocked() } } @@ -159,6 +183,7 @@ func SetItemFinalizing(itemID string) { if item, ok := multiProgress.Items[itemID]; ok { item.Progress = 1.0 item.Status = "finalizing" + markMultiProgressDirtyLocked() } } @@ -167,6 +192,7 @@ func RemoveItemProgress(itemID string) { defer multiMu.Unlock() delete(multiProgress.Items, itemID) + markMultiProgressDirtyLocked() } func ClearAllItemProgress() { @@ -174,6 +200,7 @@ func ClearAllItemProgress() { defer multiMu.Unlock() multiProgress.Items = make(map[string]*ItemProgress) + markMultiProgressDirtyLocked() } func setDownloadDir(path string) error { diff --git a/lib/main.dart b/lib/main.dart index e0f155e5..63d74d3a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; @@ -91,16 +92,18 @@ class _EagerInitialization extends ConsumerStatefulWidget { class _EagerInitializationState extends ConsumerState<_EagerInitialization> { ProviderSubscription? _localLibraryEnabledSub; - bool _localLibraryPreloaded = false; + Timer? _downloadHistoryWarmupTimer; + Timer? _libraryCollectionsWarmupTimer; + Timer? _localLibraryWarmupTimer; + bool _localLibraryWarmupScheduled = false; @override void initState() { super.initState(); - _initializeAppServices(); - _initializeExtensions(); - ref.read(downloadHistoryProvider); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; + _initializeAppServices(); + _initializeExtensions(); _initializeDeferredProviders(); }); } @@ -108,12 +111,23 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { @override void dispose() { _localLibraryEnabledSub?.close(); + _downloadHistoryWarmupTimer?.cancel(); + _libraryCollectionsWarmupTimer?.cancel(); + _localLibraryWarmupTimer?.cancel(); super.dispose(); } void _initializeDeferredProviders() { - ref.read(libraryCollectionsProvider); - _maybePreloadLocalLibrary( + _downloadHistoryWarmupTimer = _scheduleProviderWarmup( + const Duration(milliseconds: 400), + () => ref.read(downloadHistoryProvider), + ); + _libraryCollectionsWarmupTimer = _scheduleProviderWarmup( + const Duration(milliseconds: 900), + () => ref.read(libraryCollectionsProvider), + ); + + _maybeScheduleLocalLibraryWarmup( ref.read( settingsProvider.select((settings) => settings.localLibraryEnabled), ), @@ -123,18 +137,26 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { settingsProvider.select((settings) => settings.localLibraryEnabled), (previous, next) { if (next == true) { - _maybePreloadLocalLibrary(true); + _maybeScheduleLocalLibraryWarmup(true); } }, ); } - void _maybePreloadLocalLibrary(bool enabled) { - if (!enabled || _localLibraryPreloaded) return; - _localLibraryPreloaded = true; - ref.read(localLibraryProvider); - _localLibraryEnabledSub?.close(); - _localLibraryEnabledSub = null; + Timer _scheduleProviderWarmup(Duration delay, VoidCallback action) { + return Timer(delay, () { + if (!mounted) return; + action(); + }); + } + + void _maybeScheduleLocalLibraryWarmup(bool enabled) { + if (!enabled || _localLibraryWarmupScheduled) return; + _localLibraryWarmupScheduled = true; + _localLibraryWarmupTimer = _scheduleProviderWarmup( + const Duration(milliseconds: 1600), + () => ref.read(localLibraryProvider), + ); } Future _initializeAppServices() async { diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 9df46a5b..4761a22a 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -929,11 +929,12 @@ class DownloadQueueNotifier extends Notifier { StreamSubscription>? _progressStreamSub; int _downloadCount = 0; static const _cleanupInterval = 50; - static const _progressPollingInterval = Duration(milliseconds: 800); + static const _progressPollingInterval = Duration(milliseconds: 1200); static const _idleProgressPollEveryTicks = 3; static const _queueSchedulingInterval = Duration(milliseconds: 250); static const _queuePersistDebounceDuration = Duration(milliseconds: 350); static const _bytesUiStep = 104857; // ~0.1 MiB, matches one-decimal MB UI. + static const _serviceProgressStepPercent = 2; final NotificationService _notificationService = NotificationService(); final AppStateDatabase _appStateDb = AppStateDatabase.instance; int _totalQueuedAtStart = 0; @@ -1470,12 +1471,17 @@ class DownloadQueueNotifier extends Notifier { .round() .clamp(0, 100) .toInt(); + final progressBucket = progressPercent == 100 + ? 100 + : ((progressPercent ~/ _serviceProgressStepPercent) * + _serviceProgressStepPercent) + .clamp(0, 100); final didContentChange = trackName != _lastServiceTrackName || artistName != _lastServiceArtistName || queueCount != _lastServiceQueueCount || - progressPercent != _lastServicePercent; + progressBucket != _lastServicePercent; final allowHeartbeat = now.difference(_lastServiceUpdateAt) >= const Duration(seconds: 5); @@ -1485,7 +1491,7 @@ class DownloadQueueNotifier extends Notifier { _lastServiceTrackName = trackName; _lastServiceArtistName = artistName; - _lastServicePercent = progressPercent; + _lastServicePercent = progressBucket; _lastServiceQueueCount = queueCount; _lastServiceUpdateAt = now; diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index 33f1ae6a..78ac4f6f 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -120,7 +120,7 @@ class LocalLibraryNotifier extends Notifier { final LibraryDatabase _db = LibraryDatabase.instance; final HistoryDatabase _historyDb = HistoryDatabase.instance; final NotificationService _notificationService = NotificationService(); - static const _progressPollingInterval = Duration(milliseconds: 800); + static const _progressPollingInterval = Duration(milliseconds: 1200); Timer? _progressTimer; Timer? _progressStreamBootstrapTimer; StreamSubscription>? _progressStreamSub; @@ -378,18 +378,41 @@ class LocalLibraryNotifier extends Notifier { _log.i('Backfilled ${backfilledModTimes.length} legacy mod times'); } - // Use appropriate incremental scan method based on SAF or not - final Map result; - if (isSaf) { - result = await PlatformBridge.scanSafTreeIncremental( - effectiveFolderPath, - existingFiles, - ); - } else { - result = await PlatformBridge.scanLibraryFolderIncremental( - effectiveFolderPath, - existingFiles, - ); + final useSnapshotBridge = + Platform.isAndroid && existingFiles.isNotEmpty; + final snapshotPath = useSnapshotBridge + ? await _db.writeFileModTimesSnapshot() + : null; + + Map result; + try { + if (isSaf) { + result = useSnapshotBridge && snapshotPath != null + ? await PlatformBridge.scanSafTreeIncrementalFromSnapshot( + effectiveFolderPath, + snapshotPath, + ) + : await PlatformBridge.scanSafTreeIncremental( + effectiveFolderPath, + existingFiles, + ); + } else { + result = useSnapshotBridge && snapshotPath != null + ? await PlatformBridge.scanLibraryFolderIncrementalFromSnapshot( + effectiveFolderPath, + snapshotPath, + ) + : await PlatformBridge.scanLibraryFolderIncremental( + effectiveFolderPath, + existingFiles, + ); + } + } finally { + if (snapshotPath != null) { + try { + await File(snapshotPath).delete(); + } catch (_) {} + } } if (_scanCancelRequested) { diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index d7c2a8eb..142c59de 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -81,6 +82,37 @@ class _SearchResultBuckets { }); } +const _homeHistoryPreviewLimit = 48; + +class _HomeHistoryPreview { + final List items; + + const _HomeHistoryPreview(this.items); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _HomeHistoryPreview && listEquals(items, other.items); + + @override + int get hashCode => Object.hashAll(items); +} + +final _homeHistoryPreviewProvider = Provider>((ref) { + final preview = ref.watch( + downloadHistoryProvider.select((s) { + final items = s.items; + if (items.length <= _homeHistoryPreviewLimit) { + return _HomeHistoryPreview(items); + } + return _HomeHistoryPreview( + items.take(_homeHistoryPreviewLimit).toList(growable: false), + ); + }), + ); + return preview.items; +}); + _RecentAccessView _buildRecentAccessViewData( List items, List historyItems, @@ -164,9 +196,7 @@ _RecentAccessView _buildRecentAccessViewData( } final recentAccessViewProvider = Provider<_RecentAccessView>((ref) { - final historyItems = ref.watch( - downloadHistoryProvider.select((s) => s.items), - ); + final historyItems = ref.watch(_homeHistoryPreviewProvider); final recentAccessItems = ref.watch( recentAccessProvider.select((s) => s.items), ); @@ -987,9 +1017,7 @@ class _HomeTabState extends ConsumerState final mediaQuery = MediaQuery.of(context); final screenHeight = mediaQuery.size.height; final topPadding = normalizedHeaderTopPadding(context); - final historyItems = ref.watch( - downloadHistoryProvider.select((s) => s.items), - ); + final historyItems = ref.watch(_homeHistoryPreviewProvider); final recentModeRequested = isShowingRecentAccess || isSearchFocused; final showRecentAccess = @@ -1822,9 +1850,9 @@ class _HomeTabState extends ConsumerState ), ); } else { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.l10n.homeAlbumInfoUnavailable))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.homeAlbumInfoUnavailable)), + ); } } diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index cf9ceab5..70c21461 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -33,7 +33,7 @@ class MainShell extends ConsumerStatefulWidget { class _MainShellState extends ConsumerState { int _currentIndex = 0; - late PageController _pageController; + late final PageController _pageController; bool _hasCheckedUpdate = false; StreamSubscription? _shareSubscription; DateTime? _lastBackPress; @@ -113,17 +113,18 @@ class _MainShellState extends ConsumerState { if (trackState.error != null && mounted) { final l10n = context.l10n; final errorMsg = trackState.error!; - final isRateLimit = errorMsg.contains('429') || + final isRateLimit = + errorMsg.contains('429') || errorMsg.toLowerCase().contains('rate limit') || errorMsg.toLowerCase().contains('too many requests'); final displayMessage = errorMsg == 'url_not_recognized' ? l10n.errorUrlNotRecognizedMessage : isRateLimit - ? l10n.errorRateLimitedMessage - : l10n.errorUrlFetchFailed; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(displayMessage)), - ); + ? l10n.errorRateLimitedMessage + : l10n.errorUrlFetchFailed; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(displayMessage))); } } @@ -212,9 +213,7 @@ class _MainShellState extends ConsumerState { ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.safMigrationSuccess), - ), + SnackBar(content: Text(context.l10n.safMigrationSuccess)), ); } } @@ -253,6 +252,7 @@ class _MainShellState extends ConsumerState { } if (_currentIndex != index) { + final shouldResetHome = index == 0; HapticFeedback.selectionClick(); setState(() => _currentIndex = index); final showStore = ref.read( @@ -262,6 +262,10 @@ class _MainShellState extends ConsumerState { currentTabIndex: _currentIndex, showStoreTab: showStore, ); + FocusManager.instance.primaryFocus?.unfocus(); + if (shouldResetHome) { + _resetHomeToMain(); + } _pageController.animateToPage( index, duration: const Duration(milliseconds: 250), @@ -501,11 +505,15 @@ class _MainShellState extends ConsumerState { return true; }, child: Scaffold( - body: PageView( + body: PageView.builder( controller: _pageController, + itemCount: tabs.length, onPageChanged: _onPageChanged, physics: const NeverScrollableScrollPhysics(), - children: tabs, + itemBuilder: (context, index) => _KeepAliveTabPage( + key: ValueKey('page-$index'), + child: tabs[index], + ), ), bottomNavigationBar: NavigationBar( selectedIndex: _currentIndex.clamp(0, maxIndex), @@ -566,6 +574,27 @@ class _LibraryTabRoot extends ConsumerWidget { } } +class _KeepAliveTabPage extends StatefulWidget { + final Widget child; + + const _KeepAliveTabPage({super.key, required this.child}); + + @override + State<_KeepAliveTabPage> createState() => _KeepAliveTabPageState(); +} + +class _KeepAliveTabPageState extends State<_KeepAliveTabPage> + with AutomaticKeepAliveClientMixin<_KeepAliveTabPage> { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } +} + class BouncingIcon extends StatefulWidget { final Widget child; const BouncingIcon({super.key, required this.child}); diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 8645950d..0a56f7a1 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -314,6 +314,348 @@ class _QueueItemIdsSnapshot { int get hashCode => Object.hashAll(ids); } +class _QueueGroupedAlbumFilterRequest { + final String searchQuery; + final String? filterSource; + final String? filterQuality; + final String? filterFormat; + final String sortMode; + + const _QueueGroupedAlbumFilterRequest({ + required this.searchQuery, + required this.filterSource, + required this.filterQuality, + required this.filterFormat, + required this.sortMode, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _QueueGroupedAlbumFilterRequest && + searchQuery == other.searchQuery && + filterSource == other.filterSource && + filterQuality == other.filterQuality && + filterFormat == other.filterFormat && + sortMode == other.sortMode; + + @override + int get hashCode => Object.hash( + searchQuery, + filterSource, + filterQuality, + filterFormat, + sortMode, + ); +} + +String _queueFileExtLower(String filePath) { + final slashIndex = filePath.lastIndexOf('/'); + final dotIndex = filePath.lastIndexOf('.'); + if (dotIndex == -1 || dotIndex < slashIndex + 1) { + return ''; + } + return filePath.substring(dotIndex + 1).toLowerCase(); +} + +String? _queueLocalQualityLabel(LocalLibraryItem item) { + if (item.bitrate != null && item.bitrate! > 0) { + return '${item.bitrate}kbps'; + } + if (item.bitDepth == null || item.bitDepth == 0 || item.sampleRate == null) { + return null; + } + return '${item.bitDepth}bit/${(item.sampleRate! / 1000).toStringAsFixed(1)}kHz'; +} + +bool _queuePassesQualityFilter(String? filterQuality, String? quality) { + if (filterQuality == null) return true; + if (quality == null) return filterQuality == 'lossy'; + final normalized = quality.toLowerCase(); + switch (filterQuality) { + case 'hires': + return normalized.startsWith('24'); + case 'cd': + return normalized.startsWith('16'); + case 'lossy': + return !normalized.startsWith('24') && !normalized.startsWith('16'); + default: + return true; + } +} + +bool _queuePassesFormatFilter(String? filterFormat, String filePath) { + if (filterFormat == null) return true; + return _queueFileExtLower(filePath) == filterFormat; +} + +_HistoryStats _buildQueueHistoryStats( + List items, [ + List localItems = const [], +]) { + final albumCounts = {}; + final albumMap = >{}; + for (final item in items) { + final key = + '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; + albumCounts[key] = (albumCounts[key] ?? 0) + 1; + albumMap.putIfAbsent(key, () => []).add(item); + } + + var singleTracks = 0; + for (final item in items) { + final key = + '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; + if ((albumCounts[key] ?? 0) <= 1) { + singleTracks++; + } + } + + final groupedAlbums = <_GroupedAlbum>[]; + albumMap.forEach((_, tracks) { + if (tracks.length <= 1) return; + tracks.sort((a, b) { + final aNum = a.trackNumber ?? 999; + final bNum = b.trackNumber ?? 999; + return aNum.compareTo(bNum); + }); + + groupedAlbums.add( + _GroupedAlbum( + albumName: tracks.first.albumName, + artistName: tracks.first.albumArtist ?? tracks.first.artistName, + coverUrl: tracks.first.coverUrl, + sampleFilePath: tracks.first.filePath, + tracks: tracks, + latestDownload: tracks + .map((t) => t.downloadedAt) + .reduce((a, b) => a.isAfter(b) ? a : b), + ), + ); + }); + groupedAlbums.sort((a, b) => b.latestDownload.compareTo(a.latestDownload)); + + var albumCount = 0; + for (final count in albumCounts.values) { + if (count > 1) albumCount++; + } + + final downloadedPathKeys = {}; + for (final item in items) { + downloadedPathKeys.addAll(buildPathMatchKeys(item.filePath)); + } + + final dedupedLocalItems = localItems + .where((item) { + final localPathKeys = buildPathMatchKeys(item.filePath); + return !localPathKeys.any(downloadedPathKeys.contains); + }) + .toList(growable: false); + + final localAlbumCounts = {}; + final localAlbumMap = >{}; + for (final item in dedupedLocalItems) { + final key = + '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; + localAlbumCounts[key] = (localAlbumCounts[key] ?? 0) + 1; + localAlbumMap.putIfAbsent(key, () => []).add(item); + } + + var localAlbumCount = 0; + var localSingleTracks = 0; + for (final count in localAlbumCounts.values) { + if (count > 1) { + localAlbumCount++; + } else { + localSingleTracks++; + } + } + + final groupedLocalAlbums = <_GroupedLocalAlbum>[]; + localAlbumMap.forEach((_, tracks) { + if (tracks.length <= 1) return; + tracks.sort((a, b) { + final aNum = a.trackNumber ?? 999; + final bNum = b.trackNumber ?? 999; + return aNum.compareTo(bNum); + }); + + groupedLocalAlbums.add( + _GroupedLocalAlbum( + albumName: tracks.first.albumName, + artistName: tracks.first.albumArtist ?? tracks.first.artistName, + coverPath: tracks + .firstWhere( + (t) => t.coverPath != null && t.coverPath!.isNotEmpty, + orElse: () => tracks.first, + ) + .coverPath, + tracks: tracks, + latestScanned: tracks + .map((t) => t.scannedAt) + .reduce((a, b) => a.isAfter(b) ? a : b), + ), + ); + }); + groupedLocalAlbums.sort((a, b) => b.latestScanned.compareTo(a.latestScanned)); + + return _HistoryStats( + albumCounts: albumCounts, + localAlbumCounts: localAlbumCounts, + groupedAlbums: groupedAlbums, + groupedLocalAlbums: groupedLocalAlbums, + albumCount: albumCount, + singleTracks: singleTracks, + localAlbumCount: localAlbumCount, + localSingleTracks: localSingleTracks, + ); +} + +List<_GroupedAlbum> _queueFilterGroupedAlbums( + List<_GroupedAlbum> albums, + _QueueGroupedAlbumFilterRequest request, +) { + if (request.filterSource == 'local') return const []; + if (request.filterSource == null && + request.filterQuality == null && + request.filterFormat == null && + request.searchQuery.isEmpty && + request.sortMode == 'latest') { + return albums; + } + + final result = <_GroupedAlbum>[]; + for (final album in albums) { + if (request.searchQuery.isNotEmpty && + !album.searchKey.contains(request.searchQuery)) { + continue; + } + + if (request.filterQuality != null || request.filterFormat != null) { + var hasMatchingTrack = false; + for (final track in album.tracks) { + if (!_queuePassesQualityFilter(request.filterQuality, track.quality)) { + continue; + } + if (!_queuePassesFormatFilter(request.filterFormat, track.filePath)) { + continue; + } + hasMatchingTrack = true; + break; + } + if (!hasMatchingTrack) continue; + } + + result.add(album); + } + + switch (request.sortMode) { + case 'oldest': + result.sort((a, b) => a.latestDownload.compareTo(b.latestDownload)); + case 'a-z': + result.sort( + (a, b) => + a.albumName.toLowerCase().compareTo(b.albumName.toLowerCase()), + ); + case 'z-a': + result.sort( + (a, b) => + b.albumName.toLowerCase().compareTo(a.albumName.toLowerCase()), + ); + default: + break; + } + return result; +} + +List<_GroupedLocalAlbum> _queueFilterGroupedLocalAlbums( + List<_GroupedLocalAlbum> albums, + _QueueGroupedAlbumFilterRequest request, +) { + if (request.filterSource == 'downloaded') return const []; + if (request.filterSource == null && + request.filterQuality == null && + request.filterFormat == null && + request.searchQuery.isEmpty && + request.sortMode == 'latest') { + return albums; + } + + final result = <_GroupedLocalAlbum>[]; + for (final album in albums) { + if (request.searchQuery.isNotEmpty && + !album.searchKey.contains(request.searchQuery)) { + continue; + } + + if (request.filterQuality != null || request.filterFormat != null) { + var hasMatchingTrack = false; + for (final track in album.tracks) { + if (!_queuePassesQualityFilter( + request.filterQuality, + _queueLocalQualityLabel(track), + )) { + continue; + } + if (!_queuePassesFormatFilter(request.filterFormat, track.filePath)) { + continue; + } + hasMatchingTrack = true; + break; + } + if (!hasMatchingTrack) continue; + } + + result.add(album); + } + + switch (request.sortMode) { + case 'oldest': + result.sort((a, b) => a.latestScanned.compareTo(b.latestScanned)); + case 'a-z': + result.sort( + (a, b) => + a.albumName.toLowerCase().compareTo(b.albumName.toLowerCase()), + ); + case 'z-a': + result.sort( + (a, b) => + b.albumName.toLowerCase().compareTo(a.albumName.toLowerCase()), + ); + default: + break; + } + return result; +} + +final _queueHistoryStatsProvider = Provider<_HistoryStats>((ref) { + final historyItems = ref.watch( + downloadHistoryProvider.select((s) => s.items), + ); + final localLibraryEnabled = ref.watch( + settingsProvider.select((s) => s.localLibraryEnabled), + ); + final localItems = localLibraryEnabled + ? ref.watch(localLibraryProvider.select((s) => s.items)) + : const []; + return _buildQueueHistoryStats(historyItems, localItems); +}); + +final _queueFilteredAlbumsProvider = + Provider.family< + ({List<_GroupedAlbum> albums, List<_GroupedLocalAlbum> localAlbums}), + _QueueGroupedAlbumFilterRequest + >((ref, request) { + final historyStats = ref.watch(_queueHistoryStatsProvider); + return ( + albums: _queueFilterGroupedAlbums(historyStats.groupedAlbums, request), + localAlbums: _queueFilterGroupedLocalAlbums( + historyStats.groupedLocalAlbums, + request, + ), + ); + }); + Map> _filterHistoryInIsolate(Map payload) { final entries = (payload['entries'] as List).cast(); final albumCounts = (payload['albumCounts'] as Map).cast(); @@ -431,14 +773,6 @@ class _QueueTabState extends ConsumerState { String? _filterCacheQuality; String? _filterCacheFormat; String _filterCacheSortMode = 'latest'; - _HistoryStats? _groupedAlbumFilterHistoryStatsCache; - String _groupedAlbumFilterSearchQuery = ''; - String? _groupedAlbumFilterSource; - String? _groupedAlbumFilterQuality; - String? _groupedAlbumFilterFormat; - String _groupedAlbumFilterSortMode = 'latest'; - List<_GroupedAlbum> _filteredGroupedAlbumsCache = const []; - List<_GroupedLocalAlbum> _filteredGroupedLocalAlbumsCache = const []; // Advanced filters String? _filterSource; // null = all, 'downloaded', 'local' String? _filterQuality; // null = all, 'hires', 'cd', 'lossy' @@ -549,6 +883,7 @@ class _QueueTabState extends ConsumerState { void _ensureHistoryCaches( List items, List localItems, + _HistoryStats historyStats, ) { final historyChanged = !identical(items, _historyItemsCache); final localChanged = !identical(localItems, _localLibraryItemsCache); @@ -557,7 +892,7 @@ class _QueueTabState extends ConsumerState { _historyItemsCache = items; _localLibraryItemsCache = localItems; - _historyStatsCache = _buildHistoryStats(items, localItems); + _historyStatsCache = historyStats; if (historyChanged) { _searchIndexCache.clear(); } @@ -1361,18 +1696,6 @@ class _QueueTabState extends ConsumerState { return filePath.substring(dotIndex + 1).toLowerCase(); } - String? _localQualityLabel(LocalLibraryItem item) { - if (item.bitrate != null && item.bitrate! > 0) { - return '${item.bitrate}kbps'; - } - if (item.bitDepth == null || - item.bitDepth == 0 || - item.sampleRate == null) { - return null; - } - return '${item.bitDepth}bit/${(item.sampleRate! / 1000).toStringAsFixed(1)}kHz'; - } - List _applyAdvancedFilters( List items, ) { @@ -1445,179 +1768,6 @@ class _QueueTabState extends ConsumerState { return sorted; } - bool _passesQualityFilter(String? quality) { - if (_filterQuality == null) return true; - if (quality == null) return _filterQuality == 'lossy'; - final q = quality.toLowerCase(); - switch (_filterQuality) { - case 'hires': - return q.startsWith('24'); - case 'cd': - return q.startsWith('16'); - case 'lossy': - return !q.startsWith('24') && !q.startsWith('16'); - default: - return true; - } - } - - bool _passesFormatFilter(String filePath) { - if (_filterFormat == null) return true; - return _fileExtLower(filePath) == _filterFormat; - } - - List<_GroupedAlbum> _filterGroupedAlbums( - List<_GroupedAlbum> albums, - String searchQuery, - ) { - if (_activeFilterCount == 0 && - searchQuery.isEmpty && - _sortMode == 'latest') { - return albums; - } - - // Source filter: if filtering local only, hide all download albums - if (_filterSource == 'local') return const []; - - final result = <_GroupedAlbum>[]; - for (final album in albums) { - if (searchQuery.isNotEmpty && !album.searchKey.contains(searchQuery)) { - continue; - } - - // Filter tracks within the album by advanced filters - if (_filterQuality != null || _filterFormat != null) { - var hasMatchingTrack = false; - for (final track in album.tracks) { - if (!_passesQualityFilter(track.quality)) continue; - if (!_passesFormatFilter(track.filePath)) continue; - hasMatchingTrack = true; - break; - } - - if (!hasMatchingTrack) continue; - } - - result.add(album); - } - - // Apply sorting to albums - switch (_sortMode) { - case 'oldest': - result.sort((a, b) => a.latestDownload.compareTo(b.latestDownload)); - case 'a-z': - result.sort( - (a, b) => - a.albumName.toLowerCase().compareTo(b.albumName.toLowerCase()), - ); - case 'z-a': - result.sort( - (a, b) => - b.albumName.toLowerCase().compareTo(a.albumName.toLowerCase()), - ); - default: // 'latest' - already sorted - break; - } - - return result; - } - - List<_GroupedLocalAlbum> _filterGroupedLocalAlbums( - List<_GroupedLocalAlbum> albums, - String searchQuery, - ) { - if (_activeFilterCount == 0 && - searchQuery.isEmpty && - _sortMode == 'latest') { - return albums; - } - - // Source filter: if filtering downloaded only, hide all local albums - if (_filterSource == 'downloaded') return const []; - - final result = <_GroupedLocalAlbum>[]; - for (final album in albums) { - if (searchQuery.isNotEmpty && !album.searchKey.contains(searchQuery)) { - continue; - } - - // Filter tracks within the album by advanced filters - if (_filterQuality != null || _filterFormat != null) { - var hasMatchingTrack = false; - for (final track in album.tracks) { - if (!_passesQualityFilter(_localQualityLabel(track))) continue; - if (!_passesFormatFilter(track.filePath)) continue; - hasMatchingTrack = true; - break; - } - - if (!hasMatchingTrack) continue; - } - - result.add(album); - } - - // Apply sorting to local albums - switch (_sortMode) { - case 'oldest': - result.sort((a, b) => a.latestScanned.compareTo(b.latestScanned)); - case 'a-z': - result.sort( - (a, b) => - a.albumName.toLowerCase().compareTo(b.albumName.toLowerCase()), - ); - case 'z-a': - result.sort( - (a, b) => - b.albumName.toLowerCase().compareTo(a.albumName.toLowerCase()), - ); - default: // 'latest' - already sorted - break; - } - - return result; - } - - ({List<_GroupedAlbum> albums, List<_GroupedLocalAlbum> localAlbums}) - _resolveFilteredGroupedAlbums(_HistoryStats historyStats) { - final cacheValid = - identical(_groupedAlbumFilterHistoryStatsCache, historyStats) && - _groupedAlbumFilterSearchQuery == _searchQuery && - _groupedAlbumFilterSource == _filterSource && - _groupedAlbumFilterQuality == _filterQuality && - _groupedAlbumFilterFormat == _filterFormat && - _groupedAlbumFilterSortMode == _sortMode; - - if (cacheValid) { - return ( - albums: _filteredGroupedAlbumsCache, - localAlbums: _filteredGroupedLocalAlbumsCache, - ); - } - - final filteredGroupedAlbums = _filterGroupedAlbums( - historyStats.groupedAlbums, - _searchQuery, - ); - final filteredGroupedLocalAlbums = _filterGroupedLocalAlbums( - historyStats.groupedLocalAlbums, - _searchQuery, - ); - - _groupedAlbumFilterHistoryStatsCache = historyStats; - _groupedAlbumFilterSearchQuery = _searchQuery; - _groupedAlbumFilterSource = _filterSource; - _groupedAlbumFilterQuality = _filterQuality; - _groupedAlbumFilterFormat = _filterFormat; - _groupedAlbumFilterSortMode = _sortMode; - _filteredGroupedAlbumsCache = filteredGroupedAlbums; - _filteredGroupedLocalAlbumsCache = filteredGroupedLocalAlbums; - return ( - albums: filteredGroupedAlbums, - localAlbums: filteredGroupedLocalAlbums, - ); - } - Set _getAvailableFormats(List items) { final formats = {}; for (final item in items) { @@ -2060,134 +2210,6 @@ class _QueueTabState extends ConsumerState { } } - _HistoryStats _buildHistoryStats( - List items, [ - List localItems = const [], - ]) { - final albumCounts = {}; - final albumMap = >{}; - for (final item in items) { - final key = - '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; - albumCounts[key] = (albumCounts[key] ?? 0) + 1; - albumMap.putIfAbsent(key, () => []).add(item); - } - - int singleTracks = 0; - for (final item in items) { - final key = - '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; - if ((albumCounts[key] ?? 0) <= 1) { - singleTracks++; - } - } - - final groupedAlbums = <_GroupedAlbum>[]; - albumMap.forEach((_, tracks) { - if (tracks.length <= 1) return; - tracks.sort((a, b) { - final aNum = a.trackNumber ?? 999; - final bNum = b.trackNumber ?? 999; - return aNum.compareTo(bNum); - }); - - groupedAlbums.add( - _GroupedAlbum( - albumName: tracks.first.albumName, - artistName: tracks.first.albumArtist ?? tracks.first.artistName, - coverUrl: tracks.first.coverUrl, - sampleFilePath: tracks.first.filePath, - tracks: tracks, - latestDownload: tracks - .map((t) => t.downloadedAt) - .reduce((a, b) => a.isAfter(b) ? a : b), - ), - ); - }); - - groupedAlbums.sort((a, b) => b.latestDownload.compareTo(a.latestDownload)); - - int albumCount = 0; - for (final count in albumCounts.values) { - if (count > 1) albumCount++; - } - - final downloadedPathKeys = {}; - for (final item in items) { - downloadedPathKeys.addAll(buildPathMatchKeys(item.filePath)); - } - - final dedupedLocalItems = localItems - .where((item) { - final localPathKeys = buildPathMatchKeys(item.filePath); - return !localPathKeys.any(downloadedPathKeys.contains); - }) - .toList(growable: false); - - // Calculate local library stats - final localAlbumCounts = {}; - final localAlbumMap = >{}; - for (final item in dedupedLocalItems) { - final key = - '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; - localAlbumCounts[key] = (localAlbumCounts[key] ?? 0) + 1; - localAlbumMap.putIfAbsent(key, () => []).add(item); - } - - int localAlbumCount = 0; - int localSingleTracks = 0; - for (final count in localAlbumCounts.values) { - if (count > 1) { - localAlbumCount++; - } else { - localSingleTracks++; - } - } - - // Build grouped local albums - final groupedLocalAlbums = <_GroupedLocalAlbum>[]; - localAlbumMap.forEach((_, tracks) { - if (tracks.length <= 1) return; - tracks.sort((a, b) { - final aNum = a.trackNumber ?? 999; - final bNum = b.trackNumber ?? 999; - return aNum.compareTo(bNum); - }); - - groupedLocalAlbums.add( - _GroupedLocalAlbum( - albumName: tracks.first.albumName, - artistName: tracks.first.albumArtist ?? tracks.first.artistName, - coverPath: tracks - .firstWhere( - (t) => t.coverPath != null && t.coverPath!.isNotEmpty, - orElse: () => tracks.first, - ) - .coverPath, - tracks: tracks, - latestScanned: tracks - .map((t) => t.scannedAt) - .reduce((a, b) => a.isAfter(b) ? a : b), - ), - ); - }); - - groupedLocalAlbums.sort( - (a, b) => b.latestScanned.compareTo(a.latestScanned), - ); - - return _HistoryStats( - albumCounts: albumCounts, - localAlbumCounts: localAlbumCounts, - groupedAlbums: groupedAlbums, - groupedLocalAlbums: groupedLocalAlbums, - albumCount: albumCount, - singleTracks: singleTracks, - localAlbumCount: localAlbumCount, - localSingleTracks: localSingleTracks, - ); - } - void _navigateToDownloadedAlbum(_GroupedAlbum album) { _searchFocusNode.unfocus(); Navigator.push( @@ -2541,8 +2563,19 @@ class _QueueTabState extends ConsumerState { ? ref.watch(localLibraryProvider.select((s) => s.items)) : const []; final collectionState = ref.watch(libraryCollectionsProvider); - - _ensureHistoryCaches(allHistoryItems, localLibraryItems); + final historyStats = ref.watch(_queueHistoryStatsProvider); + final filteredGrouped = ref.watch( + _queueFilteredAlbumsProvider( + _QueueGroupedAlbumFilterRequest( + searchQuery: _searchQuery, + filterSource: _filterSource, + filterQuality: _filterQuality, + filterFormat: _filterFormat, + sortMode: _sortMode, + ), + ), + ); + _ensureHistoryCaches(allHistoryItems, localLibraryItems, historyStats); final historyViewMode = ref.watch( settingsProvider.select((s) => s.historyViewMode), ); @@ -2551,11 +2584,6 @@ class _QueueTabState extends ConsumerState { ); final colorScheme = Theme.of(context).colorScheme; final topPadding = normalizedHeaderTopPadding(context); - - final historyStats = - _historyStatsCache ?? - _buildHistoryStats(allHistoryItems, localLibraryItems); - final filteredGrouped = _resolveFilteredGroupedAlbums(historyStats); final filteredGroupedAlbums = filteredGrouped.albums; final filteredGroupedLocalAlbums = filteredGrouped.localAlbums; final albumCount = historyStats.totalAlbumCount; diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index cf37c308..16955171 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; @@ -470,6 +472,34 @@ class LibraryDatabase { return result; } + /// Export file modification times to a compact line-based snapshot that + /// native code can read without receiving a large method-channel payload. + Future writeFileModTimesSnapshot() async { + final db = await database; + final rows = await db.rawQuery( + 'SELECT file_path, COALESCE(file_mod_time, 0) AS file_mod_time FROM library', + ); + final tempDir = await getTemporaryDirectory(); + final file = File( + join( + tempDir.path, + 'library_file_mod_times_${DateTime.now().microsecondsSinceEpoch}.tsv', + ), + ); + final buffer = StringBuffer(); + for (final row in rows) { + final path = row['file_path'] as String?; + if (path == null || path.isEmpty) continue; + final modTime = (row['file_mod_time'] as num?)?.toInt() ?? 0; + buffer + ..write(modTime) + ..write('\t') + ..writeln(path); + } + await file.writeAsString(buffer.toString(), flush: true); + return file.path; + } + /// Update file_mod_time for existing rows using file_path as key. Future updateFileModTimes(Map fileModTimes) async { if (fileModTimes.isEmpty) return; diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index d5e5c46f..99696825 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1090,6 +1090,17 @@ class PlatformBridge { return jsonDecode(result as String) as Map; } + static Future> scanLibraryFolderIncrementalFromSnapshot( + String folderPath, + String snapshotPath, + ) async { + final result = await _channel.invokeMethod( + 'scanLibraryFolderIncrementalFromSnapshot', + {'folder_path': folderPath, 'snapshot_path': snapshotPath}, + ); + return jsonDecode(result as String) as Map; + } + static Future>> scanSafTree(String treeUri) async { _log.i('scanSafTree: $treeUri'); final result = await _channel.invokeMethod('scanSafTree', { @@ -1115,6 +1126,17 @@ class PlatformBridge { return jsonDecode(result as String) as Map; } + static Future> scanSafTreeIncrementalFromSnapshot( + String treeUri, + String snapshotPath, + ) async { + final result = await _channel.invokeMethod( + 'scanSafTreeIncrementalFromSnapshot', + {'tree_uri': treeUri, 'snapshot_path': snapshotPath}, + ); + return jsonDecode(result as String) as Map; + } + /// Get last-modified timestamps for a list of SAF file URIs. /// Returns map uri -> modTime (unix millis), only for files that still exist. static Future> getSafFileModTimes(List uris) async { From 134bf4375f50ffc434f88b2a3935852a83253435 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 14 Mar 2026 21:47:57 +0700 Subject: [PATCH 049/145] feat: auto-enrich metadata for extension downloads, fix artist/playlist parsing, and improve metadata screen - Add metadata provider search (Deezer/Tidal/Qobuz) in download pipeline for extension tracks with missing album/date/ISRC, using the same mechanism as ReEnrichFile - Always pass enriched metadata (album, release_date, ISRC, cover_url, track/disc number) back in DownloadResponse so Flutter can embed them - Add Deezer ISRC lookup for genre/label during download enrichment - Extend _buildTrackForMetadataEmbedding to use ISRC, cover_url, album_artist from backend response - Add Releases section support in artist page (Go + Flutter) - Fix Track ID parsing to prefer non-empty native ID over empty spotify_id - Paginate popular tracks (5 per page with swipe + dot indicators) - Fix metadata screen: duration getter checks _editedMetadata, read album/duration from file tags - Make metadata screen ID labels and Open-in buttons source-aware (Amazon/Tidal/Qobuz/Deezer/Spotify) - Copy enrichment fields (AlbumName, DurationMS, CoverURL, AlbumArtist, ID) back to download request - Update README badge, add network_requests.txt to gitignore --- .gitignore | 1 + README.md | 2 +- go_backend/exports.go | 43 ++++++ go_backend/extension_providers.go | 122 +++++++++++++++++ lib/providers/download_queue_provider.dart | 28 +++- lib/providers/track_provider.dart | 4 +- lib/screens/artist_screen.dart | 152 +++++++++++++++++++-- lib/screens/track_metadata_screen.dart | 95 +++++++++++-- 8 files changed, 408 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 3c57112b..699e248c 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ AGENTS.md # Temp/misc nul +network_requests.txt # Log files *.log diff --git a/README.md b/README.md index b52224b2..9ccd9abc 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@
    -[![GitHub All Releases](https://img.shields.io/github/downloads/zarzet/SpotiFLAC-Mobile/total?style=for-the-badge&refresh=1)](https://github.com/zarzet/SpotiFLAC-Mobile/releases) +[![GitHub Release](https://img.shields.io/github/v/release/zarzet/SpotiFLAC-Mobile?style=for-the-badge&logo=github)](https://github.com/zarzet/SpotiFLAC-Mobile/releases) [![VirusTotal](https://img.shields.io/badge/VirusTotal-Safe-brightgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/0a2bd2a033551983fc9fcd83f82fd912c83914fd1094cd8d1c7c6a68eb23233f) [![Crowdin](https://img.shields.io/badge/HELP%20TRANSLATE%20ON-CROWDIN-%2321252b?style=for-the-badge&logo=crowdin)](https://crowdin.com/project/spotiflac-mobile) diff --git a/go_backend/exports.go b/go_backend/exports.go index 2e833c01..5a8a7840 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -2602,6 +2602,28 @@ func HandleURLWithExtensionJSON(url string) (string, error) { artistResponse["albums"] = albums } + if len(result.Artist.Releases) > 0 { + releases := make([]map[string]interface{}, len(result.Artist.Releases)) + for i, release := range result.Artist.Releases { + releaseType := release.AlbumType + if releaseType == "" { + releaseType = "album" + } + releases[i] = map[string]interface{}{ + "id": release.ID, + "name": release.Name, + "artists": release.Artists, + "images": release.CoverURL, + "cover_url": release.CoverURL, + "release_date": release.ReleaseDate, + "total_tracks": release.TotalTracks, + "album_type": releaseType, + "provider_id": release.ProviderID, + } + } + artistResponse["releases"] = releases + } + if len(result.Artist.TopTracks) > 0 { topTracks := make([]map[string]interface{}, len(result.Artist.TopTracks)) for i, track := range result.Artist.TopTracks { @@ -2851,6 +2873,27 @@ func GetArtistWithExtensionJSON(extensionID, artistID string) (string, error) { "provider_id": artist.ProviderID, } + if len(artist.Releases) > 0 { + releases := make([]map[string]interface{}, len(artist.Releases)) + for i, release := range artist.Releases { + releaseType := release.AlbumType + if releaseType == "" { + releaseType = "album" + } + releases[i] = map[string]interface{}{ + "id": release.ID, + "name": release.Name, + "artists": release.Artists, + "cover_url": release.CoverURL, + "release_date": release.ReleaseDate, + "total_tracks": release.TotalTracks, + "album_type": releaseType, + "provider_id": release.ProviderID, + } + } + response["releases"] = releases + } + if artist.HeaderImage != "" { response["header_image"] = artist.HeaderImage } diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 13d759af..1866543f 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -70,6 +70,7 @@ type ExtArtistMetadata struct { HeaderImage string `json:"header_image,omitempty"` Listeners int `json:"listeners,omitempty"` Albums []ExtAlbumMetadata `json:"albums,omitempty"` + Releases []ExtAlbumMetadata `json:"releases,omitempty"` TopTracks []ExtTrackMetadata `json:"top_tracks,omitempty"` ProviderID string `json:"provider_id"` } @@ -327,6 +328,12 @@ func (p *ExtensionProviderWrapper) GetArtist(artistID string) (*ExtArtistMetadat } artist.ProviderID = p.extension.ID + for i := range artist.Releases { + artist.Releases[i].ProviderID = p.extension.ID + for j := range artist.Releases[i].Tracks { + artist.Releases[i].Tracks[j].ProviderID = p.extension.ID + } + } return &artist, nil } @@ -970,6 +977,24 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro if enrichedTrack.Artists != "" { req.ArtistName = enrichedTrack.Artists } + if enrichedTrack.AlbumName != "" && req.AlbumName == "" { + GoLog("[DownloadWithExtensionFallback] AlbumName from enrichment: %s\n", enrichedTrack.AlbumName) + req.AlbumName = enrichedTrack.AlbumName + } + if enrichedTrack.AlbumArtist != "" && req.AlbumArtist == "" { + req.AlbumArtist = enrichedTrack.AlbumArtist + } + if enrichedTrack.DurationMS > 0 && req.DurationMS == 0 { + GoLog("[DownloadWithExtensionFallback] DurationMS from enrichment: %d\n", enrichedTrack.DurationMS) + req.DurationMS = enrichedTrack.DurationMS + } + if enrichedTrack.CoverURL != "" && req.CoverURL == "" { + req.CoverURL = enrichedTrack.CoverURL + } + if enrichedTrack.ID != "" && req.SpotifyID == "" { + GoLog("[DownloadWithExtensionFallback] Track ID from enrichment: %s\n", enrichedTrack.ID) + req.SpotifyID = enrichedTrack.ID + } if enrichedTrack.Label != "" && req.Label == "" { GoLog("[DownloadWithExtensionFallback] Label from enrichment: %s\n", enrichedTrack.Label) req.Label = enrichedTrack.Label @@ -990,6 +1015,73 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } } + // If key metadata is still missing after extension enrichment, search + // configured metadata providers (Spotify/Deezer/Tidal/Qobuz) — same + // logic that ReEnrichFile uses. + if req.Source != "" && !isBuiltInProvider(strings.ToLower(req.Source)) && + req.TrackName != "" && req.ArtistName != "" && + (req.AlbumName == "" || req.ReleaseDate == "" || req.ISRC == "") { + + searchQuery := req.TrackName + " " + req.ArtistName + GoLog("[DownloadWithExtensionFallback] Metadata incomplete, searching providers for: %s\n", searchQuery) + + tracks, searchErr := extManager.SearchTracksWithMetadataProviders(searchQuery, 5, true) + if searchErr == nil && len(tracks) > 0 { + track := tracks[0] + GoLog("[DownloadWithExtensionFallback] Metadata match (%s): %s - %s (album: %s, date: %s, isrc: %s)\n", + track.ProviderID, track.Name, track.Artists, track.AlbumName, track.ReleaseDate, track.ISRC) + + if track.AlbumName != "" && req.AlbumName == "" { + req.AlbumName = track.AlbumName + } + if track.AlbumArtist != "" && req.AlbumArtist == "" { + req.AlbumArtist = track.AlbumArtist + } + if track.ReleaseDate != "" && req.ReleaseDate == "" { + req.ReleaseDate = track.ReleaseDate + } + if track.ISRC != "" && req.ISRC == "" { + req.ISRC = track.ISRC + } + if track.TrackNumber > 0 && req.TrackNumber == 0 { + req.TrackNumber = track.TrackNumber + } + if track.DiscNumber > 0 && req.DiscNumber == 0 { + req.DiscNumber = track.DiscNumber + } + if track.CoverURL != "" && req.CoverURL == "" { + req.CoverURL = track.CoverURL + } + if track.Genre != "" && req.Genre == "" { + req.Genre = track.Genre + } + if track.Label != "" && req.Label == "" { + req.Label = track.Label + } + if track.Copyright != "" && req.Copyright == "" { + req.Copyright = track.Copyright + } + } else if searchErr != nil { + GoLog("[DownloadWithExtensionFallback] Metadata provider search failed (non-fatal): %v\n", searchErr) + } + + // Try Deezer extended metadata for genre/label if we have ISRC + if req.ISRC != "" && (req.Genre == "" || req.Label == "") { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + extMeta, err := GetDeezerClient().GetExtendedMetadataByISRC(ctx, req.ISRC) + cancel() + if err == nil && extMeta != nil { + if req.Genre == "" && extMeta.Genre != "" { + req.Genre = extMeta.Genre + } + if req.Label == "" && extMeta.Label != "" { + req.Label = extMeta.Label + } + GoLog("[DownloadWithExtensionFallback] Extended metadata from Deezer: genre=%s, label=%s\n", req.Genre, req.Label) + } + } + } + if req.Source != "" && !isBuiltInProvider(strings.ToLower(req.Source)) && (!strictMode || selectedProvider == "" || strings.EqualFold(selectedProvider, req.Source)) { @@ -1083,6 +1175,30 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } } + // Always pass enriched metadata from req so Flutter can + // embed it — fills gaps from metadata provider search. + if req.AlbumName != "" && resp.Album == "" { + resp.Album = req.AlbumName + } + if req.AlbumArtist != "" && resp.AlbumArtist == "" { + resp.AlbumArtist = req.AlbumArtist + } + if req.ReleaseDate != "" && resp.ReleaseDate == "" { + resp.ReleaseDate = req.ReleaseDate + } + if req.ISRC != "" && resp.ISRC == "" { + resp.ISRC = req.ISRC + } + if req.TrackNumber > 0 && resp.TrackNumber == 0 { + resp.TrackNumber = req.TrackNumber + } + if req.DiscNumber > 0 && resp.DiscNumber == 0 { + resp.DiscNumber = req.DiscNumber + } + if req.CoverURL != "" && resp.CoverURL == "" { + resp.CoverURL = req.CoverURL + } + return resp, nil } @@ -1636,6 +1752,12 @@ func (p *ExtensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, e handleResult.Artist.Albums[i].Tracks[j].ProviderID = p.extension.ID } } + for i := range handleResult.Artist.Releases { + handleResult.Artist.Releases[i].ProviderID = p.extension.ID + for j := range handleResult.Artist.Releases[i].Tracks { + handleResult.Artist.Releases[i].Tracks[j].ProviderID = p.extension.ID + } + } for i := range handleResult.Artist.TopTracks { handleResult.Artist.TopTracks[i].ProviderID = p.extension.ID } diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 4761a22a..df46e084 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -2373,11 +2373,25 @@ class DownloadQueueNotifier extends Notifier { final backendAlbum = normalizeOptionalString( backendResult['album'] as String?, ); + final backendIsrc = normalizeOptionalString( + backendResult['isrc'] as String?, + ); + final backendCoverUrl = normalizeOptionalString( + backendResult['cover_url'] as String?, + ); + final backendAlbumArtist = normalizeOptionalString( + backendResult['album_artist'] as String?, + ); - if (backendTrackNum == null && - backendDiscNum == null && - backendYear == null && - backendAlbum == null) { + final hasOverrides = backendTrackNum != null || + backendDiscNum != null || + backendYear != null || + backendAlbum != null || + backendIsrc != null || + backendCoverUrl != null || + backendAlbumArtist != null; + + if (!hasOverrides) { return baseTrack; } @@ -2386,12 +2400,12 @@ class DownloadQueueNotifier extends Notifier { name: baseTrack.name, artistName: baseTrack.artistName, albumName: backendAlbum ?? baseTrack.albumName, - albumArtist: resolvedAlbumArtist, + albumArtist: backendAlbumArtist ?? resolvedAlbumArtist, artistId: baseTrack.artistId, albumId: baseTrack.albumId, - coverUrl: baseTrack.coverUrl, + coverUrl: backendCoverUrl ?? baseTrack.coverUrl, duration: baseTrack.duration, - isrc: baseTrack.isrc, + isrc: backendIsrc ?? baseTrack.isrc, trackNumber: backendTrackNum ?? baseTrack.trackNumber, discNumber: backendDiscNum ?? baseTrack.discNumber, releaseDate: backendYear ?? baseTrack.releaseDate, diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 8c1dd6a8..f0552eda 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -933,8 +933,10 @@ class TrackNotifier extends Notifier { Track _parseTrack(Map data) { final durationMs = _extractDurationMs(data); + final spotifyId = (data['spotify_id'] ?? '').toString(); + final nativeId = (data['id'] ?? '').toString(); return Track( - id: data['spotify_id'] as String? ?? '', + id: spotifyId.isNotEmpty ? spotifyId : nativeId, name: data['name'] as String? ?? '', artistName: data['artists'] as String? ?? '', albumName: data['album_name'] as String? ?? '', diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 31ba546e..3efc2d88 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -38,12 +38,14 @@ class _ArtistCache { static void set( String artistId, { required List albums, + List? releases, List? topTracks, String? headerImageUrl, int? monthlyListeners, }) { _cache[artistId] = _CacheEntry( albums: albums, + releases: releases, topTracks: topTracks, headerImageUrl: headerImageUrl, monthlyListeners: monthlyListeners, @@ -54,6 +56,7 @@ class _ArtistCache { class _CacheEntry { final List albums; + final List? releases; final List? topTracks; final String? headerImageUrl; final int? monthlyListeners; @@ -61,6 +64,7 @@ class _CacheEntry { _CacheEntry({ required this.albums, + this.releases, this.topTracks, this.headerImageUrl, this.monthlyListeners, @@ -97,6 +101,7 @@ class ArtistScreen extends ConsumerStatefulWidget { class _ArtistScreenState extends ConsumerState { bool _isLoadingDiscography = false; List? _albums; + List? _releases; List? _topTracks; String? _headerImageUrl; int? _monthlyListeners; @@ -104,6 +109,8 @@ class _ArtistScreenState extends ConsumerState { bool _showTitleInAppBar = false; final ScrollController _scrollController = ScrollController(); + final PageController _popularPageController = PageController(); + int _popularCurrentPage = 0; bool _isSelectionMode = false; final Set _selectedAlbumIds = {}; @@ -174,6 +181,11 @@ class _ArtistScreenState extends ConsumerState { _topTracks = widget.topTracks; _headerImageUrl = widget.headerImageUrl; _monthlyListeners = widget.monthlyListeners; + + if ((_albums == null || _albums!.isEmpty) || + (_topTracks == null || _topTracks!.isEmpty)) { + _fetchDiscography(); + } return; } @@ -190,6 +202,7 @@ class _ArtistScreenState extends ConsumerState { } } else if (cached != null) { _albums = cached.albums; + _releases = cached.releases; _topTracks = cached.topTracks; _headerImageUrl = cached.headerImageUrl; _monthlyListeners = cached.monthlyListeners; @@ -214,6 +227,7 @@ class _ArtistScreenState extends ConsumerState { void dispose() { _scrollController.removeListener(_onScroll); _scrollController.dispose(); + _popularPageController.dispose(); super.dispose(); } @@ -221,6 +235,7 @@ class _ArtistScreenState extends ConsumerState { setState(() => _isLoadingDiscography = true); try { List albums; + List? releases; List? topTracks; String? headerImage; int? listeners; @@ -259,6 +274,42 @@ class _ArtistScreenState extends ConsumerState { .toList(); final artistInfo = metadata['artist_info'] as Map?; headerImage = artistInfo?['images'] as String?; + } else if (widget.extensionId != null && widget.extensionId!.isNotEmpty) { + final result = await PlatformBridge.getArtistWithExtension( + widget.extensionId!, + widget.artistId, + ); + + if (result == null) { + throw Exception('Failed to load artist from extension'); + } + + final artistData = result; + final albumsList = artistData['albums'] as List? ?? []; + albums = albumsList + .map((a) => _parseArtistAlbum(a as Map)) + .toList(); + + final releasesList = artistData['releases'] as List? ?? []; + if (releasesList.isNotEmpty) { + releases = releasesList + .map((a) => _parseArtistAlbum(a as Map)) + .toList(); + } + + final topTracksList = + artistData['top_tracks'] as List? ?? []; + if (topTracksList.isNotEmpty) { + topTracks = topTracksList + .map((t) => _parseTrack(t as Map)) + .toList(); + } + + headerImage = + artistData['header_image'] as String? ?? + artistData['cover_url'] as String? ?? + artistData['image_url'] as String?; + listeners = artistData['listeners'] as int?; } else { final url = 'https://open.spotify.com/artist/${widget.artistId}'; final result = await PlatformBridge.handleURLWithExtension(url); @@ -299,6 +350,7 @@ class _ArtistScreenState extends ConsumerState { _ArtistCache.set( widget.artistId, albums: albums, + releases: releases, topTracks: topTracks, headerImageUrl: finalHeaderImage, monthlyListeners: finalListeners, @@ -307,6 +359,7 @@ class _ArtistScreenState extends ConsumerState { if (mounted) { setState(() { _albums = albums; + _releases = releases; _topTracks = topTracks; _headerImageUrl = finalHeaderImage; _monthlyListeners = finalListeners; @@ -332,8 +385,11 @@ class _ArtistScreenState extends ConsumerState { durationMs = durationValue.toInt(); } + final spotifyId = (data['spotify_id'] ?? '').toString(); + final nativeId = (data['id'] ?? '').toString(); + return Track( - id: (data['spotify_id'] ?? data['id'] ?? '').toString(), + id: spotifyId.isNotEmpty ? spotifyId : nativeId, name: (data['name'] ?? '').toString(), artistName: (data['artists'] ?? data['artist'] ?? '').toString(), albumName: (data['album_name'] ?? data['album'] ?? album?.name ?? '') @@ -352,20 +408,28 @@ class _ArtistScreenState extends ConsumerState { releaseDate: data['release_date']?.toString(), albumType: data['album_type']?.toString() ?? album?.albumType, totalTracks: data['total_tracks'] as int? ?? album?.totalTracks, - source: data['provider_id']?.toString(), + source: data['provider_id']?.toString() ?? widget.extensionId, ); } ArtistAlbum _parseArtistAlbum(Map data) { + final totalTracksValue = data['total_tracks']; + final totalTracks = + totalTracksValue is int + ? totalTracksValue + : int.tryParse(totalTracksValue?.toString() ?? '') ?? 0; + return ArtistAlbum( id: data['id'] as String? ?? '', - name: data['name'] as String? ?? '', - releaseDate: data['release_date'] as String? ?? '', - totalTracks: data['total_tracks'] as int? ?? 0, - coverUrl: (data['cover_url'] ?? data['images'])?.toString(), - albumType: data['album_type'] as String? ?? 'album', - artists: data['artists'] as String? ?? '', - providerId: data['provider_id']?.toString(), + name: (data['name'] ?? data['title'] ?? '').toString(), + releaseDate: (data['release_date'] ?? '').toString(), + totalTracks: totalTracks, + coverUrl: (data['cover_url'] ?? data['images'] ?? data['cover_art']) + ?.toString(), + albumType: (data['album_type'] ?? data['type'] ?? 'album').toString(), + artists: (data['artists'] ?? data['artist'] ?? widget.artistName) + .toString(), + providerId: data['provider_id']?.toString() ?? widget.extensionId, ); } @@ -388,6 +452,7 @@ class _ArtistScreenState extends ConsumerState { final colorScheme = Theme.of(context).colorScheme; final albums = _albums ?? []; _ensureAlbumBuckets(albums); + final releases = _releases ?? const []; final albumsOnly = _albumsOnlyBucket; final singles = _singlesBucket; final compilations = _compilationsBucket; @@ -433,6 +498,14 @@ class _ArtistScreenState extends ConsumerState { SliverToBoxAdapter( child: _buildPopularSection(colorScheme), ), + if (releases.isNotEmpty) + SliverToBoxAdapter( + child: _buildAlbumSection( + 'Releases', + releases, + colorScheme, + ), + ), if (albumsOnly.isNotEmpty) SliverToBoxAdapter( child: _buildAlbumSection( @@ -1258,7 +1331,9 @@ class _ArtistScreenState extends ConsumerState { return const SizedBox.shrink(); } - final tracks = _topTracks!.take(5).toList(); + final tracks = _topTracks!; + const tracksPerPage = 5; + final pageCount = (tracks.length / tracksPerPage).ceil(); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1272,11 +1347,58 @@ class _ArtistScreenState extends ConsumerState { ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), ), - ...tracks.asMap().entries.map((entry) { - final index = entry.key; - final track = entry.value; - return _buildPopularTrackItem(index + 1, track, colorScheme); - }), + SizedBox( + height: tracksPerPage * 64.0, + child: PageView.builder( + controller: _popularPageController, + itemCount: pageCount, + onPageChanged: (page) { + setState(() { + _popularCurrentPage = page; + }); + }, + itemBuilder: (context, pageIndex) { + final startIndex = pageIndex * tracksPerPage; + final endIndex = + (startIndex + tracksPerPage).clamp(0, tracks.length); + final pageTracks = tracks.sublist(startIndex, endIndex); + + return Column( + children: pageTracks.asMap().entries.map((entry) { + final globalIndex = startIndex + entry.key; + return _buildPopularTrackItem( + globalIndex + 1, + entry.value, + colorScheme, + ); + }).toList(), + ); + }, + ), + ), + if (pageCount > 1) + Center( + child: Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(pageCount, (index) { + final isActive = _popularCurrentPage == index; + return Container( + margin: const EdgeInsets.symmetric(horizontal: 3), + width: isActive ? 8 : 6, + height: isActive ? 8 : 6, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isActive + ? colorScheme.primary + : colorScheme.onSurfaceVariant.withValues(alpha: 0.3), + ), + ); + }), + ), + ), + ), ], ); } diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index d23d41e1..cd37ee6a 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -298,11 +298,23 @@ class _TrackMetadataScreenState extends ConsumerState { final resolvedBitDepth = _readPositiveInt(metadata['bit_depth']); final resolvedSampleRate = _readPositiveInt(metadata['sample_rate']); + final resolvedDuration = _readPositiveInt(metadata['duration']); + final resolvedAlbum = metadata['album']?.toString(); final resolvedQuality = buildDisplayAudioQuality( bitDepth: resolvedBitDepth ?? bitDepth, sampleRate: resolvedSampleRate ?? sampleRate, storedQuality: _quality, ); + + // Fill in album name from file tags if stored value is empty + final needsAlbum = resolvedAlbum != null && + resolvedAlbum.isNotEmpty && + (albumName.isEmpty); + // Fill in duration from file if stored value is missing/zero + final needsDuration = resolvedDuration != null && + resolvedDuration > 0 && + (duration == null || duration == 0); + final shouldPersistResolvedAudioMetadata = resolvedBitDepth != null || resolvedSampleRate != null || @@ -310,6 +322,8 @@ class _TrackMetadataScreenState extends ConsumerState { if ((resolvedBitDepth != null || resolvedSampleRate != null || + needsAlbum || + needsDuration || isPlaceholderQualityLabel(_quality)) && mounted) { setState(() { @@ -317,6 +331,8 @@ class _TrackMetadataScreenState extends ConsumerState { ...?_editedMetadata, if (resolvedBitDepth != null) 'bit_depth': resolvedBitDepth, if (resolvedSampleRate != null) 'sample_rate': resolvedSampleRate, + if (needsAlbum) 'album': resolvedAlbum, + if (needsDuration) 'duration': resolvedDuration, }; }); } @@ -486,7 +502,8 @@ class _TrackMetadataScreenState extends ConsumerState { _editedMetadata?['copyright']?.toString() ?? (_isLocalItem ? null : _downloadItem!.copyright); int? get duration => - _isLocalItem ? _localLibraryItem!.duration : _downloadItem!.duration; + _readPositiveInt(_editedMetadata?['duration']) ?? + (_isLocalItem ? _localLibraryItem!.duration : _downloadItem!.duration); int? get bitDepth => _readPositiveInt(_editedMetadata?['bit_depth']) ?? (_isLocalItem ? _localLibraryItem!.bitDepth : _downloadItem!.bitDepth); @@ -1035,14 +1052,23 @@ class _TrackMetadataScreenState extends ConsumerState { Builder( builder: (context) { final isDeezer = _spotifyId!.contains('deezer'); + final svc = _service.toLowerCase(); + String buttonLabel; + if (isDeezer) { + buttonLabel = context.l10n.trackOpenInDeezer; + } else if (svc == 'amazon') { + buttonLabel = 'Open in Amazon Music'; + } else if (svc == 'tidal') { + buttonLabel = 'Open in Tidal'; + } else if (svc == 'qobuz') { + buttonLabel = 'Open in Qobuz'; + } else { + buttonLabel = context.l10n.trackOpenInSpotify; + } return OutlinedButton.icon( onPressed: () => _openServiceUrl(context), icon: const Icon(Icons.open_in_new, size: 18), - label: Text( - isDeezer - ? context.l10n.trackOpenInDeezer - : context.l10n.trackOpenInSpotify, - ), + label: Text(buttonLabel), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 16, @@ -1067,14 +1093,33 @@ class _TrackMetadataScreenState extends ConsumerState { final isDeezer = _spotifyId!.contains('deezer'); final rawId = _spotifyId!.replaceAll('deezer:', ''); + final svc = _service.toLowerCase(); - final webUrl = isDeezer - ? 'https://www.deezer.com/track/$rawId' - : 'https://open.spotify.com/track/$rawId'; + String webUrl; + Uri? appUri; + String serviceName; - final appUri = isDeezer - ? Uri.parse('deezer://www.deezer.com/track/$rawId') - : Uri.parse('spotify:track:$rawId'); + if (isDeezer) { + webUrl = 'https://www.deezer.com/track/$rawId'; + appUri = Uri.parse('deezer://www.deezer.com/track/$rawId'); + serviceName = 'Deezer'; + } else if (svc == 'amazon') { + webUrl = 'https://music.amazon.com/search/$rawId'; + appUri = Uri.parse('amznm://search/$rawId'); + serviceName = 'Amazon Music'; + } else if (svc == 'tidal') { + webUrl = 'https://listen.tidal.com/track/$rawId'; + appUri = Uri.parse('tidal://track/$rawId'); + serviceName = 'Tidal'; + } else if (svc == 'qobuz') { + webUrl = 'https://play.qobuz.com/track/$rawId'; + appUri = Uri.parse('qobuz://track/$rawId'); + serviceName = 'Qobuz'; + } else { + webUrl = 'https://open.spotify.com/track/$rawId'; + appUri = Uri.parse('spotify:track:$rawId'); + serviceName = 'Spotify'; + } try { final launched = await launchUrl( @@ -1100,7 +1145,7 @@ class _TrackMetadataScreenState extends ConsumerState { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - context.l10n.snackbarUrlCopied(isDeezer ? 'Deezer' : 'Spotify'), + context.l10n.snackbarUrlCopied(serviceName), ), ), ); @@ -1140,7 +1185,22 @@ class _TrackMetadataScreenState extends ConsumerState { if (!_isLocalItem && _spotifyId != null && _spotifyId!.isNotEmpty) { final isDeezer = _spotifyId!.contains('deezer'); final cleanId = _spotifyId!.replaceAll('deezer:', ''); - items.add(_MetadataItem(isDeezer ? 'Deezer ID' : 'Spotify ID', cleanId)); + String idLabel; + if (isDeezer) { + idLabel = 'Deezer ID'; + } else { + switch (_service.toLowerCase()) { + case 'amazon': + idLabel = 'Amazon ASIN'; + case 'tidal': + idLabel = 'Tidal ID'; + case 'qobuz': + idLabel = 'Qobuz ID'; + default: + idLabel = 'Spotify ID'; + } + } + items.add(_MetadataItem(idLabel, cleanId)); } items.add( @@ -1153,7 +1213,12 @@ class _TrackMetadataScreenState extends ConsumerState { return Column( children: items.map((metadata) { final isCopyable = - metadata.label == 'ISRC' || metadata.label == 'Spotify ID'; + metadata.label == 'ISRC' || + metadata.label == 'Spotify ID' || + metadata.label == 'Deezer ID' || + metadata.label == 'Amazon ASIN' || + metadata.label == 'Tidal ID' || + metadata.label == 'Qobuz ID'; return InkWell( onTap: isCopyable ? () => _copyToClipboard(context, metadata.value) From d5381afcf9f50ee8bbd8c176f9680749a8fd0d04 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 14 Mar 2026 21:49:22 +0700 Subject: [PATCH 050/145] chore: bump app version to 3.8.0+106 --- lib/constants/app_info.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 911e99fe..03766dff 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -3,8 +3,8 @@ import 'package:flutter/foundation.dart'; /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.7.2'; - static const String buildNumber = '105'; + static const String version = '3.8.0'; + static const String buildNumber = '106'; static const String fullVersion = '$version+$buildNumber'; /// Shows "Internal" in debug builds, actual version in release. diff --git a/pubspec.yaml b/pubspec.yaml index 638e579d..1aa0fcf1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Deezer publish_to: "none" -version: 3.7.2+105 +version: 3.8.0+106 environment: sdk: ^3.10.0 From 10c5293f6427b58efe53152f0ae29eee614d6a66 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 14 Mar 2026 23:10:19 +0700 Subject: [PATCH 051/145] chore: update VirusTotal hash for v3.8.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ccd9abc..bbdd10d9 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@
    [![GitHub Release](https://img.shields.io/github/v/release/zarzet/SpotiFLAC-Mobile?style=for-the-badge&logo=github)](https://github.com/zarzet/SpotiFLAC-Mobile/releases) -[![VirusTotal](https://img.shields.io/badge/VirusTotal-Safe-brightgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/0a2bd2a033551983fc9fcd83f82fd912c83914fd1094cd8d1c7c6a68eb23233f) +[![VirusTotal](https://img.shields.io/badge/VirusTotal-Safe-brightgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/63a445a956fa71ea347ad3695a62d543e14e341933326b9dbb9a15d79614ef58) [![Crowdin](https://img.shields.io/badge/HELP%20TRANSLATE%20ON-CROWDIN-%2321252b?style=for-the-badge&logo=crowdin)](https://crowdin.com/project/spotiflac-mobile) [![Telegram Channel](https://img.shields.io/badge/CHANNEL-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/spotiflac) From c6856bd1a1b6a5a523283fcb830b8edc9347210b Mon Sep 17 00:00:00 2001 From: ViscousPot Date: Sat, 14 Mar 2026 01:38:32 +0000 Subject: [PATCH 052/145] feat: add option to download multiple selected playlists --- lib/screens/queue_tab.dart | 117 +++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 0a56f7a1..7e3f96f9 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -31,6 +31,7 @@ import 'package:spotiflac_android/screens/local_album_screen.dart'; import 'package:spotiflac_android/utils/clickable_metadata.dart'; import 'package:spotiflac_android/utils/path_match_keys.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; +import 'package:spotiflac_android/widgets/download_service_picker.dart'; enum LibraryItemSource { downloaded, local } @@ -1314,6 +1315,93 @@ class _QueueTabState extends ConsumerState { }); } + Future _downloadAllSelectedPlaylists(BuildContext context) async { + final collectionsState = ref.read(libraryCollectionsProvider); + final selectedPlaylists = collectionsState.playlists + .where((p) => _selectedPlaylistIds.contains(p.id)) + .toList(); + + final totalTracks = + selectedPlaylists.fold(0, (sum, p) => sum + p.tracks.length); + + if (totalTracks == 0) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Selected playlists have no tracks')), + ); + return; + } + + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Download All'), + content: Text( + 'Download $totalTracks ${totalTracks == 1 ? 'track' : 'tracks'} ' + 'from ${selectedPlaylists.length} ' + '${selectedPlaylists.length == 1 ? 'playlist' : 'playlists'}?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text(ctx.l10n.dialogCancel), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Download'), + ), + ], + ), + ); + + if (confirmed != true || !context.mounted) return; + + final settings = ref.read(settingsProvider); + final queueNotifier = ref.read(downloadQueueProvider.notifier); + + void enqueueAll({String? qualityOverride, String? service}) { + final svc = service ?? settings.defaultService; + for (final playlist in selectedPlaylists) { + final tracks = playlist.tracks.map((e) => e.track).toList(); + queueNotifier.addMultipleToQueue( + tracks, + svc, + qualityOverride: qualityOverride, + playlistName: playlist.name, + ); + } + } + + if (settings.askQualityBeforeDownload) { + DownloadServicePicker.show( + context, + trackName: '$totalTracks tracks', + artistName: '${selectedPlaylists.length} playlists', + onSelect: (quality, service) { + enqueueAll(qualityOverride: quality, service: service); + if (!mounted) return; + _exitPlaylistSelectionMode(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.snackbarAddedTracksToQueue(totalTracks), + ), + ), + ); + }, + ); + } else { + enqueueAll(); + _exitPlaylistSelectionMode(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.snackbarAddedTracksToQueue(totalTracks), + ), + ), + ); + } + } + Future _deleteSelectedPlaylists(BuildContext context) async { final count = _selectedPlaylistIds.length; final confirmed = await showDialog( @@ -1452,6 +1540,35 @@ class _QueueTabState extends ConsumerState { const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: selectedCount > 0 + ? () => _downloadAllSelectedPlaylists(context) + : null, + icon: const Icon(Icons.download_rounded), + label: Text( + selectedCount > 0 + ? 'Download $selectedCount ${selectedCount == 1 ? 'playlist' : 'playlists'}' + : 'Select playlists to download', + ), + style: FilledButton.styleFrom( + backgroundColor: selectedCount > 0 + ? colorScheme.primary + : colorScheme.surfaceContainerHighest, + foregroundColor: selectedCount > 0 + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ), + + const SizedBox(height: 8), + SizedBox( width: double.infinity, child: FilledButton.icon( From 54ddc1f59c1edf56aee35385088fc7436929f6ff Mon Sep 17 00:00:00 2001 From: ViscousPot Date: Sat, 14 Mar 2026 02:08:49 +0000 Subject: [PATCH 053/145] feat: auto fill playlist name during import --- lib/screens/playlist_screen.dart | 2 +- lib/widgets/playlist_picker_sheet.dart | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index ba16a01f..8a035def 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -533,7 +533,7 @@ class _PlaylistScreenState extends ConsumerState { tooltip: context.l10n.tooltipAddToPlaylist, onPressed: _tracks.isEmpty ? null - : () => showAddTracksToPlaylistSheet(context, ref, _tracks), + : () => showAddTracksToPlaylistSheet(context, ref, _tracks, playlistNamePrefill: widget.playlistName), ); } diff --git a/lib/widgets/playlist_picker_sheet.dart b/lib/widgets/playlist_picker_sheet.dart index 2b6fd162..238b5350 100644 --- a/lib/widgets/playlist_picker_sheet.dart +++ b/lib/widgets/playlist_picker_sheet.dart @@ -20,6 +20,7 @@ Future showAddTracksToPlaylistSheet( BuildContext context, WidgetRef ref, List tracks, + {String? playlistNamePrefill} ) async { if (tracks.isEmpty) return; @@ -31,15 +32,16 @@ Future showAddTracksToPlaylistSheet( showDragHandle: true, isScrollControlled: true, builder: (sheetContext) { - return _PlaylistPickerSheetContent(tracks: tracks); + return _PlaylistPickerSheetContent(tracks: tracks, playlistNamePrefill: playlistNamePrefill); }, ); } class _PlaylistPickerSheetContent extends ConsumerStatefulWidget { final List tracks; + final String? playlistNamePrefill; - const _PlaylistPickerSheetContent({required this.tracks}); + const _PlaylistPickerSheetContent({required this.tracks, this.playlistNamePrefill}); @override ConsumerState<_PlaylistPickerSheetContent> createState() => @@ -130,7 +132,7 @@ class _PlaylistPickerSheetContentState leading: const Icon(Icons.add_circle_outline), title: Text(context.l10n.collectionCreatePlaylist), onTap: () async { - final name = await _promptPlaylistName(context); + final name = await _promptPlaylistName(context, widget.playlistNamePrefill); if (name == null || name.trim().isEmpty || !context.mounted) { return; } @@ -221,8 +223,8 @@ class _PlaylistPickerSheetContentState } } -Future _promptPlaylistName(BuildContext context) async { - final controller = TextEditingController(); +Future _promptPlaylistName(BuildContext context, String? playlistNamePrefill) async { + final controller = TextEditingController(text: playlistNamePrefill); final formKey = GlobalKey(); final result = await showDialog( From 31018230ee7e3bf3491aef26755f23c71a0a63e5 Mon Sep 17 00:00:00 2001 From: ViscousPot Date: Sat, 14 Mar 2026 01:39:16 +0000 Subject: [PATCH 054/145] add fvm --- .fvmrc | 3 +++ .gitignore | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 .fvmrc diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..5c08b3bc --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.38.1" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 699e248c..517be8ed 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,6 @@ flutter_*.log # Development tools tool/ .claude/settings.local.json + +# FVM Version Cache +.fvm/ \ No newline at end of file From 6dad66d62d3de309675f4a8712567a2a92fe7ccb Mon Sep 17 00:00:00 2001 From: ViscousPot Date: Sun, 15 Mar 2026 04:37:00 +0000 Subject: [PATCH 055/145] Update CONTRIBUTING.md --- CONTRIBUTING.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1750732..2c1e7b6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,17 +86,31 @@ Translation files are located in `lib/l10n/arb/`. git remote add upstream https://github.com/zarzet/SpotiFLAC-Mobile.git ``` -3. **Install dependencies** +3. **Use FVM (Flutter Version: 3.38.1)** + ```bash + fvm use + ``` + +4. **Install dependencies** ```bash flutter pub get ``` -4. **Generate code** (for Riverpod, JSON serialization, etc.) +5. **Generate code** (for Riverpod, JSON serialization, etc.) ```bash dart run build_runner build --delete-conflicting-outputs ``` -5. **Run the app** +6. **Set up Go environment (Go Version: 1.25.7)** + ```bash + cd go_backend + mkdir -p ../android/app/libs + gomobile init + gomobile bind -target=android -androidapi 24 -o ../android/app/libs/gobackend.aar . + cd .. + ``` + +7. **Run the app** ```bash flutter run ``` From 7b9ed3ec8ed98bd1a18156f39c31969237233ab2 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 18:52:41 +0700 Subject: [PATCH 056/145] feat: add Qobuz Afkar API provider and prefer request metadata for consistent album grouping --- go_backend/exports.go | 59 +++++++++++++++++--------- go_backend/exports_test.go | 86 ++++++++++++++++++++++++++++++++++++++ go_backend/qobuz.go | 29 ++++++++++--- go_backend/qobuz_test.go | 6 ++- go_backend/tidal.go | 16 +++++-- pubspec.lock | 32 ++++++-------- 6 files changed, 178 insertions(+), 50 deletions(-) create mode 100644 go_backend/exports_test.go diff --git a/go_backend/exports.go b/go_backend/exports.go index 5a8a7840..b8178d77 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -135,6 +135,36 @@ type DownloadResult struct { DecryptionKey string } +func preferredReleaseMetadata( + req DownloadRequest, + album string, + releaseDate string, + trackNumber int, + discNumber int, +) (string, string, int, int) { + preferredAlbum := strings.TrimSpace(req.AlbumName) + if preferredAlbum == "" { + preferredAlbum = album + } + + preferredReleaseDate := strings.TrimSpace(req.ReleaseDate) + if preferredReleaseDate == "" { + preferredReleaseDate = releaseDate + } + + preferredTrackNumber := req.TrackNumber + if preferredTrackNumber == 0 { + preferredTrackNumber = trackNumber + } + + preferredDiscNumber := req.DiscNumber + if preferredDiscNumber == 0 { + preferredDiscNumber = discNumber + } + + return preferredAlbum, preferredReleaseDate, preferredTrackNumber, preferredDiscNumber +} + func buildDownloadSuccessResponse( req DownloadRequest, result DownloadResult, @@ -153,25 +183,16 @@ func buildDownloadSuccessResponse( artist = req.ArtistName } - album := result.Album - if album == "" { - album = req.AlbumName - } - - releaseDate := result.ReleaseDate - if releaseDate == "" { - releaseDate = req.ReleaseDate - } - - trackNumber := result.TrackNumber - if trackNumber == 0 { - trackNumber = req.TrackNumber - } - - discNumber := result.DiscNumber - if discNumber == 0 { - discNumber = req.DiscNumber - } + // Preserve requested release metadata when available so mixed-provider + // fallback downloads from the same source album do not get split into + // different albums just because Tidal/Qobuz report variant titles/dates. + album, releaseDate, trackNumber, discNumber := preferredReleaseMetadata( + req, + result.Album, + result.ReleaseDate, + result.TrackNumber, + result.DiscNumber, + ) isrc := result.ISRC if isrc == "" { diff --git a/go_backend/exports_test.go b/go_backend/exports_test.go new file mode 100644 index 00000000..dbce4278 --- /dev/null +++ b/go_backend/exports_test.go @@ -0,0 +1,86 @@ +package gobackend + +import "testing" + +func TestBuildDownloadSuccessResponsePrefersRequestedAlbumMetadata(t *testing.T) { + req := DownloadRequest{ + TrackName: "Bonus Track", + ArtistName: "Artist", + AlbumName: "Album (Deluxe)", + AlbumArtist: "Artist", + ReleaseDate: "2024-01-01", + TrackNumber: 14, + DiscNumber: 1, + ISRC: "REQ123", + CoverURL: "https://example.com/cover.jpg", + Genre: "Pop", + Label: "Label", + Copyright: "Copyright", + } + + result := DownloadResult{ + Title: "Bonus Track", + Artist: "Artist", + Album: "Album", + ReleaseDate: "2023-12-01", + TrackNumber: 2, + DiscNumber: 9, + ISRC: "RES456", + } + + resp := buildDownloadSuccessResponse( + req, + result, + "tidal", + "ok", + "/tmp/test.flac", + false, + ) + + if resp.Album != req.AlbumName { + t.Fatalf("album = %q, want %q", resp.Album, req.AlbumName) + } + if resp.ReleaseDate != req.ReleaseDate { + t.Fatalf("release date = %q, want %q", resp.ReleaseDate, req.ReleaseDate) + } + if resp.TrackNumber != req.TrackNumber { + t.Fatalf("track number = %d, want %d", resp.TrackNumber, req.TrackNumber) + } + if resp.DiscNumber != req.DiscNumber { + t.Fatalf("disc number = %d, want %d", resp.DiscNumber, req.DiscNumber) + } + if resp.Artist != result.Artist { + t.Fatalf("artist = %q, want provider artist %q", resp.Artist, result.Artist) + } + if resp.ISRC != result.ISRC { + t.Fatalf("isrc = %q, want provider isrc %q", resp.ISRC, result.ISRC) + } +} + +func TestPreferredReleaseMetadataPrefersRequestValues(t *testing.T) { + album, releaseDate, trackNumber, discNumber := preferredReleaseMetadata( + DownloadRequest{ + AlbumName: "Album (Deluxe Edition)", + ReleaseDate: "2024-01-01", + TrackNumber: 13, + DiscNumber: 2, + }, + "Album", + "2023-01-01", + 3, + 1, + ) + + if album != "Album (Deluxe Edition)" { + t.Fatalf("album = %q", album) + } + if releaseDate != "2024-01-01" { + t.Fatalf("release date = %q", releaseDate) + } + if trackNumber != 13 { + t.Fatalf("track number = %d", trackNumber) + } + if discNumber != 2 { + t.Fatalf("disc number = %d", discNumber) + } +} diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 788f816f..ec0489ff 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -54,6 +54,7 @@ const ( qobuzDownloadAPIURL = "https://www.musicdl.me/api/qobuz/download" qobuzDabMusicAPIURL = "https://dabmusic.xyz/api/stream?trackId=" qobuzDeebAPIURL = "https://dab.yeet.su/api/stream?trackId=" + qobuzAfkarAPIURL = "https://qbz.afkarxyz.fun/api/track/" qobuzSquidAPIURL = "https://qobuz.squid.wtf/api/download-music?country=US&track_id=" qobuzDebugKeyXORMask = byte(0x5A) ) @@ -1019,6 +1020,10 @@ func (q *QobuzDownloader) GetArtistMetadata(resourceID string) (*ArtistResponseP func (q *QobuzDownloader) GetAvailableAPIs() []string { return []string{ qobuzDownloadAPIURL, + qobuzDabMusicAPIURL, + qobuzDeebAPIURL, + qobuzAfkarAPIURL, + qobuzSquidAPIURL, } } @@ -1039,6 +1044,8 @@ func (q *QobuzDownloader) GetAvailableProviders() []qobuzAPIProvider { {Name: "dabmusic", URL: qobuzDabMusicAPIURL, Kind: qobuzAPIKindStandard}, // "deeb" is mapped from the legacy reference fallback endpoint. {Name: "deeb", URL: qobuzDeebAPIURL, Kind: qobuzAPIKindStandard}, + // "qbz" comes from the desktop reference app and uses /api/track/{id}?quality=... + {Name: "qbz", URL: qobuzAfkarAPIURL, Kind: qobuzAPIKindStandard}, {Name: "squid", URL: qobuzSquidAPIURL, Kind: qobuzAPIKindStandard}, } } @@ -2156,6 +2163,10 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { if req.AlbumName != "" { albumName = req.AlbumName } + releaseDate := track.Album.ReleaseDate + if req.ReleaseDate != "" { + releaseDate = req.ReleaseDate + } actualTrackNumber := req.TrackNumber if actualTrackNumber == 0 { @@ -2167,7 +2178,7 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { Artist: track.Performer.Name, Album: albumName, AlbumArtist: req.AlbumArtist, - Date: track.Album.ReleaseDate, + Date: releaseDate, TrackNumber: actualTrackNumber, TotalTracks: req.TotalTracks, DiscNumber: req.DiscNumber, @@ -2231,16 +2242,24 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { lyricsLRC = parallelResult.LyricsLRC } + resultAlbum, resultReleaseDate, resultTrackNumber, resultDiscNumber := preferredReleaseMetadata( + req, + track.Album.Title, + track.Album.ReleaseDate, + actualTrackNumber, + req.DiscNumber, + ) + return QobuzDownloadResult{ FilePath: outputPath, BitDepth: actualBitDepth, SampleRate: actualSampleRate, Title: track.Title, Artist: track.Performer.Name, - Album: track.Album.Title, - ReleaseDate: track.Album.ReleaseDate, - TrackNumber: actualTrackNumber, - DiscNumber: req.DiscNumber, + Album: resultAlbum, + ReleaseDate: resultReleaseDate, + TrackNumber: resultTrackNumber, + DiscNumber: resultDiscNumber, ISRC: track.ISRC, LyricsLRC: lyricsLRC, }, nil diff --git a/go_backend/qobuz_test.go b/go_backend/qobuz_test.go index d19629b6..9d15d318 100644 --- a/go_backend/qobuz_test.go +++ b/go_backend/qobuz_test.go @@ -213,14 +213,16 @@ func TestExtractQobuzAlbumIDsFromArtistHTML(t *testing.T) { func TestQobuzAvailableProviders(t *testing.T) { providers := NewQobuzDownloader().GetAvailableProviders() - if len(providers) != 3 { - t.Fatalf("expected 3 Qobuz providers, got %d", len(providers)) + if len(providers) != 5 { + t.Fatalf("expected 5 Qobuz providers, got %d", len(providers)) } want := map[string]string{ "musicdl": qobuzAPIKindMusicDL, "dabmusic": qobuzAPIKindStandard, "deeb": qobuzAPIKindStandard, + "qbz": qobuzAPIKindStandard, + "squid": qobuzAPIKindStandard, } for _, provider := range providers { diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 8a68826d..a837380d 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -2189,16 +2189,24 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { lyricsLRC = parallelResult.LyricsLRC } + resultAlbum, resultReleaseDate, resultTrackNumber, resultDiscNumber := preferredReleaseMetadata( + req, + track.Album.Title, + track.Album.ReleaseDate, + actualTrackNumber, + actualDiscNumber, + ) + return TidalDownloadResult{ FilePath: actualOutputPath, BitDepth: bitDepth, SampleRate: sampleRate, Title: track.Title, Artist: track.Artist.Name, - Album: track.Album.Title, - ReleaseDate: track.Album.ReleaseDate, - TrackNumber: actualTrackNumber, - DiscNumber: actualDiscNumber, + Album: resultAlbum, + ReleaseDate: resultReleaseDate, + TrackNumber: resultTrackNumber, + DiscNumber: resultDiscNumber, ISRC: track.ISRC, LyricsLRC: lyricsLRC, }, nil diff --git a/pubspec.lock b/pubspec.lock index 4fc7941f..0370159a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -557,14 +557,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: "direct main" description: @@ -633,18 +625,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -1166,26 +1158,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.30.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.16" timezone: dependency: transitive description: From 20f789f8e05c86db7d0249c66aeb37efb36440fd Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 19:01:45 +0700 Subject: [PATCH 057/145] fix(i18n): localize hardcoded strings in bulk playlist download and fix trailing newlines --- .fvmrc | 2 +- .gitignore | 2 +- lib/l10n/app_localizations.dart | 44 +++++++++++++++++++++---- lib/l10n/app_localizations_de.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_en.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_es.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_fr.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_hi.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_id.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_ja.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_ko.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_nl.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_pt.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_ru.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_tr.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_zh.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/arb/app_en.arb | 46 ++++++++++++++++++++++++++ lib/screens/queue_tab.dart | 18 +++++------ 18 files changed, 730 insertions(+), 58 deletions(-) diff --git a/.fvmrc b/.fvmrc index 5c08b3bc..3a62807c 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { "flutter": "3.38.1" -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index 517be8ed..b24e9371 100644 --- a/.gitignore +++ b/.gitignore @@ -79,4 +79,4 @@ tool/ .claude/settings.local.json # FVM Version Cache -.fvm/ \ No newline at end of file +.fvm/ diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 53bcba7f..ce8174e2 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1066,6 +1066,12 @@ abstract class AppLocalizations { /// **'Import'** String get dialogImport; + /// Confirm button in Download All dialog + /// + /// In en, this message translates to: + /// **'Download'** + String get dialogDownload; + /// Dialog button - discard changes /// /// In en, this message translates to: @@ -4434,7 +4440,7 @@ abstract class AppLocalizations { /// **'Added {count} tracks to Loved'** String snackbarAddedTracksToLoved(int count); - /// Title of the Download All confirmation dialog + /// Dialog title for bulk download confirmation /// /// In en, this message translates to: /// **'Download All'** @@ -4446,12 +4452,6 @@ abstract class AppLocalizations { /// **'Download {count} tracks?'** String dialogDownloadAllMessage(int count); - /// Confirm button in Download All dialog - /// - /// In en, this message translates to: - /// **'Download'** - String get dialogDownload; - /// Checkbox label in import dialog to skip already-downloaded songs /// /// In en, this message translates to: @@ -4817,6 +4817,36 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Refresh'** String get cacheRefresh; + + /// Dialog message for bulk playlist download confirmation + /// + /// In en, this message translates to: + /// **'Download {trackCount} {trackCount, plural, =1{track} other{tracks}} from {playlistCount} {playlistCount, plural, =1{playlist} other{playlists}}?'** + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount); + + /// Button label for bulk downloading selected playlists + /// + /// In en, this message translates to: + /// **'Download {count} {count, plural, =1{playlist} other{playlists}}'** + String bulkDownloadPlaylistsButton(int count); + + /// Button label when no playlists are selected for download + /// + /// In en, this message translates to: + /// **'Select playlists to download'** + String get bulkDownloadSelectPlaylists; + + /// Snackbar when selected playlists contain no tracks + /// + /// In en, this message translates to: + /// **'Selected playlists have no tracks'** + String get snackbarSelectedPlaylistsEmpty; + + /// Playlist count display + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 playlist} other{{count} playlists}}'** + String playlistsCount(int count); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index a3d43076..b913d844 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -536,6 +536,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get dialogImport => 'Importieren'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Verwerfen'; @@ -2580,9 +2583,6 @@ class AppLocalizationsDe extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2790,4 +2790,50 @@ class AppLocalizationsDe extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 03adf67d..07e3ca2d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -525,6 +525,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsEn extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,4 +2762,50 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c953dbc8..8a5a974b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -525,6 +525,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsEs extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,6 +2762,52 @@ class AppLocalizationsEs extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } /// The translations for Spanish Castilian, as used in Spain (`es_ES`). diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4baf9cba..68d677e1 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -527,6 +527,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2554,9 +2557,6 @@ class AppLocalizationsFr extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2764,4 +2764,50 @@ class AppLocalizationsFr extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 46d6572a..5da28983 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -525,6 +525,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsHi extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,4 +2762,50 @@ class AppLocalizationsHi extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 6aa97510..7fc8d1de 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -528,6 +528,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get dialogImport => 'Impor'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Buang'; @@ -2559,9 +2562,6 @@ class AppLocalizationsId extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2769,4 +2769,50 @@ class AppLocalizationsId extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 92184e0b..04a7a0c2 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -521,6 +521,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get dialogImport => 'インポート'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => '破棄'; @@ -2539,9 +2542,6 @@ class AppLocalizationsJa extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2749,4 +2749,50 @@ class AppLocalizationsJa extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index aed486d5..80e8a1df 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -510,6 +510,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get dialogImport => '불러오기'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => '취소'; @@ -2532,9 +2535,6 @@ class AppLocalizationsKo extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2742,4 +2742,50 @@ class AppLocalizationsKo extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 87c5385a..af603f60 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -525,6 +525,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsNl extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,4 +2762,50 @@ class AppLocalizationsNl extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index d45df4f9..fc40c186 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -525,6 +525,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsPt extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,6 +2762,52 @@ class AppLocalizationsPt extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } /// The translations for Portuguese, as used in Portugal (`pt_PT`). diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 3e9d7e73..7a542295 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -534,6 +534,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get dialogImport => 'Импорт'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Отменить'; @@ -2611,9 +2614,6 @@ class AppLocalizationsRu extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2821,4 +2821,50 @@ class AppLocalizationsRu extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 48a9a7d2..1e5727bf 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -530,6 +530,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get dialogImport => 'İçe aktar'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Vazgeç'; @@ -2564,9 +2567,6 @@ class AppLocalizationsTr extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2774,4 +2774,50 @@ class AppLocalizationsTr extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 2287f76a..68381882 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -525,6 +525,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsZh extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,6 +2762,52 @@ class AppLocalizationsZh extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 10bf4938..bc3f2e13 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -671,6 +671,10 @@ "@dialogImport": { "description": "Dialog button - import data" }, + "dialogDownload": "Download", + "@dialogDownload": { + "description": "Dialog button - download action" + }, "dialogDiscard": "Discard", "@dialogDiscard": { "description": "Dialog button - discard changes" @@ -3657,5 +3661,47 @@ "cacheRefresh": "Refresh", "@cacheRefresh": { "description": "Tooltip for refresh button on cache management page" + }, + "dialogDownloadAllTitle": "Download All", + "@dialogDownloadAllTitle": { + "description": "Dialog title for bulk download confirmation" + }, + "dialogDownloadPlaylistsMessage": "Download {trackCount} {trackCount, plural, =1{track} other{tracks}} from {playlistCount} {playlistCount, plural, =1{playlist} other{playlists}}?", + "@dialogDownloadPlaylistsMessage": { + "description": "Dialog message for bulk playlist download confirmation", + "placeholders": { + "trackCount": { + "type": "int" + }, + "playlistCount": { + "type": "int" + } + } + }, + "bulkDownloadPlaylistsButton": "Download {count} {count, plural, =1{playlist} other{playlists}}", + "@bulkDownloadPlaylistsButton": { + "description": "Button label for bulk downloading selected playlists", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "bulkDownloadSelectPlaylists": "Select playlists to download", + "@bulkDownloadSelectPlaylists": { + "description": "Button label when no playlists are selected for download" + }, + "snackbarSelectedPlaylistsEmpty": "Selected playlists have no tracks", + "@snackbarSelectedPlaylistsEmpty": { + "description": "Snackbar when selected playlists contain no tracks" + }, + "playlistsCount": "{count, plural, =1{1 playlist} other{{count} playlists}}", + "@playlistsCount": { + "description": "Playlist count display", + "placeholders": { + "count": { + "type": "int" + } + } } } diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 7e3f96f9..5b14ff21 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -1326,7 +1326,7 @@ class _QueueTabState extends ConsumerState { if (totalTracks == 0) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Selected playlists have no tracks')), + SnackBar(content: Text(context.l10n.snackbarSelectedPlaylistsEmpty)), ); return; } @@ -1334,11 +1334,9 @@ class _QueueTabState extends ConsumerState { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Download All'), + title: Text(ctx.l10n.dialogDownloadAllTitle), content: Text( - 'Download $totalTracks ${totalTracks == 1 ? 'track' : 'tracks'} ' - 'from ${selectedPlaylists.length} ' - '${selectedPlaylists.length == 1 ? 'playlist' : 'playlists'}?', + ctx.l10n.dialogDownloadPlaylistsMessage(totalTracks, selectedPlaylists.length), ), actions: [ TextButton( @@ -1347,7 +1345,7 @@ class _QueueTabState extends ConsumerState { ), FilledButton( onPressed: () => Navigator.pop(ctx, true), - child: const Text('Download'), + child: Text(ctx.l10n.dialogDownload), ), ], ), @@ -1374,8 +1372,8 @@ class _QueueTabState extends ConsumerState { if (settings.askQualityBeforeDownload) { DownloadServicePicker.show( context, - trackName: '$totalTracks tracks', - artistName: '${selectedPlaylists.length} playlists', + trackName: context.l10n.tracksCount(totalTracks), + artistName: context.l10n.playlistsCount(selectedPlaylists.length), onSelect: (quality, service) { enqueueAll(qualityOverride: quality, service: service); if (!mounted) return; @@ -1549,8 +1547,8 @@ class _QueueTabState extends ConsumerState { icon: const Icon(Icons.download_rounded), label: Text( selectedCount > 0 - ? 'Download $selectedCount ${selectedCount == 1 ? 'playlist' : 'playlists'}' - : 'Select playlists to download', + ? context.l10n.bulkDownloadPlaylistsButton(selectedCount) + : context.l10n.bulkDownloadSelectPlaylists, ), style: FilledButton.styleFrom( backgroundColor: selectedCount > 0 From 8540da484fc39b6dcac73b585cff77ce5602cc39 Mon Sep 17 00:00:00 2001 From: Amonoman Date: Sun, 15 Mar 2026 13:02:23 +0100 Subject: [PATCH 058/145] Add AltStore source and update README --- README.md | 7 ++++++- apps.json | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 apps.json diff --git a/README.md b/README.md index bbdd10d9..21feb955 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,11 @@ A: Yes, the app is open source and you can verify the code yourself. Each releas **Q: Why is download not working in my country?** A: Some countries have restricted access to certain streaming service APIs. If downloads are failing, try using a VPN to connect through a different region. +**Q: Can I add SpotiFLAC to AltStore or SideStore?** +A: Yes! You can add the official source to receive updates directly within the app. Just copy this link: +https://raw.githubusercontent.com/zarzet/SpotiFLAC-Mobile/refs/heads/main/apps.json +In AltStore/SideStore, go to the Browse tab, tap Sources at the top, then tap the + icon and paste the link. + ### Want to support SpotiFLAC-Mobile? @@ -87,4 +92,4 @@ _If this software is useful and brings you value, consider supporting the projec > [!TIP] > -> **Star Us**, You will receive all release notifications from GitHub without any delay ~ +> **Star Us**, You will receive all release notifications from GitHub without any delay diff --git a/apps.json b/apps.json new file mode 100644 index 00000000..459bc17d --- /dev/null +++ b/apps.json @@ -0,0 +1,18 @@ +{ + "name": "SpotiFLAC Source", + "identifier": "com.zarzet.spotiflac.source", + "subtitle": "FLAC Downloader for iOS", + "apps": [ + { + "name": "SpotiFLAC", + "bundleIdentifier": "com.zarzet.spotiflac", + "developerName": "zarzet", + "version": "3.8.0", + "versionDate": "2026-03-14", + "downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v3.8.0/SpotiFLAC-v3.8.0-ios-unsigned.ipa", + "localizedDescription": "Mobile version of SpotiFLAC written in Flutter. Download Tracks in true FLAC from Tidal, Qobuz, & Amazon Music.", + "iconURL": "https://raw.githubusercontent.com/zarzet/SpotiFLAC-Mobile/main/assets/icon/icon.png", + "size": 33659289 + } + ] +} From 8a5dc0edfe61bfc7ed4aba6687e0da26507829a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 12:02:29 +0000 Subject: [PATCH 059/145] chore(deps): update dependency flutter to v3.41.4 --- .fvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.fvmrc b/.fvmrc index 3a62807c..c8437d38 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.38.1" + "flutter": "3.41.4" } From 7bb808cba53bfd2abbda894ba68d951c467dd20c Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 19:11:29 +0700 Subject: [PATCH 060/145] ci: auto-update AltStore source (apps.json) on release --- .github/workflows/release.yml | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57772b30..b45e04e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -393,6 +393,63 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + update-altstore: + runs-on: ubuntu-latest + needs: [get-version, build-ios, create-release] + if: ${{ needs.get-version.outputs.is_prerelease != 'true' }} + permissions: + contents: write + + steps: + - name: Checkout main branch + uses: actions/checkout@v6 + with: + ref: main + + - name: Download iOS IPA + uses: actions/download-artifact@v7 + with: + name: ios-ipa + path: ./release + + - name: Update apps.json + run: | + VERSION="${{ needs.get-version.outputs.version }}" + VERSION_NUM="${VERSION#v}" + DATE=$(date -u +%Y-%m-%d) + IPA_FILE=$(find ./release -name "*ios*.ipa" | head -1) + + if [ -z "$IPA_FILE" ]; then + echo "WARNING: IPA file not found, skipping apps.json update" + exit 0 + fi + + IPA_SIZE=$(stat -c%s "$IPA_FILE" 2>/dev/null || stat -f%z "$IPA_FILE") + + if [ ! -f apps.json ]; then + echo "WARNING: apps.json not found on main, skipping" + exit 0 + fi + + jq --arg ver "$VERSION_NUM" \ + --arg date "$DATE" \ + --arg url "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/${VERSION}/SpotiFLAC-${VERSION}-ios-unsigned.ipa" \ + --argjson size "$IPA_SIZE" \ + '.apps[0].version = $ver | .apps[0].versionDate = $date | .apps[0].downloadURL = $url | .apps[0].size = $size' \ + apps.json > apps.json.tmp && mv apps.json.tmp apps.json + + echo "Updated apps.json:" + cat apps.json + + - name: Commit and push + run: | + VERSION="${{ needs.get-version.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add apps.json + git diff --cached --quiet && echo "No changes to commit" || \ + (git commit -m "chore: update AltStore source to ${VERSION}" && git push) + notify-telegram: runs-on: ubuntu-latest needs: [get-version, create-release] From 18607597e925a1e536775d56df0b3de727ee0966 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 19:41:25 +0700 Subject: [PATCH 061/145] fix: correct AltStore icon URL to assets/images/logo.png --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 459bc17d..2e4c6718 100644 --- a/apps.json +++ b/apps.json @@ -11,7 +11,7 @@ "versionDate": "2026-03-14", "downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v3.8.0/SpotiFLAC-v3.8.0-ios-unsigned.ipa", "localizedDescription": "Mobile version of SpotiFLAC written in Flutter. Download Tracks in true FLAC from Tidal, Qobuz, & Amazon Music.", - "iconURL": "https://raw.githubusercontent.com/zarzet/SpotiFLAC-Mobile/main/assets/icon/icon.png", + "iconURL": "https://raw.githubusercontent.com/zarzet/SpotiFLAC-Mobile/main/assets/images/logo.png", "size": 33659289 } ] From 941347b007bda64015331d603f5c8758ea2c1843 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:16:44 +0700 Subject: [PATCH 062/145] feat: add Opus 320kbps quality, remove Tidal HIGH tier - Add YouTubeQualityOpus320 constant and opus_320 parser case in Go backend - Expand opus supported bitrates to [128, 256, 320] across Go, Dart settings, and UI - Update default YouTube Opus option from 256 to 320kbps - Remove Tidal HIGH (lossy 320kbps) quality from Go backend, settings model, settings provider, download queue provider (both SAF and non-SAF paths), settings UI (quality option, format picker, helper methods), and l10n keys - Add settings migration v6: auto-migrate users with audioQuality=HIGH to LOSSLESS - Update and add Go test cases for opus_320 and adjusted max bitrate - Regenerate l10n files, remove 10 unused downloadLossy* l10n keys --- go_backend/tidal.go | 40 +- go_backend/youtube.go | 5 +- go_backend/youtube_quality_test.go | 17 +- lib/l10n/app_localizations.dart | 90 ++--- lib/l10n/app_localizations_de.dart | 54 +-- lib/l10n/app_localizations_en.dart | 54 +-- lib/l10n/app_localizations_es.dart | 54 +-- lib/l10n/app_localizations_fr.dart | 54 +-- lib/l10n/app_localizations_hi.dart | 54 +-- lib/l10n/app_localizations_id.dart | 54 +-- lib/l10n/app_localizations_ja.dart | 54 +-- lib/l10n/app_localizations_ko.dart | 54 +-- lib/l10n/app_localizations_nl.dart | 54 +-- lib/l10n/app_localizations_pt.dart | 54 +-- lib/l10n/app_localizations_ru.dart | 54 +-- lib/l10n/app_localizations_tr.dart | 54 +-- lib/l10n/app_localizations_zh.dart | 54 +-- lib/l10n/arb/app_en.arb | 83 ++-- lib/models/settings.dart | 7 +- lib/models/settings.g.dart | 2 - lib/providers/download_queue_provider.dart | 381 +++++------------- lib/providers/settings_provider.dart | 30 +- .../settings/download_settings_page.dart | 132 +----- lib/widgets/download_service_picker.dart | 10 +- 24 files changed, 513 insertions(+), 986 deletions(-) diff --git a/go_backend/tidal.go b/go_backend/tidal.go index a837380d..582e4ff6 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1961,11 +1961,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { outputExt := strings.TrimSpace(req.OutputExt) if outputExt == "" { - if quality == "HIGH" { - outputExt = ".m4a" - } else { - outputExt = ".flac" - } + outputExt = ".flac" } else if !strings.HasPrefix(outputExt, ".") { outputExt = "." + outputExt } @@ -1979,7 +1975,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { } m4aPath = outputPath } else { - if outputExt == ".m4a" || quality == "HIGH" { + if outputExt == ".m4a" { filename = sanitizeFilename(filename) + ".m4a" outputPath = filepath.Join(req.OutputDir, filename) m4aPath = outputPath @@ -1992,10 +1988,8 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { if fileInfo, statErr := os.Stat(outputPath); statErr == nil && fileInfo.Size() > 0 { return TidalDownloadResult{FilePath: "EXISTS:" + outputPath}, nil } - if quality != "HIGH" { - if fileInfo, statErr := os.Stat(m4aPath); statErr == nil && fileInfo.Size() > 0 { - return TidalDownloadResult{FilePath: "EXISTS:" + m4aPath}, nil - } + if fileInfo, statErr := os.Stat(m4aPath); statErr == nil && fileInfo.Size() > 0 { + return TidalDownloadResult{FilePath: "EXISTS:" + m4aPath}, nil } } @@ -2151,27 +2145,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { fmt.Println("[Tidal] No lyrics available from parallel fetch") } } else if (isSafOutput && actualExt == ".m4a") || (!isSafOutput && strings.HasSuffix(actualOutputPath, ".m4a")) { - if quality == "HIGH" { - GoLog("[Tidal] HIGH quality M4A - skipping metadata embedding (file from server is already valid)\n") - - if req.EmbedMetadata && req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { - lyricsMode := req.LyricsMode - if lyricsMode == "" { - lyricsMode = "embed" - } - - if !isSafOutput && (lyricsMode == "external" || lyricsMode == "both") { - GoLog("[Tidal] Saving external LRC file for M4A (mode: %s)...\n", lyricsMode) - if lrcPath, lrcErr := SaveLRCFile(actualOutputPath, parallelResult.LyricsLRC); lrcErr != nil { - GoLog("[Tidal] Warning: failed to save LRC file: %v\n", lrcErr) - } else { - GoLog("[Tidal] LRC file saved: %s\n", lrcPath) - } - } - } - } else { - fmt.Println("[Tidal] Skipping metadata embedding for M4A file (will be handled after FFmpeg conversion)") - } + fmt.Println("[Tidal] Skipping metadata embedding for M4A file (will be handled after FFmpeg conversion)") } if !isSafOutput { @@ -2181,10 +2155,6 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { bitDepth := downloadInfo.BitDepth sampleRate := downloadInfo.SampleRate lyricsLRC := "" - if quality == "HIGH" { - bitDepth = 0 - sampleRate = 44100 - } if req.EmbedMetadata && req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { lyricsLRC = parallelResult.LyricsLRC } diff --git a/go_backend/youtube.go b/go_backend/youtube.go index 58e5d751..3bb65f5a 100644 --- a/go_backend/youtube.go +++ b/go_backend/youtube.go @@ -29,6 +29,7 @@ var ( type YouTubeQuality string const ( + YouTubeQualityOpus320 YouTubeQuality = "opus_320" YouTubeQualityOpus256 YouTubeQuality = "opus_256" YouTubeQualityOpus128 YouTubeQuality = "opus_128" YouTubeQualityMP3128 YouTubeQuality = "mp3_128" @@ -37,7 +38,7 @@ const ( ) var ( - youtubeOpusSupportedBitrates = []int{128, 256} + youtubeOpusSupportedBitrates = []int{128, 256, 320} youtubeMp3SupportedBitrates = []int{128, 256, 320} ) @@ -146,6 +147,8 @@ func parseYouTubeQualityInput(raw string) (format string, bitrate int, normalize switch normalizedRaw { case "opus_256", "opus256", "opus": return "opus", 256, YouTubeQualityOpus256 + case "opus_320", "opus320": + return "opus", 320, YouTubeQualityOpus320 case "opus_128", "opus128": return "opus", 128, YouTubeQualityOpus128 case "mp3_320", "mp3320", "mp3", "": diff --git a/go_backend/youtube_quality_test.go b/go_backend/youtube_quality_test.go index cb77e5bb..e0f2ebbf 100644 --- a/go_backend/youtube_quality_test.go +++ b/go_backend/youtube_quality_test.go @@ -30,8 +30,8 @@ func TestParseYouTubeQualityInput_Mp3NormalizesToSupportedBitrates(t *testing.T) func TestParseYouTubeQualityInput_PicksNearestSupportedBitrate(t *testing.T) { _, opusBitrate, _ := parseYouTubeQualityInput("opus_999") - if opusBitrate != 256 { - t.Fatalf("expected opus normalization to 256, got %d", opusBitrate) + if opusBitrate != 320 { + t.Fatalf("expected opus normalization to 320, got %d", opusBitrate) } _, mp3Bitrate, _ := parseYouTubeQualityInput("mp3_1") @@ -39,3 +39,16 @@ func TestParseYouTubeQualityInput_PicksNearestSupportedBitrate(t *testing.T) { t.Fatalf("expected mp3 normalization to 128, got %d", mp3Bitrate) } } + +func TestParseYouTubeQualityInput_Opus320(t *testing.T) { + format, bitrate, normalized := parseYouTubeQualityInput("opus_320") + if format != "opus" { + t.Fatalf("expected opus format, got %s", format) + } + if bitrate != 320 { + t.Fatalf("expected 320 bitrate, got %d", bitrate) + } + if normalized != YouTubeQualityOpus320 { + t.Fatalf("expected %s normalized, got %s", YouTubeQualityOpus320, normalized) + } +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ce8174e2..ae9a4cc0 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3838,6 +3838,36 @@ abstract class AppLocalizations { /// **'FFmpeg metadata embed failed'** String get trackReEnrichFfmpegFailed; + /// Action/button label for queueing FLAC redownloads for local tracks + /// + /// In en, this message translates to: + /// **'Queue FLAC'** + String get queueFlacAction; + + /// Confirmation dialog body before queueing FLAC redownloads for local tracks + /// + /// In en, this message translates to: + /// **'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n{count} selected'** + String queueFlacConfirmMessage(int count); + + /// Snackbar while resolving remote matches for local FLAC redownloads + /// + /// In en, this message translates to: + /// **'Finding FLAC matches... ({current}/{total})'** + String queueFlacFindingProgress(int current, int total); + + /// Snackbar when no safe FLAC redownload matches were found + /// + /// In en, this message translates to: + /// **'No reliable online matches found for the selection'** + String get queueFlacNoReliableMatches; + + /// Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped + /// + /// In en, this message translates to: + /// **'Added {addedCount} tracks to queue, skipped {skippedCount}'** + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount); + /// Snackbar when save operation fails /// /// In en, this message translates to: @@ -4602,18 +4632,6 @@ abstract class AppLocalizations { /// **'Select a built-in service to enable'** String get downloadSelectServiceToEnable; - /// Quality option label for Tidal lossy 320kbps - /// - /// In en, this message translates to: - /// **'Lossy 320kbps'** - String get downloadLossy320; - - /// Setting title to pick output format for Tidal lossy downloads - /// - /// In en, this message translates to: - /// **'Lossy Format'** - String get downloadLossyFormat; - /// Info hint when non-Tidal/Qobuz service is selected /// /// In en, this message translates to: @@ -4740,54 +4758,6 @@ abstract class AppLocalizations { /// **'Auto'** String get downloadMusixmatchAuto; - /// Title of the Tidal lossy format picker bottom sheet - /// - /// In en, this message translates to: - /// **'Lossy 320kbps Format'** - String get downloadLossy320Format; - - /// Description in the Tidal lossy format picker - /// - /// In en, this message translates to: - /// **'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'** - String get downloadLossy320FormatDesc; - - /// Tidal lossy format option - MP3 320kbps - /// - /// In en, this message translates to: - /// **'MP3 320kbps'** - String get downloadLossyMp3; - - /// Subtitle for MP3 320kbps option - /// - /// In en, this message translates to: - /// **'Best compatibility, ~10MB per track'** - String get downloadLossyMp3Subtitle; - - /// Tidal lossy format option - Opus 256kbps - /// - /// In en, this message translates to: - /// **'Opus 256kbps'** - String get downloadLossyOpus256; - - /// Subtitle for Opus 256kbps option - /// - /// In en, this message translates to: - /// **'Best quality Opus, ~8MB per track'** - String get downloadLossyOpus256Subtitle; - - /// Tidal lossy format option - Opus 128kbps - /// - /// In en, this message translates to: - /// **'Opus 128kbps'** - String get downloadLossyOpus128; - - /// Subtitle for Opus 128kbps option - /// - /// In en, this message translates to: - /// **'Smallest size, ~4MB per track'** - String get downloadLossyOpus128Subtitle; - /// Subtitle for 'Any' network mode option /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b913d844..ce489e3d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2172,6 +2172,28 @@ class AppLocalizationsDe extends AppLocalizations { String get trackReEnrichFfmpegFailed => 'FFmpeg Metadaten-Einbettung fehlgeschlagen'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Fehler: $error'; @@ -2666,12 +2688,6 @@ class AppLocalizationsDe extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2748,32 +2764,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 07e3ca2d..bd280e94 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsEn extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 8a5a974b..81ce15ff 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsEs extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsEs extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 68d677e1..b35b22d0 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2147,6 +2147,28 @@ class AppLocalizationsFr extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2640,12 +2662,6 @@ class AppLocalizationsFr extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2722,32 +2738,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 5da28983..e36316e9 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsHi extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsHi extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 7fc8d1de..68bffbe5 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2152,6 +2152,28 @@ class AppLocalizationsId extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Antrekan FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Cari kecocokan online untuk track yang dipilih lalu antrekan download FLAC.\n\nFile yang sudah ada tidak akan diubah atau dihapus.\n\nHanya kecocokan dengan keyakinan tinggi yang akan diantrikan otomatis.\n\n$count dipilih'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Mencari kecocokan FLAC... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'Tidak ada kecocokan online yang cukup meyakinkan untuk pilihan ini'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Menambahkan $addedCount track ke antrean, melewati $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2645,12 +2667,6 @@ class AppLocalizationsId extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2727,32 +2743,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 04a7a0c2..8145c794 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2132,6 +2132,28 @@ class AppLocalizationsJa extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return '失敗: $error'; @@ -2625,12 +2647,6 @@ class AppLocalizationsJa extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2707,32 +2723,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 80e8a1df..66d21aec 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2125,6 +2125,28 @@ class AppLocalizationsKo extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2618,12 +2640,6 @@ class AppLocalizationsKo extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2700,32 +2716,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index af603f60..e87906ac 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsNl extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsNl extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index fc40c186..466572ce 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsPt extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsPt extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 7a542295..dd460698 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2198,6 +2198,28 @@ class AppLocalizationsRu extends AppLocalizations { String get trackReEnrichFfmpegFailed => 'Ошибка встраивания метаданных FFmpeg'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Ошибка: $error'; @@ -2697,12 +2719,6 @@ class AppLocalizationsRu extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2779,32 +2795,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 1e5727bf..6e1a37f1 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2157,6 +2157,28 @@ class AppLocalizationsTr extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2650,12 +2672,6 @@ class AppLocalizationsTr extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2732,32 +2748,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 68381882..9248d997 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsZh extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsZh extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index bc3f2e13..3de4ec31 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2819,6 +2819,47 @@ "@trackReEnrichFfmpegFailed": { "description": "Snackbar when FFmpeg embed fails for MP3/Opus" }, + "queueFlacAction": "Queue FLAC", + "@queueFlacAction": { + "description": "Action/button label for queueing FLAC redownloads for local tracks" + }, + "queueFlacConfirmMessage": "Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n{count} selected", + "@queueFlacConfirmMessage": { + "description": "Confirmation dialog body before queueing FLAC redownloads for local tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueFlacFindingProgress": "Finding FLAC matches... ({current}/{total})", + "@queueFlacFindingProgress": { + "description": "Snackbar while resolving remote matches for local FLAC redownloads", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "queueFlacNoReliableMatches": "No reliable online matches found for the selection", + "@queueFlacNoReliableMatches": { + "description": "Snackbar when no safe FLAC redownload matches were found" + }, + "queueFlacQueuedWithSkipped": "Added {addedCount} tracks to queue, skipped {skippedCount}", + "@queueFlacQueuedWithSkipped": { + "description": "Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped", + "placeholders": { + "addedCount": { + "type": "int" + }, + "skippedCount": { + "type": "int" + } + } + }, "trackSaveFailed": "Failed: {error}", "@trackSaveFailed": { "description": "Snackbar when save operation fails", @@ -3513,14 +3554,7 @@ "@downloadSelectServiceToEnable": { "description": "Hint shown instead of Ask-quality subtitle when no built-in service selected" }, - "downloadLossy320": "Lossy 320kbps", - "@downloadLossy320": { - "description": "Quality option label for Tidal lossy 320kbps" - }, - "downloadLossyFormat": "Lossy Format", - "@downloadLossyFormat": { - "description": "Setting title to pick output format for Tidal lossy downloads" - }, + "downloadSelectTidalQobuz": "Select Tidal or Qobuz above to configure quality", "@downloadSelectTidalQobuz": { "description": "Info hint when non-Tidal/Qobuz service is selected" @@ -3606,38 +3640,7 @@ "@downloadMusixmatchAuto": { "description": "Button to reset Musixmatch language to automatic" }, - "downloadLossy320Format": "Lossy 320kbps Format", - "@downloadLossy320Format": { - "description": "Title of the Tidal lossy format picker bottom sheet" - }, - "downloadLossy320FormatDesc": "Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.", - "@downloadLossy320FormatDesc": { - "description": "Description in the Tidal lossy format picker" - }, - "downloadLossyMp3": "MP3 320kbps", - "@downloadLossyMp3": { - "description": "Tidal lossy format option - MP3 320kbps" - }, - "downloadLossyMp3Subtitle": "Best compatibility, ~10MB per track", - "@downloadLossyMp3Subtitle": { - "description": "Subtitle for MP3 320kbps option" - }, - "downloadLossyOpus256": "Opus 256kbps", - "@downloadLossyOpus256": { - "description": "Tidal lossy format option - Opus 256kbps" - }, - "downloadLossyOpus256Subtitle": "Best quality Opus, ~8MB per track", - "@downloadLossyOpus256Subtitle": { - "description": "Subtitle for Opus 256kbps option" - }, - "downloadLossyOpus128": "Opus 128kbps", - "@downloadLossyOpus128": { - "description": "Tidal lossy format option - Opus 128kbps" - }, - "downloadLossyOpus128Subtitle": "Smallest size, ~4MB per track", - "@downloadLossyOpus128Subtitle": { - "description": "Subtitle for Opus 128kbps option" - }, + "downloadNetworkAnySubtitle": "WiFi + Mobile Data", "@downloadNetworkAnySubtitle": { "description": "Subtitle for 'Any' network mode option" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 67e3e9ac..9bf2ee10 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -38,10 +38,8 @@ class AppSettings { final bool showExtensionStore; final String locale; final String lyricsMode; - final String - tidalHighFormat; // Format for Tidal HIGH quality: 'mp3_320', 'opus_256', or 'opus_128' final int - youtubeOpusBitrate; // YouTube Opus bitrate (supported: 128/256 kbps) + youtubeOpusBitrate; // YouTube Opus bitrate (supported: 128/256/320 kbps) final int youtubeMp3Bitrate; // YouTube MP3 bitrate (supported: 128/256/320 kbps) final bool @@ -114,7 +112,6 @@ class AppSettings { this.showExtensionStore = true, this.locale = 'system', this.lyricsMode = 'embed', - this.tidalHighFormat = 'mp3_320', this.youtubeOpusBitrate = 256, this.youtubeMp3Bitrate = 320, this.useAllFilesAccess = false, @@ -178,7 +175,6 @@ class AppSettings { bool? showExtensionStore, String? locale, String? lyricsMode, - String? tidalHighFormat, int? youtubeOpusBitrate, int? youtubeMp3Bitrate, bool? useAllFilesAccess, @@ -241,7 +237,6 @@ class AppSettings { showExtensionStore: showExtensionStore ?? this.showExtensionStore, locale: locale ?? this.locale, lyricsMode: lyricsMode ?? this.lyricsMode, - tidalHighFormat: tidalHighFormat ?? this.tidalHighFormat, youtubeOpusBitrate: youtubeOpusBitrate ?? this.youtubeOpusBitrate, youtubeMp3Bitrate: youtubeMp3Bitrate ?? this.youtubeMp3Bitrate, useAllFilesAccess: useAllFilesAccess ?? this.useAllFilesAccess, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index c3eecb50..99a5114d 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -44,7 +44,6 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( showExtensionStore: json['showExtensionStore'] as bool? ?? true, locale: json['locale'] as String? ?? 'system', lyricsMode: json['lyricsMode'] as String? ?? 'embed', - tidalHighFormat: json['tidalHighFormat'] as String? ?? 'mp3_320', youtubeOpusBitrate: (json['youtubeOpusBitrate'] as num?)?.toInt() ?? 256, youtubeMp3Bitrate: (json['youtubeMp3Bitrate'] as num?)?.toInt() ?? 320, useAllFilesAccess: json['useAllFilesAccess'] as bool? ?? false, @@ -119,7 +118,6 @@ Map _$AppSettingsToJson( 'showExtensionStore': instance.showExtensionStore, 'locale': instance.locale, 'lyricsMode': instance.lyricsMode, - 'tidalHighFormat': instance.tidalHighFormat, 'youtubeOpusBitrate': instance.youtubeOpusBitrate, 'youtubeMp3Bitrate': instance.youtubeMp3Bitrate, 'useAllFilesAccess': instance.useAllFilesAccess, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index df46e084..a01b8e88 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1840,7 +1840,7 @@ class DownloadQueueNotifier extends Notifier { return '.opus'; } if (service.toLowerCase() == 'tidal' && quality == 'HIGH') { - return '.m4a'; + return '.flac'; // HIGH quality no longer available; fallback to FLAC } return '.flac'; } @@ -2383,7 +2383,8 @@ class DownloadQueueNotifier extends Notifier { backendResult['album_artist'] as String?, ); - final hasOverrides = backendTrackNum != null || + final hasOverrides = + backendTrackNum != null || backendDiscNum != null || backendYear != null || backendAlbum != null || @@ -3612,6 +3613,11 @@ class DownloadQueueNotifier extends Notifier { 'Quality: $quality${item.qualityOverride != null ? ' (override)' : ''}', ); } + + if (!useSaf) { + await _ensureDirExists(outputDir, label: 'Output folder'); + } + _log.d('Output dir: $outputDir'); final normalizedTrackNumber = @@ -3903,7 +3909,6 @@ class DownloadQueueNotifier extends Notifier { isContentUriPath && effectiveSafMode && actualService == 'tidal' && - quality != 'HIGH' && filePath.endsWith('.flac') && (mimeType == null || mimeType.contains('flac')); @@ -3918,73 +3923,50 @@ class DownloadQueueNotifier extends Notifier { final currentFilePath = filePath; if (isContentUriPath && effectiveSafMode) { - if (quality == 'HIGH') { - final tidalHighFormat = settings.tidalHighFormat; - _log.i( - 'Tidal HIGH quality (SAF), converting M4A to $tidalHighFormat...', - ); - - final tempPath = await _copySafToTemp(currentFilePath); - if (tempPath != null) { - String? convertedPath; - try { + _log.d('M4A file detected (SAF), converting to FLAC...'); + final tempPath = await _copySafToTemp(currentFilePath); + if (tempPath != null) { + String? flacPath; + try { + final length = await File(tempPath).length(); + if (length < 1024) { + _log.w('Temp M4A is too small (<1KB), skipping conversion'); + } else { updateItemStatus( item.id, DownloadStatus.downloading, progress: 0.95, ); - - final format = tidalHighFormat.startsWith('opus') - ? 'opus' - : 'mp3'; - convertedPath = await FFmpegService.convertM4aToLossy( - tempPath, - format: format, - bitrate: tidalHighFormat, - deleteOriginal: false, - ); - - if (convertedPath != null) { - _log.i( - 'Successfully converted M4A to $format (temp): $convertedPath', - ); - _log.i('Embedding metadata to $format...'); - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.99, + flacPath = await FFmpegService.convertM4aToFlac(tempPath); + if (flacPath != null) { + _log.d('Converted to FLAC (temp): $flacPath'); + _log.d('Embedding metadata and cover to converted FLAC...'); + final finalTrack = _buildTrackForMetadataEmbedding( + trackToDownload, + result, + resolvedAlbumArtist, ); final backendGenre = result['genre'] as String?; final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; - if (format == 'mp3') { - await _embedMetadataToMp3( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - } else { - await _embedMetadataToOpus( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - } + await _embedMetadataAndCover( + flacPath, + finalTrack, + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + writeExternalLrc: false, + ); - final newExt = format == 'opus' ? '.opus' : '.mp3'; - final newFileName = '${safBaseName ?? 'track'}$newExt'; + final newFileName = '${safBaseName ?? 'track'}.flac'; final newUri = await _writeTempToSaf( treeUri: settings.downloadTreeUri, relativeDir: effectiveOutputDir, fileName: newFileName, - mimeType: _mimeTypeForExt(newExt), - srcPath: convertedPath, + mimeType: _mimeTypeForExt('.flac'), + srcPath: flacPath, ); if (newUri != null) { @@ -3993,58 +3975,60 @@ class DownloadQueueNotifier extends Notifier { } filePath = newUri; finalSafFileName = newFileName; - final bitrateDisplay = tidalHighFormat.contains('_') - ? '${tidalHighFormat.split('_').last}kbps' - : '320kbps'; - actualQuality = '${format.toUpperCase()} $bitrateDisplay'; } else { - _log.w( - 'Failed to write converted $format to SAF, keeping M4A', - ); - actualQuality = 'AAC 320kbps'; + _log.w('Failed to write FLAC to SAF, keeping M4A'); } } else { - _log.w( - 'M4A to $format conversion failed, keeping M4A file', - ); - actualQuality = 'AAC 320kbps'; - } - } catch (e) { - _log.w('SAF M4A conversion failed: $e'); - actualQuality = 'AAC 320kbps'; - } finally { - // Clean up temp files - try { - await File(tempPath).delete(); - } catch (_) {} - if (convertedPath != null) { - try { - await File(convertedPath).delete(); - } catch (_) {} + _log.w('FFmpeg conversion returned null, keeping M4A file'); } } - } - } else { - _log.d('M4A file detected (SAF), converting to FLAC...'); - final tempPath = await _copySafToTemp(currentFilePath); - if (tempPath != null) { - String? flacPath; + } catch (e) { + _log.w('SAF M4A->FLAC conversion failed: $e'); + } finally { + // Clean up temp files try { - final length = await File(tempPath).length(); - if (length < 1024) { - _log.w('Temp M4A is too small (<1KB), skipping conversion'); - } else { - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.95, - ); - flacPath = await FFmpegService.convertM4aToFlac(tempPath); - if (flacPath != null) { - _log.d('Converted to FLAC (temp): $flacPath'); - _log.d( - 'Embedding metadata and cover to converted FLAC...', - ); + await File(tempPath).delete(); + } catch (_) {} + if (flacPath != null) { + try { + await File(flacPath).delete(); + } catch (_) {} + } + } + } + } else { + _log.d( + 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', + ); + + try { + final file = File(currentFilePath); + if (!await file.exists()) { + _log.e('File does not exist at path: $filePath'); + } else { + final length = await file.length(); + _log.i('File size before conversion: ${length / 1024} KB'); + + if (length < 1024) { + _log.w( + 'File is too small (<1KB), skipping conversion. Download might be corrupt.', + ); + } else { + updateItemStatus( + item.id, + DownloadStatus.downloading, + progress: 0.95, + ); + final flacPath = await FFmpegService.convertM4aToFlac( + currentFilePath, + ); + + if (flacPath != null) { + filePath = flacPath; + _log.d('Converted to FLAC: $flacPath'); + + _log.d('Embedding metadata and cover to converted FLAC...'); + try { final finalTrack = _buildTrackForMetadataEmbedding( trackToDownload, result, @@ -4055,201 +4039,32 @@ class DownloadQueueNotifier extends Notifier { final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; + if (backendGenre != null || + backendLabel != null || + backendCopyright != null) { + _log.d( + 'Extended metadata from backend - Genre: $backendGenre, Label: $backendLabel, Copyright: $backendCopyright', + ); + } + await _embedMetadataAndCover( flacPath, finalTrack, genre: backendGenre ?? genre, label: backendLabel ?? label, copyright: backendCopyright, - writeExternalLrc: false, - ); - - final newFileName = '${safBaseName ?? 'track'}.flac'; - final newUri = await _writeTempToSaf( - treeUri: settings.downloadTreeUri, - relativeDir: effectiveOutputDir, - fileName: newFileName, - mimeType: _mimeTypeForExt('.flac'), - srcPath: flacPath, - ); - - if (newUri != null) { - if (newUri != currentFilePath) { - await _deleteSafFile(currentFilePath); - } - filePath = newUri; - finalSafFileName = newFileName; - } else { - _log.w('Failed to write FLAC to SAF, keeping M4A'); - } - } else { - _log.w( - 'FFmpeg conversion returned null, keeping M4A file', ); + _log.d('Metadata and cover embedded successfully'); + } catch (e) { + _log.w('Warning: Failed to embed metadata/cover: $e'); } - } - } catch (e) { - _log.w('SAF M4A->FLAC conversion failed: $e'); - } finally { - // Clean up temp files - try { - await File(tempPath).delete(); - } catch (_) {} - if (flacPath != null) { - try { - await File(flacPath).delete(); - } catch (_) {} - } - } - } - } - } else { - if (quality == 'HIGH') { - final tidalHighFormat = settings.tidalHighFormat; - _log.i( - 'Tidal HIGH quality download, converting M4A to $tidalHighFormat...', - ); - - try { - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.95, - ); - - final format = tidalHighFormat.startsWith('opus') - ? 'opus' - : 'mp3'; - final convertedPath = await FFmpegService.convertM4aToLossy( - currentFilePath, - format: format, - bitrate: tidalHighFormat, - deleteOriginal: true, - ); - - if (convertedPath != null) { - filePath = convertedPath; - final bitrateDisplay = tidalHighFormat.contains('_') - ? '${tidalHighFormat.split('_').last}kbps' - : '320kbps'; - actualQuality = '${format.toUpperCase()} $bitrateDisplay'; - _log.i( - 'Successfully converted M4A to $format: $convertedPath', - ); - - _log.i('Embedding metadata to $format...'); - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.99, - ); - - final backendGenre = result['genre'] as String?; - final backendLabel = result['label'] as String?; - final backendCopyright = result['copyright'] as String?; - - if (format == 'mp3') { - await _embedMetadataToMp3( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); } else { - await _embedMetadataToOpus( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - } - _log.d('Metadata embedded successfully'); - } else { - _log.w('M4A to $format conversion failed, keeping M4A file'); - actualQuality = 'AAC 320kbps'; - } - } catch (e) { - _log.w('M4A conversion process failed: $e, keeping M4A file'); - actualQuality = 'AAC 320kbps'; - } - } else { - _log.d( - 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', - ); - - try { - final file = File(currentFilePath); - if (!await file.exists()) { - _log.e('File does not exist at path: $filePath'); - } else { - final length = await file.length(); - _log.i('File size before conversion: ${length / 1024} KB'); - - if (length < 1024) { - _log.w( - 'File is too small (<1KB), skipping conversion. Download might be corrupt.', - ); - } else { - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.95, - ); - final flacPath = await FFmpegService.convertM4aToFlac( - currentFilePath, - ); - - if (flacPath != null) { - filePath = flacPath; - _log.d('Converted to FLAC: $flacPath'); - - _log.d( - 'Embedding metadata and cover to converted FLAC...', - ); - try { - final finalTrack = _buildTrackForMetadataEmbedding( - trackToDownload, - result, - resolvedAlbumArtist, - ); - - final backendGenre = result['genre'] as String?; - final backendLabel = result['label'] as String?; - final backendCopyright = result['copyright'] as String?; - - if (backendGenre != null || - backendLabel != null || - backendCopyright != null) { - _log.d( - 'Extended metadata from backend - Genre: $backendGenre, Label: $backendLabel, Copyright: $backendCopyright', - ); - } - - await _embedMetadataAndCover( - flacPath, - finalTrack, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - _log.d('Metadata and cover embedded successfully'); - } catch (e) { - _log.w('Warning: Failed to embed metadata/cover: $e'); - } - } else { - _log.w( - 'FFmpeg conversion returned null, keeping M4A file', - ); - } + _log.w('FFmpeg conversion returned null, keeping M4A file'); } } - } catch (e) { - _log.w( - 'FFmpeg conversion process failed: $e, keeping M4A file', - ); } + } catch (e) { + _log.w('FFmpeg conversion process failed: $e, keeping M4A file'); } } } else if (metadataEmbeddingEnabled && diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index ed44fea4..e98a70e0 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -1,20 +1,22 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/logger.dart'; const _settingsKey = 'app_settings'; const _migrationVersionKey = 'settings_migration_version'; -const _currentMigrationVersion = 5; +const _currentMigrationVersion = 6; const _spotifyClientSecretKey = 'spotify_client_secret'; final _log = AppLogger('SettingsProvider'); class SettingsNotifier extends Notifier { - static const List _youtubeOpusSupportedBitrates = [128, 256]; + static const List _youtubeOpusSupportedBitrates = [128, 256, 320]; static const List _youtubeMp3SupportedBitrates = [128, 256, 320]; static final RegExp _isoRegionPattern = RegExp(r'^[A-Z]{2}$'); @@ -37,6 +39,7 @@ class SettingsNotifier extends Notifier { state = AppSettings.fromJson(jsonDecode(json)); await _runMigrations(prefs); + await _normalizeIosDownloadDirectoryIfNeeded(); await _normalizeYouTubeBitratesIfNeeded(); await _normalizeSongLinkRegionIfNeeded(); } @@ -114,6 +117,10 @@ class SettingsNotifier extends Notifier { useCustomSpotifyCredentials: false, ); } + // Migration 6: Tidal HIGH quality removed — migrate to LOSSLESS + if (state.audioQuality == 'HIGH') { + state = state.copyWith(audioQuality: 'LOSSLESS'); + } state = state.copyWith(lastSeenVersion: AppInfo.version); await prefs.setInt(_migrationVersionKey, _currentMigrationVersion); await _saveSettings(); @@ -189,6 +196,20 @@ class SettingsNotifier extends Notifier { await _saveSettings(); } + Future _normalizeIosDownloadDirectoryIfNeeded() async { + if (!Platform.isIOS) return; + + final currentDir = state.downloadDirectory.trim(); + if (currentDir.isEmpty) return; + + final normalizedDir = await validateOrFixIosPath(currentDir); + if (normalizedDir == currentDir) return; + + _log.i('Normalized iOS download directory: $currentDir -> $normalizedDir'); + state = state.copyWith(downloadDirectory: normalizedDir); + await _saveSettings(); + } + String _normalizeSongLinkRegion(String region) { final normalized = region.trim().toUpperCase(); if (_isoRegionPattern.hasMatch(normalized)) return normalized; @@ -430,11 +451,6 @@ class SettingsNotifier extends Notifier { _saveSettings(); } - void setTidalHighFormat(String format) { - state = state.copyWith(tidalHighFormat: format); - _saveSettings(); - } - void setYoutubeOpusBitrate(int bitrate) { final normalized = _normalizeYouTubeOpusBitrate(bitrate); state = state.copyWith(youtubeOpusBitrate: normalized); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index c502d142..b4a6d1a7 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -300,7 +300,6 @@ class _DownloadSettingsPageState extends ConsumerState { final topPadding = normalizedHeaderTopPadding(context); final isBuiltInService = _builtInServices.contains(settings.defaultService); - final isTidalService = settings.defaultService == 'tidal'; return PopScope( canPop: true, @@ -408,35 +407,8 @@ class _DownloadSettingsPageState extends ConsumerState { onTap: () => ref .read(settingsProvider.notifier) .setAudioQuality('HI_RES_LOSSLESS'), - showDivider: isTidalService, + showDivider: false, ), - // Lossy 320kbps option (Tidal only) - downloads M4A, converts to MP3/Opus - if (isTidalService) - _QualityOption( - title: context.l10n.downloadLossy320, - subtitle: _getTidalHighFormatLabel( - settings.tidalHighFormat, - ), - isSelected: settings.audioQuality == 'HIGH', - onTap: () => ref - .read(settingsProvider.notifier) - .setAudioQuality('HIGH'), - showDivider: false, - ), - if (isTidalService && settings.audioQuality == 'HIGH') - SettingsItem( - icon: Icons.tune, - title: context.l10n.downloadLossyFormat, - subtitle: _getTidalHighFormatLabel( - settings.tidalHighFormat, - ), - onTap: () => _showTidalHighFormatPicker( - context, - ref, - settings.tidalHighFormat, - ), - showDivider: false, - ), ], if (!isBuiltInService) ...[ Padding( @@ -464,12 +436,12 @@ class _DownloadSettingsPageState extends ConsumerState { ], SettingsItem( title: context.l10n.youtubeOpusBitrateTitle, - subtitle: '${settings.youtubeOpusBitrate}kbps (128/256)', + subtitle: '${settings.youtubeOpusBitrate}kbps (128/256/320)', onTap: () => _showYoutubeBitratePicker( context: context, title: context.l10n.youtubeOpusBitrateTitle, currentValue: settings.youtubeOpusBitrate, - options: const [128, 256], + options: const [128, 256, 320], onSave: (value) => ref .read(settingsProvider.notifier) .setYoutubeOpusBitrate(value), @@ -1691,104 +1663,6 @@ class _DownloadSettingsPageState extends ConsumerState { ); } - String _getTidalHighFormatLabel(String format) { - switch (format) { - case 'mp3_320': - return 'MP3 320kbps'; - case 'opus_256': - return 'Opus 256kbps'; - case 'opus_128': - return 'Opus 128kbps'; - default: - return 'MP3 320kbps'; - } - } - - void _showTidalHighFormatPicker( - BuildContext context, - WidgetRef ref, - String current, - ) { - final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, - useRootNavigator: true, - backgroundColor: colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - builder: (context) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - context.l10n.downloadLossy320Format, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - context.l10n.downloadLossy320FormatDesc, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ListTile( - leading: const Icon(Icons.audiotrack), - title: Text(context.l10n.downloadLossyMp3), - subtitle: Text(context.l10n.downloadLossyMp3Subtitle), - trailing: current == 'mp3_320' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('mp3_320'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.graphic_eq), - title: Text(context.l10n.downloadLossyOpus256), - subtitle: Text(context.l10n.downloadLossyOpus256Subtitle), - trailing: current == 'opus_256' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('opus_256'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.graphic_eq), - title: Text(context.l10n.downloadLossyOpus128), - subtitle: Text(context.l10n.downloadLossyOpus128Subtitle), - trailing: current == 'opus_128' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('opus_128'); - Navigator.pop(context); - }, - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } - void _showNetworkModePicker( BuildContext context, WidgetRef ref, diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index 41c8c330..977a31b6 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -23,7 +23,7 @@ class BuiltInService { } /// Default quality options for built-in services -/// Note: Tidal lossy (HIGH) removed - use YouTube for lossy downloads +/// Default quality options for each built-in service const _builtInServices = [ BuiltInService( id: 'tidal', @@ -83,9 +83,9 @@ const _builtInServices = [ label: 'YouTube', qualityOptions: [ QualityOption( - id: 'opus_256', - label: 'Opus 256kbps', - description: 'Best quality lossy (~8MB per track)', + id: 'opus_320', + label: 'Opus 320kbps', + description: 'Best quality lossy (~10MB per track)', ), QualityOption( id: 'mp3_320', @@ -146,7 +146,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget { } class _DownloadServicePickerState extends ConsumerState { - static const List _youtubeOpusSupportedBitrates = [128, 256]; + static const List _youtubeOpusSupportedBitrates = [128, 256, 320]; static const List _youtubeMp3SupportedBitrates = [128, 256, 320]; late String _selectedService; From 82f59d32b9df0f203cf69f5f14e40eed9948ae75 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:18:29 +0700 Subject: [PATCH 063/145] fix: handle nested legacy iOS Documents path in validation Detect and recover from stale sandbox container paths embedded inside the current Documents directory. Extracts helper functions for path suffix normalization and joining to reduce duplication. --- lib/utils/file_access.dart | 50 +++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/lib/utils/file_access.dart b/lib/utils/file_access.dart index 026fa57e..f686bc46 100644 --- a/lib/utils/file_access.dart +++ b/lib/utils/file_access.dart @@ -20,6 +20,22 @@ final _iosLegacyRelativeDocumentsPattern = RegExp( r'^Data/Application/[A-F0-9\-]+/Documents(?:/(.*))?$', caseSensitive: false, ); +final _iosNestedLegacyDocumentsPattern = RegExp( + r'/Documents/Data/Application/[A-F0-9\-]+/Documents(?:/(.*))?$', + caseSensitive: false, +); + +String _normalizeRecoveredIosSuffix(String suffix) { + final trimmed = suffix.trim(); + if (trimmed.isEmpty) return ''; + return trimmed.startsWith('/') ? trimmed.substring(1) : trimmed; +} + +String _joinRecoveredIosPath(String documentsPath, String suffix) { + final normalizedSuffix = _normalizeRecoveredIosSuffix(suffix); + if (normalizedSuffix.isEmpty) return documentsPath; + return '$documentsPath/$normalizedSuffix'; +} /// Checks if a path is a valid writable directory on iOS. /// Returns false if: @@ -43,6 +59,12 @@ bool isValidIosWritablePath(String path) { return false; } + // Reject stale paths where an old sandbox container path has been embedded + // inside the current Documents directory. + if (_iosNestedLegacyDocumentsPattern.hasMatch(path)) { + return false; + } + // Ensure path contains a valid subdirectory (Documents, tmp, Library, etc.) // This handles cases where FilePicker returns container root final containerPattern = RegExp( @@ -70,11 +92,19 @@ Future validateOrFixIosPath( if (!Platform.isIOS) return path; final trimmed = path.trim(); + final docDir = await getApplicationDocumentsDirectory(); + + final nestedLegacyMatch = _iosNestedLegacyDocumentsPattern.firstMatch( + trimmed, + ); + if (nestedLegacyMatch != null) { + return _joinRecoveredIosPath(docDir.path, nestedLegacyMatch.group(1) ?? ''); + } + if (isValidIosWritablePath(trimmed)) { return trimmed; } - final docDir = await getApplicationDocumentsDirectory(); final candidates = []; if (trimmed.isNotEmpty) { @@ -92,14 +122,8 @@ Future validateOrFixIosPath( trimmed, ); if (legacyRelativeMatch != null) { - final suffix = (legacyRelativeMatch.group(1) ?? '').trim(); - final normalizedSuffix = suffix.startsWith('/') - ? suffix.substring(1) - : suffix; candidates.add( - normalizedSuffix.isEmpty - ? docDir.path - : '${docDir.path}/$normalizedSuffix', + _joinRecoveredIosPath(docDir.path, legacyRelativeMatch.group(1) ?? ''), ); } @@ -109,7 +133,7 @@ Future validateOrFixIosPath( final index = trimmed.indexOf(documentsMarker); if (index >= 0) { final suffix = trimmed.substring(index + documentsMarker.length).trim(); - candidates.add(suffix.isEmpty ? docDir.path : '${docDir.path}/$suffix'); + candidates.add(_joinRecoveredIosPath(docDir.path, suffix)); } } @@ -181,6 +205,14 @@ IosPathValidationResult validateIosPath(String path) { ); } + if (_iosNestedLegacyDocumentsPattern.hasMatch(path)) { + return const IosPathValidationResult( + isValid: false, + errorReason: + 'Invalid iOS app folder path. Please choose App Documents or another local folder.', + ); + } + // Check for container root without subdirectory final containerPattern = RegExp( r'/var/mobile/Containers/Data/Application/[A-F0-9\-]+', From 42f0267277a62d20bb942e9ecafd24136ec1358e Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:18:58 +0700 Subject: [PATCH 064/145] feat: queue FLAC redownloads for local library tracks Add LocalTrackRedownloadService with confidence-scored metadata matching (ISRC, title, artist, album, duration, track/disc number, year) to find reliable online matches for locally-stored tracks. Wire up 'Queue FLAC' selection action in both local_album_screen and queue_tab (library tab). Shows progress snackbar during resolution, skips ambiguous or low-confidence matches, and reports results. Add Indonesian (id) translations for all queueFlac l10n keys. --- lib/l10n/arb/app_id.arb | 43 ++- lib/screens/local_album_screen.dart | 141 +++++++- lib/screens/queue_tab.dart | 156 +++++++- .../local_track_redownload_service.dart | 338 ++++++++++++++++++ 4 files changed, 667 insertions(+), 11 deletions(-) create mode 100644 lib/services/local_track_redownload_service.dart diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index de9d90d0..bab66bf2 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -2755,6 +2755,47 @@ "@trackReEnrichFfmpegFailed": { "description": "Snackbar when FFmpeg embed fails for MP3/Opus" }, + "queueFlacAction": "Antrekan FLAC", + "@queueFlacAction": { + "description": "Action/button label for queueing FLAC redownloads for local tracks" + }, + "queueFlacConfirmMessage": "Cari kecocokan online untuk track yang dipilih lalu antrekan download FLAC.\n\nFile yang sudah ada tidak akan diubah atau dihapus.\n\nHanya kecocokan dengan keyakinan tinggi yang akan diantrikan otomatis.\n\n{count} dipilih", + "@queueFlacConfirmMessage": { + "description": "Confirmation dialog body before queueing FLAC redownloads for local tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueFlacFindingProgress": "Mencari kecocokan FLAC... ({current}/{total})", + "@queueFlacFindingProgress": { + "description": "Snackbar while resolving remote matches for local FLAC redownloads", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "queueFlacNoReliableMatches": "Tidak ada kecocokan online yang cukup meyakinkan untuk pilihan ini", + "@queueFlacNoReliableMatches": { + "description": "Snackbar when no safe FLAC redownload matches were found" + }, + "queueFlacQueuedWithSkipped": "Menambahkan {addedCount} track ke antrean, melewati {skippedCount}", + "@queueFlacQueuedWithSkipped": { + "description": "Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped", + "placeholders": { + "addedCount": { + "type": "int" + }, + "skippedCount": { + "type": "int" + } + } + }, "trackSaveFailed": "Failed: {error}", "@trackSaveFailed": { "description": "Snackbar when save operation fails", @@ -3114,4 +3155,4 @@ "@downloadUseAlbumArtistForFoldersTrackSubtitle": { "description": "Subtitle when Track Artist is used for folder naming" } -} \ No newline at end of file +} diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index e5584b34..122a4bab 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -4,11 +4,15 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/models/track.dart'; +import 'package:spotiflac_android/providers/download_queue_provider.dart'; +import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart'; import 'package:spotiflac_android/services/library_database.dart'; import 'package:spotiflac_android/services/ffmpeg_service.dart'; +import 'package:spotiflac_android/services/local_track_redownload_service.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/playback_provider.dart'; @@ -41,11 +45,10 @@ class _LocalAlbumScreenState extends ConsumerState { void _showCueVirtualTrackSnackBar() { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text(cueVirtualTrackRequiresSplitMessage), - ), + const SnackBar(content: Text(cueVirtualTrackRequiresSplitMessage)), ); } + late List _sortedDiscNumbersCache; late bool _hasMultipleDiscsCache; String? _commonQualityCache; @@ -897,6 +900,127 @@ class _LocalAlbumScreenState extends ConsumerState { return false; } + Future _queueSelectedAsFlac(List allTracks) async { + final tracksById = {for (final t in allTracks) t.id: t}; + final selected = []; + + for (final id in _selectedIds) { + final item = tracksById[id]; + if (item != null) { + selected.add(item); + } + } + + if (selected.isEmpty) { + return; + } + + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(context.l10n.queueFlacAction), + content: Text(context.l10n.queueFlacConfirmMessage(selected.length)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text(context.l10n.dialogCancel), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: Text(context.l10n.queueFlacAction), + ), + ], + ), + ); + + if (confirmed != true || !mounted) { + return; + } + + final settings = ref.read(settingsProvider); + final extensionState = ref.read(extensionProvider); + final includeExtensions = + settings.useExtensionProviders && + extensionState.extensions.any( + (ext) => ext.enabled && ext.hasMetadataProvider, + ); + final targetService = LocalTrackRedownloadService.preferredFlacService( + settings, + ); + final targetQuality = + LocalTrackRedownloadService.preferredFlacQualityForService( + targetService, + ); + + final matchedTracks = []; + var skippedCount = 0; + final total = selected.length; + + for (var i = 0; i < total; i++) { + if (!mounted) break; + + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.queueFlacFindingProgress(i + 1, total), + ), + duration: const Duration(seconds: 30), + ), + ); + + try { + final resolution = await LocalTrackRedownloadService.resolveBestMatch( + selected[i], + includeExtensions: includeExtensions, + ); + if (resolution.canQueue && resolution.match != null) { + matchedTracks.add(resolution.match!); + } else { + skippedCount++; + } + } catch (_) { + skippedCount++; + } + } + + if (!mounted) { + return; + } + + ScaffoldMessenger.of(context).clearSnackBars(); + + if (matchedTracks.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.queueFlacNoReliableMatches)), + ); + return; + } + + ref + .read(downloadQueueProvider.notifier) + .addMultipleToQueue( + matchedTracks, + targetService, + qualityOverride: targetQuality, + ); + + final summary = skippedCount == 0 + ? context.l10n.snackbarAddedTracksToQueue(matchedTracks.length) + : context.l10n.queueFlacQueuedWithSkipped( + matchedTracks.length, + skippedCount, + ); + + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(summary))); + setState(() { + _selectedIds.clear(); + _isSelectionMode = false; + }); + } + Future _reEnrichSelected(List allTracks) async { final tracksById = {for (final t in allTracks) t.id: t}; final selected = []; @@ -1525,6 +1649,17 @@ class _LocalAlbumScreenState extends ConsumerState { Row( children: [ + Expanded( + child: _LocalAlbumSelectionActionButton( + icon: Icons.download_for_offline_outlined, + label: '${context.l10n.queueFlacAction} ($selectedCount)', + onPressed: selectedCount > 0 + ? () => _queueSelectedAsFlac(tracks) + : null, + colorScheme: colorScheme, + ), + ), + const SizedBox(width: 8), Expanded( child: _LocalAlbumSelectionActionButton( icon: Icons.auto_fix_high_outlined, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 5b14ff21..52ca3309 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -17,11 +17,13 @@ import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; +import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/playback_provider.dart'; import 'package:spotiflac_android/services/library_database.dart'; +import 'package:spotiflac_android/services/local_track_redownload_service.dart'; import 'package:spotiflac_android/services/history_database.dart'; import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; @@ -1321,8 +1323,10 @@ class _QueueTabState extends ConsumerState { .where((p) => _selectedPlaylistIds.contains(p.id)) .toList(); - final totalTracks = - selectedPlaylists.fold(0, (sum, p) => sum + p.tracks.length); + final totalTracks = selectedPlaylists.fold( + 0, + (sum, p) => sum + p.tracks.length, + ); if (totalTracks == 0) { ScaffoldMessenger.of(context).showSnackBar( @@ -1336,7 +1340,10 @@ class _QueueTabState extends ConsumerState { builder: (ctx) => AlertDialog( title: Text(ctx.l10n.dialogDownloadAllTitle), content: Text( - ctx.l10n.dialogDownloadPlaylistsMessage(totalTracks, selectedPlaylists.length), + ctx.l10n.dialogDownloadPlaylistsMessage( + totalTracks, + selectedPlaylists.length, + ), ), actions: [ TextButton( @@ -1392,9 +1399,7 @@ class _QueueTabState extends ConsumerState { _exitPlaylistSelectionMode(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.snackbarAddedTracksToQueue(totalTracks), - ), + content: Text(context.l10n.snackbarAddedTracksToQueue(totalTracks)), ), ); } @@ -1547,7 +1552,9 @@ class _QueueTabState extends ConsumerState { icon: const Icon(Icons.download_rounded), label: Text( selectedCount > 0 - ? context.l10n.bulkDownloadPlaylistsButton(selectedCount) + ? context.l10n.bulkDownloadPlaylistsButton( + selectedCount, + ) : context.l10n.bulkDownloadSelectPlaylists, ), style: FilledButton.styleFrom( @@ -4477,6 +4484,127 @@ class _QueueTabState extends ConsumerState { return false; } + Future _queueSelectedLocalAsFlac( + List allItems, + ) async { + final selectedItems = _selectedItemsFromAll(allItems); + final selectedLocalItems = selectedItems + .map((item) => item.localItem) + .whereType() + .toList(growable: false); + + if (selectedLocalItems.isEmpty) { + return; + } + + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(context.l10n.queueFlacAction), + content: Text( + context.l10n.queueFlacConfirmMessage(selectedLocalItems.length), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text(context.l10n.dialogCancel), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: Text(context.l10n.queueFlacAction), + ), + ], + ), + ); + + if (confirmed != true || !mounted) { + return; + } + + final settings = ref.read(settingsProvider); + final extensionState = ref.read(extensionProvider); + final includeExtensions = + settings.useExtensionProviders && + extensionState.extensions.any( + (ext) => ext.enabled && ext.hasMetadataProvider, + ); + final targetService = LocalTrackRedownloadService.preferredFlacService( + settings, + ); + final targetQuality = + LocalTrackRedownloadService.preferredFlacQualityForService( + targetService, + ); + + final matchedTracks = []; + var skippedCount = 0; + final total = selectedLocalItems.length; + + for (var i = 0; i < total; i++) { + if (!mounted) break; + + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.queueFlacFindingProgress(i + 1, total), + ), + duration: const Duration(seconds: 30), + ), + ); + + try { + final resolution = await LocalTrackRedownloadService.resolveBestMatch( + selectedLocalItems[i], + includeExtensions: includeExtensions, + ); + if (resolution.canQueue && resolution.match != null) { + matchedTracks.add(resolution.match!); + } else { + skippedCount++; + } + } catch (_) { + skippedCount++; + } + } + + if (!mounted) { + return; + } + + ScaffoldMessenger.of(context).clearSnackBars(); + + if (matchedTracks.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.queueFlacNoReliableMatches)), + ); + return; + } + + ref + .read(downloadQueueProvider.notifier) + .addMultipleToQueue( + matchedTracks, + targetService, + qualityOverride: targetQuality, + ); + + final summary = skippedCount == 0 + ? context.l10n.snackbarAddedTracksToQueue(matchedTracks.length) + : context.l10n.queueFlacQueuedWithSkipped( + matchedTracks.length, + skippedCount, + ); + + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(summary))); + setState(() { + _selectedIds.clear(); + _isSelectionMode = false; + }); + } + Future _reEnrichSelectedLocalFromQueue( List allItems, ) async { @@ -5244,6 +5372,20 @@ class _QueueTabState extends ConsumerState { // Action buttons row: Share/Re-enrich, Convert, Delete Row( children: [ + if (localOnlySelection) ...[ + Expanded( + child: _SelectionActionButton( + icon: Icons.download_for_offline_outlined, + label: + '${context.l10n.queueFlacAction} ($selectedCount)', + onPressed: selectedCount > 0 + ? () => _queueSelectedLocalAsFlac(unifiedItems) + : null, + colorScheme: colorScheme, + ), + ), + const SizedBox(width: 8), + ], Expanded( child: _SelectionActionButton( icon: localOnlySelection diff --git a/lib/services/local_track_redownload_service.dart b/lib/services/local_track_redownload_service.dart new file mode 100644 index 00000000..4b8500c7 --- /dev/null +++ b/lib/services/local_track_redownload_service.dart @@ -0,0 +1,338 @@ +import 'package:spotiflac_android/models/settings.dart'; +import 'package:spotiflac_android/models/track.dart'; +import 'package:spotiflac_android/services/library_database.dart'; +import 'package:spotiflac_android/services/platform_bridge.dart'; + +class LocalTrackRedownloadResolution { + final LocalLibraryItem localItem; + final Track? match; + final int score; + final String reason; + + const LocalTrackRedownloadResolution({ + required this.localItem, + required this.match, + required this.score, + required this.reason, + }); + + bool get canQueue => match != null; +} + +class LocalTrackRedownloadService { + static const int _minimumConfidenceScore = 85; + static const int _ambiguousScoreGap = 8; + + static Future resolveBestMatch( + LocalLibraryItem item, { + required bool includeExtensions, + }) async { + final query = _buildSearchQuery(item); + final rawResults = await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 10, + includeExtensions: includeExtensions, + ); + + if (rawResults.isEmpty) { + return LocalTrackRedownloadResolution( + localItem: item, + match: null, + score: 0, + reason: 'No candidates found', + ); + } + + final scored = + rawResults + .map( + (raw) => ( + track: _parseSearchTrack(raw), + score: _scoreMatch(item, raw), + ), + ) + .where((entry) => entry.track.name.trim().isNotEmpty) + .toList(growable: false) + ..sort((a, b) => b.score.compareTo(a.score)); + + if (scored.isEmpty) { + return LocalTrackRedownloadResolution( + localItem: item, + match: null, + score: 0, + reason: 'No usable candidates found', + ); + } + + final best = scored.first; + final runnerUp = scored.length > 1 ? scored[1] : null; + final exactIsrc = + _normalizedIsrc(item.isrc) != null && + _normalizedIsrc(item.isrc) == _normalizedIsrc(best.track.isrc); + final isAmbiguous = + !exactIsrc && + runnerUp != null && + best.score < (_minimumConfidenceScore + 10) && + (best.score - runnerUp.score) <= _ambiguousScoreGap; + + if (!exactIsrc && (best.score < _minimumConfidenceScore || isAmbiguous)) { + return LocalTrackRedownloadResolution( + localItem: item, + match: null, + score: best.score, + reason: isAmbiguous ? 'Ambiguous match' : 'Low-confidence match', + ); + } + + return LocalTrackRedownloadResolution( + localItem: item, + match: best.track, + score: best.score, + reason: exactIsrc ? 'Exact ISRC match' : 'High-confidence metadata match', + ); + } + + static String preferredFlacService(AppSettings settings) { + switch (settings.defaultService.toLowerCase()) { + case 'tidal': + case 'qobuz': + case 'deezer': + return settings.defaultService.toLowerCase(); + default: + return 'tidal'; + } + } + + static String preferredFlacQualityForService(String service) { + return service.toLowerCase() == 'deezer' ? 'FLAC' : 'LOSSLESS'; + } + + static String _buildSearchQuery(LocalLibraryItem item) { + final artist = _primaryArtist(item.artistName); + final album = item.albumName.trim(); + if (album.isNotEmpty && album.toLowerCase() != 'unknown album') { + return '${item.trackName} $artist $album'.trim(); + } + return '${item.trackName} $artist'.trim(); + } + + static Track _parseSearchTrack(Map data) { + final durationMs = _extractDurationMs(data); + final itemType = data['item_type']?.toString(); + + return Track( + id: (data['spotify_id'] ?? data['id'] ?? '').toString(), + name: (data['name'] ?? '').toString(), + artistName: (data['artists'] ?? data['artist'] ?? '').toString(), + albumName: (data['album_name'] ?? data['album'] ?? '').toString(), + albumArtist: data['album_artist']?.toString(), + artistId: (data['artist_id'] ?? data['artistId'])?.toString(), + albumId: data['album_id']?.toString(), + coverUrl: (data['cover_url'] ?? data['images'])?.toString(), + isrc: data['isrc']?.toString(), + duration: (durationMs / 1000).round(), + trackNumber: data['track_number'] as int?, + discNumber: data['disc_number'] as int?, + releaseDate: data['release_date']?.toString(), + totalTracks: data['total_tracks'] as int?, + source: data['source']?.toString() ?? data['provider_id']?.toString(), + albumType: data['album_type']?.toString(), + itemType: itemType, + ); + } + + static int _extractDurationMs(Map data) { + final durationMsRaw = data['duration_ms']; + if (durationMsRaw is num && durationMsRaw > 0) { + return durationMsRaw.toInt(); + } + if (durationMsRaw is String) { + final parsed = num.tryParse(durationMsRaw.trim()); + if (parsed != null && parsed > 0) { + return parsed.toInt(); + } + } + + final durationSecRaw = data['duration']; + if (durationSecRaw is num && durationSecRaw > 0) { + return (durationSecRaw * 1000).toInt(); + } + if (durationSecRaw is String) { + final parsed = num.tryParse(durationSecRaw.trim()); + if (parsed != null && parsed > 0) { + return (parsed * 1000).toInt(); + } + } + + return 0; + } + + static int _scoreMatch(LocalLibraryItem item, Map raw) { + final track = _parseSearchTrack(raw); + var score = 0; + + final localIsrc = _normalizedIsrc(item.isrc); + final candidateIsrc = _normalizedIsrc(track.isrc); + if (localIsrc != null && candidateIsrc != null) { + score += localIsrc == candidateIsrc ? 140 : -120; + } + + final localTitle = _normalizedTitle(item.trackName); + final candidateTitle = _normalizedTitle(track.name); + if (localTitle == candidateTitle) { + score += 45; + } else if (_tokenOverlap(localTitle, candidateTitle) >= 0.75) { + score += 24; + } else { + score -= 25; + } + + final localArtist = _normalizedArtistGroup(item.artistName); + final candidateArtist = _normalizedArtistGroup(track.artistName); + final artistOverlap = _tokenOverlap(localArtist, candidateArtist); + if (localArtist == candidateArtist) { + score += 30; + } else if (artistOverlap >= 0.6) { + score += 16; + } else { + score -= 20; + } + + final localAlbum = _normalizedText(item.albumName); + final candidateAlbum = _normalizedText(track.albumName); + if (localAlbum.isNotEmpty && candidateAlbum.isNotEmpty) { + if (localAlbum == candidateAlbum) { + score += 12; + } else if (_tokenOverlap(localAlbum, candidateAlbum) >= 0.7) { + score += 6; + } + } + + final localDuration = item.duration ?? 0; + final candidateDuration = track.duration; + if (localDuration > 0 && candidateDuration > 0) { + final diff = (localDuration - candidateDuration).abs(); + if (diff <= 2) { + score += 20; + } else if (diff <= 5) { + score += 12; + } else if (diff <= 10) { + score += 5; + } else if (diff > 20) { + score -= 30; + } + } + + if (item.trackNumber != null && + track.trackNumber != null && + item.trackNumber == track.trackNumber) { + score += 6; + } + if (item.discNumber != null && + track.discNumber != null && + item.discNumber == track.discNumber) { + score += 4; + } + + final localYear = _extractYear(item.releaseDate); + final candidateYear = _extractYear(track.releaseDate); + if (localYear != null && + candidateYear != null && + localYear == candidateYear) { + score += 4; + } + + score += _versionPenalty(item.trackName, track.name); + return score; + } + + static String? _normalizedIsrc(String? value) { + final normalized = value?.trim().toUpperCase(); + if (normalized == null || normalized.isEmpty) { + return null; + } + return normalized; + } + + static String _normalizedTitle(String value) { + final cleaned = _normalizedText(value) + .replaceAll(RegExp(r'\b(feat|ft|featuring)\b.*$'), ' ') + .replaceAll(RegExp(r'\b(remaster(?:ed)?|deluxe|bonus)\b'), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + return cleaned; + } + + static String _normalizedArtistGroup(String value) { + return _normalizedText( + value + .replaceAll(RegExp(r'\b(feat|ft|featuring|with|x)\b'), ',') + .replaceAll('&', ','), + ); + } + + static String _primaryArtist(String value) { + final parts = _normalizedArtistGroup( + value, + ).split(',').map((part) => part.trim()).where((part) => part.isNotEmpty); + return parts.isEmpty ? value.trim() : parts.first; + } + + static String _normalizedText(String value) { + return value + .toLowerCase() + .replaceAll(RegExp(r'[\(\)\[\]\{\}]'), ' ') + .replaceAll(RegExp(r'[^a-z0-9, ]+'), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + } + + static double _tokenOverlap(String left, String right) { + final leftTokens = left + .split(RegExp(r'[\s,]+')) + .where((token) => token.isNotEmpty) + .toSet(); + final rightTokens = right + .split(RegExp(r'[\s,]+')) + .where((token) => token.isNotEmpty) + .toSet(); + if (leftTokens.isEmpty || rightTokens.isEmpty) { + return 0; + } + final intersection = leftTokens.intersection(rightTokens).length; + final denominator = leftTokens.length > rightTokens.length + ? leftTokens.length + : rightTokens.length; + return intersection / denominator; + } + + static int _versionPenalty(String localTitle, String candidateTitle) { + const riskyMarkers = [ + 'live', + 'karaoke', + 'instrumental', + 'acoustic', + 'radio edit', + 'sped up', + 'slowed', + ]; + final local = _normalizedText(localTitle); + final candidate = _normalizedText(candidateTitle); + var penalty = 0; + for (final marker in riskyMarkers) { + final localHas = local.contains(marker); + final candidateHas = candidate.contains(marker); + if (!localHas && candidateHas) { + penalty -= 18; + } + } + return penalty; + } + + static int? _extractYear(String? date) { + if (date == null || date.length < 4) { + return null; + } + return int.tryParse(date.substring(0, 4)); + } +} From 1665e4cd576d06e7be04ea13c264eacbd81e3bc3 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:35:42 +0700 Subject: [PATCH 065/145] feat: selective auto-fill from online in Edit Metadata sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 'Auto-fill from online' expandable section to the metadata editor that lets users choose exactly which fields to populate from online metadata search. Users can select individual fields via filter chips, use 'All' or 'Empty only' quick-select buttons, then tap 'Fetch & Fill' to search metadata providers and fill only the selected controllers. The search uses existing searchTracksWithMetadataProviders API with ISRC-preferring best-match selection. Extended metadata (genre, label, copyright) is fetched via Deezer extended metadata API when available. Cover art is downloaded from the match's cover_url. All results are previewed in the editor before saving — nothing is written to the file until the user taps Save. Add 21 new l10n keys (editMetadata* namespace) for all UI strings. --- lib/l10n/app_localizations.dart | 126 +++++++ lib/l10n/app_localizations_de.dart | 74 ++++ lib/l10n/app_localizations_en.dart | 74 ++++ lib/l10n/app_localizations_es.dart | 74 ++++ lib/l10n/app_localizations_fr.dart | 74 ++++ lib/l10n/app_localizations_hi.dart | 74 ++++ lib/l10n/app_localizations_id.dart | 74 ++++ lib/l10n/app_localizations_ja.dart | 74 ++++ lib/l10n/app_localizations_ko.dart | 74 ++++ lib/l10n/app_localizations_nl.dart | 74 ++++ lib/l10n/app_localizations_pt.dart | 74 ++++ lib/l10n/app_localizations_ru.dart | 74 ++++ lib/l10n/app_localizations_tr.dart | 74 ++++ lib/l10n/app_localizations_zh.dart | 74 ++++ lib/l10n/arb/app_en.arb | 89 +++++ lib/screens/track_metadata_screen.dart | 479 +++++++++++++++++++++++++ 16 files changed, 1656 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ae9a4cc0..01af801f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4817,6 +4817,132 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'{count, plural, =1{1 playlist} other{{count} playlists}}'** String playlistsCount(int count); + + /// Section title for selective online metadata auto-fill in the edit metadata sheet + /// + /// In en, this message translates to: + /// **'Auto-fill from online'** + String get editMetadataAutoFill; + + /// Description for the auto-fill section + /// + /// In en, this message translates to: + /// **'Select fields to fill automatically from online metadata'** + String get editMetadataAutoFillDesc; + + /// Button label to fetch online metadata and fill selected fields + /// + /// In en, this message translates to: + /// **'Fetch & Fill'** + String get editMetadataAutoFillFetch; + + /// Snackbar shown while searching for online metadata + /// + /// In en, this message translates to: + /// **'Searching online...'** + String get editMetadataAutoFillSearching; + + /// Snackbar when online metadata search returns no results + /// + /// In en, this message translates to: + /// **'No matching metadata found online'** + String get editMetadataAutoFillNoResults; + + /// Snackbar confirming how many fields were auto-filled + /// + /// In en, this message translates to: + /// **'Filled {count} {count, plural, =1{field} other{fields}} from online metadata'** + String editMetadataAutoFillDone(int count); + + /// Snackbar when user taps Fetch without selecting any fields + /// + /// In en, this message translates to: + /// **'Select at least one field to auto-fill'** + String get editMetadataAutoFillNoneSelected; + + /// Chip label for title field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Title'** + String get editMetadataFieldTitle; + + /// Chip label for artist field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Artist'** + String get editMetadataFieldArtist; + + /// Chip label for album field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Album'** + String get editMetadataFieldAlbum; + + /// Chip label for album artist field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Album Artist'** + String get editMetadataFieldAlbumArtist; + + /// Chip label for date field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Date'** + String get editMetadataFieldDate; + + /// Chip label for track number field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Track #'** + String get editMetadataFieldTrackNum; + + /// Chip label for disc number field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Disc #'** + String get editMetadataFieldDiscNum; + + /// Chip label for genre field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Genre'** + String get editMetadataFieldGenre; + + /// Chip label for ISRC field in auto-fill selector + /// + /// In en, this message translates to: + /// **'ISRC'** + String get editMetadataFieldIsrc; + + /// Chip label for label field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Label'** + String get editMetadataFieldLabel; + + /// Chip label for copyright field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Copyright'** + String get editMetadataFieldCopyright; + + /// Chip label for cover art field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Cover Art'** + String get editMetadataFieldCover; + + /// Button to select all fields for auto-fill + /// + /// In en, this message translates to: + /// **'All'** + String get editMetadataSelectAll; + + /// Button to select only fields that are currently empty + /// + /// In en, this message translates to: + /// **'Empty only'** + String get editMetadataSelectEmpty; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ce489e3d..c3ee5083 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2826,4 +2826,78 @@ class AppLocalizationsDe extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index bd280e94..180810f3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2798,4 +2798,78 @@ class AppLocalizationsEn extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 81ce15ff..cea3915b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2798,6 +2798,80 @@ class AppLocalizationsEs extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } /// The translations for Spanish Castilian, as used in Spain (`es_ES`). diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index b35b22d0..8270c7c2 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2800,4 +2800,78 @@ class AppLocalizationsFr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index e36316e9..1767c438 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2798,4 +2798,78 @@ class AppLocalizationsHi extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 68bffbe5..67a9c2fc 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2805,4 +2805,78 @@ class AppLocalizationsId extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 8145c794..e406ba35 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2785,4 +2785,78 @@ class AppLocalizationsJa extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 66d21aec..b8085a70 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2778,4 +2778,78 @@ class AppLocalizationsKo extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e87906ac..c54e044a 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2798,4 +2798,78 @@ class AppLocalizationsNl extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 466572ce..ac0f577d 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2798,6 +2798,80 @@ class AppLocalizationsPt extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } /// The translations for Portuguese, as used in Portugal (`pt_PT`). diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index dd460698..76814d38 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2857,4 +2857,78 @@ class AppLocalizationsRu extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 6e1a37f1..f24d9f88 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2810,4 +2810,78 @@ class AppLocalizationsTr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 9248d997..d9440cff 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2798,6 +2798,80 @@ class AppLocalizationsZh extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 3de4ec31..6da406d0 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -3706,5 +3706,94 @@ "type": "int" } } + }, + "editMetadataAutoFill": "Auto-fill from online", + "@editMetadataAutoFill": { + "description": "Section title for selective online metadata auto-fill in the edit metadata sheet" + }, + "editMetadataAutoFillDesc": "Select fields to fill automatically from online metadata", + "@editMetadataAutoFillDesc": { + "description": "Description for the auto-fill section" + }, + "editMetadataAutoFillFetch": "Fetch & Fill", + "@editMetadataAutoFillFetch": { + "description": "Button label to fetch online metadata and fill selected fields" + }, + "editMetadataAutoFillSearching": "Searching online...", + "@editMetadataAutoFillSearching": { + "description": "Snackbar shown while searching for online metadata" + }, + "editMetadataAutoFillNoResults": "No matching metadata found online", + "@editMetadataAutoFillNoResults": { + "description": "Snackbar when online metadata search returns no results" + }, + "editMetadataAutoFillDone": "Filled {count} {count, plural, =1{field} other{fields}} from online metadata", + "@editMetadataAutoFillDone": { + "description": "Snackbar confirming how many fields were auto-filled", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "editMetadataAutoFillNoneSelected": "Select at least one field to auto-fill", + "@editMetadataAutoFillNoneSelected": { + "description": "Snackbar when user taps Fetch without selecting any fields" + }, + "editMetadataFieldTitle": "Title", + "@editMetadataFieldTitle": { + "description": "Chip label for title field in auto-fill selector" + }, + "editMetadataFieldArtist": "Artist", + "@editMetadataFieldArtist": { + "description": "Chip label for artist field in auto-fill selector" + }, + "editMetadataFieldAlbum": "Album", + "@editMetadataFieldAlbum": { + "description": "Chip label for album field in auto-fill selector" + }, + "editMetadataFieldAlbumArtist": "Album Artist", + "@editMetadataFieldAlbumArtist": { + "description": "Chip label for album artist field in auto-fill selector" + }, + "editMetadataFieldDate": "Date", + "@editMetadataFieldDate": { + "description": "Chip label for date field in auto-fill selector" + }, + "editMetadataFieldTrackNum": "Track #", + "@editMetadataFieldTrackNum": { + "description": "Chip label for track number field in auto-fill selector" + }, + "editMetadataFieldDiscNum": "Disc #", + "@editMetadataFieldDiscNum": { + "description": "Chip label for disc number field in auto-fill selector" + }, + "editMetadataFieldGenre": "Genre", + "@editMetadataFieldGenre": { + "description": "Chip label for genre field in auto-fill selector" + }, + "editMetadataFieldIsrc": "ISRC", + "@editMetadataFieldIsrc": { + "description": "Chip label for ISRC field in auto-fill selector" + }, + "editMetadataFieldLabel": "Label", + "@editMetadataFieldLabel": { + "description": "Chip label for label field in auto-fill selector" + }, + "editMetadataFieldCopyright": "Copyright", + "@editMetadataFieldCopyright": { + "description": "Chip label for copyright field in auto-fill selector" + }, + "editMetadataFieldCover": "Cover Art", + "@editMetadataFieldCover": { + "description": "Chip label for cover art field in auto-fill selector" + }, + "editMetadataSelectAll": "All", + "@editMetadataSelectAll": { + "description": "Button to select all fields for auto-fill" + }, + "editMetadataSelectEmpty": "Empty only", + "@editMetadataSelectEmpty": { + "description": "Button to select only fields that are currently empty" } } diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index cd37ee6a..989b6a47 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -3932,6 +3932,8 @@ class _EditMetadataSheet extends StatefulWidget { class _EditMetadataSheetState extends State<_EditMetadataSheet> { bool _saving = false; bool _showAdvanced = false; + bool _showAutoFill = false; + bool _fetching = false; String? _selectedCoverPath; String? _selectedCoverTempDir; String? _selectedCoverName; @@ -3939,6 +3941,25 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { String? _currentCoverTempDir; bool _loadingCurrentCover = false; + // Auto-fill field selection — which fields the user wants to fetch + final Set _autoFillFields = {}; + + // All auto-fillable fields and their mapping + static const _fieldDefs = { + 'title': 'title', + 'artist': 'artist', + 'album': 'album', + 'album_artist': 'album_artist', + 'date': 'date', + 'track_number': 'track_number', + 'disc_number': 'disc_number', + 'genre': 'genre', + 'isrc': 'isrc', + 'label': 'label', + 'copyright': 'copyright', + 'cover': 'cover', + }; + late final TextEditingController _titleCtrl; late final TextEditingController _artistCtrl; late final TextEditingController _albumCtrl; @@ -4132,6 +4153,286 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } } + String _fieldLabel(String key) { + final l10n = context.l10n; + switch (key) { + case 'title': + return l10n.editMetadataFieldTitle; + case 'artist': + return l10n.editMetadataFieldArtist; + case 'album': + return l10n.editMetadataFieldAlbum; + case 'album_artist': + return l10n.editMetadataFieldAlbumArtist; + case 'date': + return l10n.editMetadataFieldDate; + case 'track_number': + return l10n.editMetadataFieldTrackNum; + case 'disc_number': + return l10n.editMetadataFieldDiscNum; + case 'genre': + return l10n.editMetadataFieldGenre; + case 'isrc': + return l10n.editMetadataFieldIsrc; + case 'label': + return l10n.editMetadataFieldLabel; + case 'copyright': + return l10n.editMetadataFieldCopyright; + case 'cover': + return l10n.editMetadataFieldCover; + default: + return key; + } + } + + TextEditingController? _controllerForKey(String key) { + switch (key) { + case 'title': + return _titleCtrl; + case 'artist': + return _artistCtrl; + case 'album': + return _albumCtrl; + case 'album_artist': + return _albumArtistCtrl; + case 'date': + return _dateCtrl; + case 'track_number': + return _trackNumCtrl; + case 'disc_number': + return _discNumCtrl; + case 'genre': + return _genreCtrl; + case 'isrc': + return _isrcCtrl; + case 'label': + return _labelCtrl; + case 'copyright': + return _copyrightCtrl; + default: + return null; + } + } + + void _selectAllFields() { + setState(() { + _autoFillFields.addAll(_fieldDefs.keys); + }); + } + + void _selectEmptyFields() { + setState(() { + _autoFillFields.clear(); + for (final key in _fieldDefs.keys) { + if (key == 'cover') { + if (!_hasValue(_currentCoverPath) && !_hasValue(_selectedCoverPath)) { + _autoFillFields.add(key); + } + continue; + } + final ctrl = _controllerForKey(key); + if (ctrl != null && ctrl.text.trim().isEmpty) { + _autoFillFields.add(key); + } + } + }); + } + + Future _fetchAndFill() async { + if (_autoFillFields.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.editMetadataAutoFillNoneSelected)), + ); + return; + } + + setState(() => _fetching = true); + + try { + // Build search query from current field values + final title = _titleCtrl.text.trim(); + final artist = _artistCtrl.text.trim(); + final album = _albumCtrl.text.trim(); + final queryParts = []; + if (title.isNotEmpty) queryParts.add(title); + if (artist.isNotEmpty) queryParts.add(artist); + if (album.isNotEmpty) queryParts.add(album); + + if (queryParts.isEmpty) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.editMetadataAutoFillNoResults), + ), + ); + } + return; + } + + final query = queryParts.join(' '); + final results = await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 5, + ); + + if (!mounted) return; + + if (results.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.editMetadataAutoFillNoResults)), + ); + return; + } + + // Pick best match: prefer ISRC match, then first result + final currentIsrc = _isrcCtrl.text.trim().toUpperCase(); + Map? best; + if (currentIsrc.isNotEmpty) { + for (final r in results) { + final candidateIsrc = + (r['isrc']?.toString() ?? '').trim().toUpperCase(); + if (candidateIsrc == currentIsrc) { + best = r; + break; + } + } + } + best ??= results.first; + + // Extract metadata from best match + final enriched = { + 'title': (best['name'] ?? '').toString(), + 'artist': (best['artists'] ?? best['artist'] ?? '').toString(), + 'album': (best['album_name'] ?? best['album'] ?? '').toString(), + 'album_artist': (best['album_artist'] ?? '').toString(), + 'date': (best['release_date'] ?? '').toString(), + 'track_number': (best['track_number'] ?? '').toString(), + 'disc_number': (best['disc_number'] ?? '').toString(), + 'isrc': (best['isrc'] ?? '').toString(), + }; + + // Try to get extended metadata (genre, label, copyright) from Deezer + final trackId = + (best['spotify_id'] ?? best['id'] ?? '').toString(); + final source = (best['source'] ?? best['provider_id'] ?? '').toString(); + + if ((_autoFillFields.contains('genre') || + _autoFillFields.contains('label') || + _autoFillFields.contains('copyright')) && + trackId.isNotEmpty) { + try { + // If source is Deezer, fetch extended metadata directly + Map? extended; + if (source.toLowerCase().contains('deezer')) { + extended = await PlatformBridge.getDeezerExtendedMetadata(trackId); + } else { + // Try ISRC lookup via Deezer for genre/label/copyright + final isrcForLookup = enriched['isrc'] ?? ''; + if (isrcForLookup.isNotEmpty) { + try { + final deezerResult = await PlatformBridge.searchDeezerByISRC( + isrcForLookup, + ); + final deezerTrackId = + (deezerResult['id'] ?? deezerResult['track_id'] ?? '') + .toString(); + if (deezerTrackId.isNotEmpty) { + extended = await PlatformBridge.getDeezerExtendedMetadata( + deezerTrackId, + ); + } + } catch (_) {} + } + } + if (extended != null) { + enriched['genre'] = extended['genre'] ?? ''; + enriched['label'] = extended['label'] ?? ''; + enriched['copyright'] = extended['copyright'] ?? ''; + } + } catch (_) { + // Extended metadata is best-effort + } + } + + if (!mounted) return; + + // Apply selected fields to controllers + var filledCount = 0; + for (final key in _autoFillFields) { + if (key == 'cover') continue; // Handle cover separately below + final value = enriched[key]; + if (value != null && value.isNotEmpty && value != '0' && value != 'null') { + final ctrl = _controllerForKey(key); + if (ctrl != null) { + ctrl.text = value; + filledCount++; + } + } + } + + // Handle cover art download + if (_autoFillFields.contains('cover')) { + final coverUrl = + (best['cover_url'] ?? best['images'] ?? '').toString(); + if (coverUrl.isNotEmpty) { + try { + final tempDir = await Directory.systemTemp.createTemp( + 'autofill_cover_', + ); + final coverOutput = + '${tempDir.path}${Platform.pathSeparator}cover.jpg'; + final response = await HttpClient() + .getUrl(Uri.parse(coverUrl)) + .then((req) => req.close()); + final file = File(coverOutput); + final sink = file.openWrite(); + await response.pipe(sink); + if (await file.exists() && await file.length() > 0) { + await _cleanupSelectedCoverTemp(); + if (mounted) { + setState(() { + _selectedCoverPath = coverOutput; + _selectedCoverTempDir = tempDir.path; + _selectedCoverName = 'Online cover'; + }); + filledCount++; + } + } else { + try { + await tempDir.delete(recursive: true); + } catch (_) {} + } + } catch (_) { + // Cover download is best-effort + } + } + } + + if (mounted) { + setState(() {}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + filledCount > 0 + ? context.l10n.editMetadataAutoFillDone(filledCount) + : context.l10n.editMetadataAutoFillNoResults, + ), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.snackbarError(e.toString())), + ), + ); + } + } finally { + if (mounted) setState(() => _fetching = false); + } + } + @override void initState() { super.initState(); @@ -4416,6 +4717,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { children: [ const SizedBox(height: 6), _buildCoverEditor(cs), + _buildAutoFillSection(cs), _field('Title', _titleCtrl), _field('Artist', _artistCtrl), _field('Album', _albumCtrl), @@ -4487,6 +4789,183 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ); } + Widget _buildAutoFillSection(ColorScheme cs) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + decoration: BoxDecoration( + color: cs.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.5)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () => setState(() => _showAutoFill = !_showAutoFill), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + child: Row( + children: [ + Icon( + Icons.travel_explore, + size: 20, + color: cs.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + context.l10n.editMetadataAutoFill, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: cs.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ), + Icon( + _showAutoFill ? Icons.expand_less : Icons.expand_more, + size: 20, + color: cs.onSurfaceVariant, + ), + ], + ), + ), + ), + if (_showAutoFill) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + context.l10n.editMetadataAutoFillDesc, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: cs.onSurfaceVariant, + ), + ), + ), + const SizedBox(height: 8), + // Quick select buttons + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + _quickSelectButton( + label: context.l10n.editMetadataSelectAll, + onTap: _selectAllFields, + cs: cs, + ), + const SizedBox(width: 8), + _quickSelectButton( + label: context.l10n.editMetadataSelectEmpty, + onTap: _selectEmptyFields, + cs: cs, + ), + ], + ), + ), + const SizedBox(height: 8), + // Field chips + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Wrap( + spacing: 6, + runSpacing: 4, + children: _fieldDefs.keys.map((key) { + final selected = _autoFillFields.contains(key); + return FilterChip( + label: Text(_fieldLabel(key)), + selected: selected, + onSelected: _fetching + ? null + : (val) { + setState(() { + if (val) { + _autoFillFields.add(key); + } else { + _autoFillFields.remove(key); + } + }); + }, + selectedColor: cs.primaryContainer, + checkmarkColor: cs.onPrimaryContainer, + labelStyle: Theme.of(context).textTheme.labelSmall + ?.copyWith( + color: selected + ? cs.onPrimaryContainer + : cs.onSurfaceVariant, + ), + visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + }).toList(), + ), + ), + const SizedBox(height: 10), + // Fetch button + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + bottom: 12, + ), + child: SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: + (_fetching || _saving || _autoFillFields.isEmpty) + ? null + : _fetchAndFill, + icon: _fetching + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.auto_fix_high), + label: Text( + _fetching + ? context.l10n.editMetadataAutoFillSearching + : context.l10n.editMetadataAutoFillFetch, + ), + ), + ), + ), + ], + ], + ), + ), + ); + } + + Widget _quickSelectButton({ + required String label, + required VoidCallback onTap, + required ColorScheme cs, + }) { + return InkWell( + onTap: _fetching ? null : onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: cs.outline.withValues(alpha: 0.5)), + ), + child: Text( + label, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: cs.primary, + ), + ), + ), + ); + } + Widget _buildCoverEditor(ColorScheme cs) { final hasSelectedCover = _hasValue(_selectedCoverPath); final hasCurrentCover = _hasValue(_currentCoverPath); From f36096e0ac27fc0b36e8256e5079fa2764f2fe63 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:42:22 +0700 Subject: [PATCH 066/145] fix: resolve all flutter analyze warnings and improve auto-fill enrichment chain - Fix use_build_context_synchronously in _embedLyrics by capturing l10n strings before async gaps (snackbarFailedToWriteStorage, snackbarFailedToEmbedLyrics, snackbarUnsupportedAudioFormat) - Improve auto-fill metadata enrichment to use proper API chain: search providers -> convertSpotifyToDeezer (SongLink) for Deezer ID -> getDeezerMetadata for ISRC -> getDeezerExtendedMetadata for genre/label/copyright. Falls back to ISRC-based Deezer lookup when SongLink conversion unavailable. - flutter analyze now reports 0 issues --- lib/screens/track_metadata_screen.dart | 135 +++++++++++++++++++------ 1 file changed, 103 insertions(+), 32 deletions(-) diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 989b6a47..5dde4282 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -1757,6 +1757,11 @@ class _TrackMetadataScreenState extends ConsumerState { setState(() => _isEmbedding = true); + // Capture l10n strings before async gaps to avoid use_build_context_synchronously + final l10nFailedToWriteStorage = context.l10n.snackbarFailedToWriteStorage; + final l10nFailedToEmbedLyrics = context.l10n.snackbarFailedToEmbedLyrics; + final l10nUnsupportedFormat = context.l10n.snackbarUnsupportedAudioFormat; + String? safTempPath; String? coverPath; @@ -1793,13 +1798,13 @@ class _TrackMetadataScreenState extends ConsumerState { ); success = ok; if (!ok) { - error = context.l10n.snackbarFailedToWriteStorage; + error = l10nFailedToWriteStorage; } } else { success = true; } } else { - error = result['error']?.toString() ?? context.l10n.snackbarFailedToEmbedLyrics; + error = result['error']?.toString() ?? l10nFailedToEmbedLyrics; } } else if (isMp3 || isOpus) { final metadata = _buildFallbackMetadata(); @@ -1845,7 +1850,7 @@ class _TrackMetadataScreenState extends ConsumerState { } if (ffmpegResult == null) { - error = context.l10n.snackbarFailedToEmbedLyrics; + error = l10nFailedToEmbedLyrics; } else if (_isSafFile) { final ok = await PlatformBridge.writeTempToSaf( ffmpegResult, @@ -1853,13 +1858,13 @@ class _TrackMetadataScreenState extends ConsumerState { ); success = ok; if (!ok) { - error = context.l10n.snackbarFailedToWriteStorage; + error = l10nFailedToWriteStorage; } } else { success = true; } } else { - error = context.l10n.snackbarUnsupportedAudioFormat; + error = l10nUnsupportedFormat; } if (mounted) { @@ -4299,7 +4304,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } best ??= results.first; - // Extract metadata from best match + // Extract basic metadata from search result final enriched = { 'title': (best['name'] ?? '').toString(), 'artist': (best['artists'] ?? best['artist'] ?? '').toString(), @@ -4311,39 +4316,82 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { 'isrc': (best['isrc'] ?? '').toString(), }; - // Try to get extended metadata (genre, label, copyright) from Deezer + final needsIsrc = _autoFillFields.contains('isrc') && + enriched['isrc']!.isEmpty; + final needsExtended = _autoFillFields.contains('genre') || + _autoFillFields.contains('label') || + _autoFillFields.contains('copyright'); + final trackId = (best['spotify_id'] ?? best['id'] ?? '').toString(); - final source = (best['source'] ?? best['provider_id'] ?? '').toString(); + final source = + (best['source'] ?? best['provider_id'] ?? '').toString(); + final isDeezerSource = source.toLowerCase().contains('deezer'); - if ((_autoFillFields.contains('genre') || - _autoFillFields.contains('label') || - _autoFillFields.contains('copyright')) && - trackId.isNotEmpty) { + // Resolve Deezer track ID for extended metadata + ISRC + String? deezerId; + + if ((needsIsrc || needsExtended) && trackId.isNotEmpty) { try { - // If source is Deezer, fetch extended metadata directly - Map? extended; - if (source.toLowerCase().contains('deezer')) { - extended = await PlatformBridge.getDeezerExtendedMetadata(trackId); + if (isDeezerSource) { + // Source is Deezer — trackId is already a Deezer ID + deezerId = trackId; } else { - // Try ISRC lookup via Deezer for genre/label/copyright - final isrcForLookup = enriched['isrc'] ?? ''; - if (isrcForLookup.isNotEmpty) { - try { - final deezerResult = await PlatformBridge.searchDeezerByISRC( - isrcForLookup, - ); - final deezerTrackId = - (deezerResult['id'] ?? deezerResult['track_id'] ?? '') - .toString(); - if (deezerTrackId.isNotEmpty) { - extended = await PlatformBridge.getDeezerExtendedMetadata( - deezerTrackId, - ); - } - } catch (_) {} + // Source is Spotify/extension — convert to Deezer via SongLink + final deezerData = await PlatformBridge.convertSpotifyToDeezer( + 'track', + trackId, + ); + final trackData = deezerData['track']; + if (trackData is Map) { + final rawId = trackData['spotify_id'] as String?; + if (rawId != null && rawId.startsWith('deezer:')) { + deezerId = rawId.split(':')[1]; + } + // Also grab ISRC and release_date from the conversion response + final convIsrc = (trackData['isrc'] ?? '').toString().trim(); + if (convIsrc.isNotEmpty && enriched['isrc']!.isEmpty) { + enriched['isrc'] = convIsrc; + } + final convDate = + (trackData['release_date'] ?? '').toString().trim(); + if (convDate.isNotEmpty && enriched['date']!.isEmpty) { + enriched['date'] = convDate; + } } + // Fallback: legacy ID format + deezerId ??= (deezerData['id'] ?? '').toString(); + if (deezerId.isEmpty) deezerId = null; } + } catch (_) { + // SongLink conversion is best-effort + } + } + + if (!mounted) return; + + // Fetch ISRC from Deezer track metadata if still missing + if (needsIsrc && enriched['isrc']!.isEmpty && deezerId != null) { + try { + final deezerMeta = await PlatformBridge.getDeezerMetadata( + 'track', + deezerId, + ); + final deezerIsrc = (deezerMeta['isrc'] ?? '').toString().trim(); + if (deezerIsrc.isNotEmpty) { + enriched['isrc'] = deezerIsrc; + } + } catch (_) {} + } + + if (!mounted) return; + + // Fetch genre/label/copyright from Deezer extended metadata + if (needsExtended && deezerId != null) { + try { + final extended = await PlatformBridge.getDeezerExtendedMetadata( + deezerId, + ); if (extended != null) { enriched['genre'] = extended['genre'] ?? ''; enriched['label'] = extended['label'] ?? ''; @@ -4354,6 +4402,29 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } } + // Fallback: if still no Deezer ID but we have ISRC, try ISRC lookup + if (needsExtended && + deezerId == null && + enriched['isrc']!.isNotEmpty) { + try { + final deezerResult = await PlatformBridge.searchDeezerByISRC( + enriched['isrc']!, + ); + final fallbackId = + (deezerResult['id'] ?? deezerResult['track_id'] ?? '') + .toString(); + if (fallbackId.isNotEmpty) { + final extended = + await PlatformBridge.getDeezerExtendedMetadata(fallbackId); + if (extended != null) { + enriched['genre'] = extended['genre'] ?? ''; + enriched['label'] = extended['label'] ?? ''; + enriched['copyright'] = extended['copyright'] ?? ''; + } + } + } catch (_) {} + } + if (!mounted) return; // Apply selected fields to controllers From 35f2f119db7d5a35788b26aaa67322c3ba9a7103 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 21:11:36 +0700 Subject: [PATCH 067/145] feat: improve auto-fill track resolution in Edit Metadata sheet - Identifier-first resolution (ISRC/Deezer/Spotify) before falling back to text search - Score-based match selection via _metadataMatchScore instead of provider order - Pass sourceTrackId from TrackMetadataScreen into _EditMetadataSheet - Refactor buildDeezerExtendedMetadataResult and buildDeezerISRCSearchResult as testable helpers - Add unit tests for buildDeezerExtendedMetadataResult and buildDeezerISRCSearchResult - Propagate copyright through Deezer enrichment chain (exports, extension_providers) --- go_backend/deezer.go | 13 +- go_backend/exports.go | 75 ++- go_backend/exports_deezer_metadata_test.go | 59 ++ go_backend/extension_providers.go | 17 +- lib/screens/track_metadata_screen.dart | 592 +++++++++++++++------ 5 files changed, 566 insertions(+), 190 deletions(-) create mode 100644 go_backend/exports_deezer_metadata_test.go diff --git a/go_backend/deezer.go b/go_backend/deezer.go index 568e61c4..a389f3c8 100644 --- a/go_backend/deezer.go +++ b/go_backend/deezer.go @@ -256,6 +256,7 @@ type deezerAlbumFull struct { NbTracks int `json:"nb_tracks"` RecordType string `json:"record_type"` Label string `json:"label"` + Copyright string `json:"copyright"` Genres struct { Data []deezerGenre `json:"data"` } `json:"genres"` @@ -1084,8 +1085,9 @@ func (c *DeezerClient) getBestAlbumImage(album deezerAlbumFull) string { } type AlbumExtendedMetadata struct { - Genre string - Label string + Genre string + Label string + Copyright string } func (c *DeezerClient) GetAlbumExtendedMetadata(ctx context.Context, albumID string) (*AlbumExtendedMetadata, error) { @@ -1116,8 +1118,9 @@ func (c *DeezerClient) GetAlbumExtendedMetadata(ctx context.Context, albumID str } result := &AlbumExtendedMetadata{ - Genre: strings.Join(genres, ", "), - Label: album.Label, + Genre: strings.Join(genres, ", "), + Label: album.Label, + Copyright: album.Copyright, } c.cacheMu.Lock() @@ -1129,7 +1132,7 @@ func (c *DeezerClient) GetAlbumExtendedMetadata(ctx context.Context, albumID str c.maybeCleanupCachesLocked(now) c.cacheMu.Unlock() - GoLog("[Deezer] Album metadata fetched - Genre: %s, Label: %s\n", result.Genre, result.Label) + GoLog("[Deezer] Album metadata fetched - Genre: %s, Label: %s, Copyright: %s\n", result.Genre, result.Label, result.Copyright) return result, nil } diff --git a/go_backend/exports.go b/go_backend/exports.go index b8178d77..1c5f84ed 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -283,7 +283,7 @@ func enrichRequestExtendedMetadata(req *DownloadRequest) { return } - if req.ISRC == "" || (req.Genre != "" && req.Label != "") { + if req.ISRC == "" || (req.Genre != "" && req.Label != "" && req.Copyright != "") { return } @@ -305,8 +305,11 @@ func enrichRequestExtendedMetadata(req *DownloadRequest) { if req.Label == "" && extMeta.Label != "" { req.Label = extMeta.Label } - if req.Genre != "" || req.Label != "" { - GoLog("[DownloadWithFallback] Extended metadata ready: genre=%s, label=%s\n", req.Genre, req.Label) + if req.Copyright == "" && extMeta.Copyright != "" { + req.Copyright = extMeta.Copyright + } + if req.Genre != "" || req.Label != "" || req.Copyright != "" { + GoLog("[DownloadWithFallback] Extended metadata ready: genre=%s, label=%s, copyright=%s\n", req.Genre, req.Label, req.Copyright) } } @@ -1335,10 +1338,7 @@ func GetDeezerExtendedMetadata(trackID string) (string, error) { return "", err } - result := map[string]string{ - "genre": metadata.Genre, - "label": metadata.Label, - } + result := buildDeezerExtendedMetadataResult(metadata) jsonBytes, err := json.Marshal(result) if err != nil { @@ -1358,7 +1358,8 @@ func SearchDeezerByISRC(isrc string) (string, error) { return "", err } - jsonBytes, err := json.Marshal(track) + result := buildDeezerISRCSearchResult(track) + jsonBytes, err := json.Marshal(result) if err != nil { return "", err } @@ -1366,6 +1367,55 @@ func SearchDeezerByISRC(isrc string) (string, error) { return string(jsonBytes), nil } +func buildDeezerExtendedMetadataResult(metadata *AlbumExtendedMetadata) map[string]string { + if metadata == nil { + return map[string]string{ + "genre": "", + "label": "", + "copyright": "", + } + } + + return map[string]string{ + "genre": metadata.Genre, + "label": metadata.Label, + "copyright": metadata.Copyright, + } +} + +func buildDeezerISRCSearchResult(track *TrackMetadata) map[string]interface{} { + if track == nil { + return map[string]interface{}{} + } + + result := map[string]interface{}{ + "spotify_id": track.SpotifyID, + "artists": track.Artists, + "name": track.Name, + "album_name": track.AlbumName, + "album_artist": track.AlbumArtist, + "duration_ms": track.DurationMS, + "images": track.Images, + "release_date": track.ReleaseDate, + "track_number": track.TrackNumber, + "total_tracks": track.TotalTracks, + "disc_number": track.DiscNumber, + "external_urls": track.ExternalURL, + "isrc": track.ISRC, + "album_id": track.AlbumID, + "artist_id": track.ArtistID, + "album_type": track.AlbumType, + } + + if deezerID := strings.TrimSpace(strings.TrimPrefix(track.SpotifyID, "deezer:")); deezerID != "" { + result["id"] = deezerID + result["track_id"] = deezerID + result["success"] = true + } + + return result +} + func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -1824,8 +1874,8 @@ func ReEnrichFile(requestJSON string) (string, error) { GoLog("[ReEnrich] Metadata provider search failed: %v\n", searchErr) } - // Try to get extended metadata (genre, label) from Deezer if not already set - if found && req.ISRC != "" && (req.Genre == "" || req.Label == "") { + // Try to get extended metadata from Deezer if not already set + if found && req.ISRC != "" && (req.Genre == "" || req.Label == "" || req.Copyright == "") { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) extMeta, err := deezerClient.GetExtendedMetadataByISRC(ctx, req.ISRC) cancel() @@ -1836,7 +1886,10 @@ func ReEnrichFile(requestJSON string) (string, error) { if req.Label == "" && extMeta.Label != "" { req.Label = extMeta.Label } - GoLog("[ReEnrich] Extended metadata: genre=%s, label=%s\n", req.Genre, req.Label) + if req.Copyright == "" && extMeta.Copyright != "" { + req.Copyright = extMeta.Copyright + } + GoLog("[ReEnrich] Extended metadata: genre=%s, label=%s, copyright=%s\n", req.Genre, req.Label, req.Copyright) } } diff --git a/go_backend/exports_deezer_metadata_test.go b/go_backend/exports_deezer_metadata_test.go new file mode 100644 index 00000000..763965fc --- /dev/null +++ b/go_backend/exports_deezer_metadata_test.go @@ -0,0 +1,59 @@ +package gobackend + +import "testing" + +func TestBuildDeezerExtendedMetadataResultHandlesNil(t *testing.T) { + result := buildDeezerExtendedMetadataResult(nil) + + if result["genre"] != "" { + t.Fatalf("expected empty genre, got %q", result["genre"]) + } + if result["label"] != "" { + t.Fatalf("expected empty label, got %q", result["label"]) + } + if result["copyright"] != "" { + t.Fatalf("expected empty copyright, got %q", result["copyright"]) + } +} + +func TestBuildDeezerExtendedMetadataResultIncludesCopyright(t *testing.T) { + result := buildDeezerExtendedMetadataResult(&AlbumExtendedMetadata{ + Genre: "Rock", + Label: "EMI", + Copyright: "(C) Queen", + }) + + if result["genre"] != "Rock" { + t.Fatalf("unexpected genre: %q", result["genre"]) + } + if result["label"] != "EMI" { + t.Fatalf("unexpected label: %q", result["label"]) + } + if result["copyright"] != "(C) Queen" { + t.Fatalf("unexpected copyright: %q", result["copyright"]) + } +} + +func TestBuildDeezerISRCSearchResultAddsCompatibilityIDs(t *testing.T) { + result := buildDeezerISRCSearchResult(&TrackMetadata{ + SpotifyID: "deezer:3135556", + Name: "Love Of My Life", + Artists: "Queen", + AlbumName: "A Night at the Opera", + ISRC: "GBUM71029604", + ReleaseDate: "1975-11-21", + }) + + if result["spotify_id"] != "deezer:3135556" { + t.Fatalf("unexpected spotify_id: %v", result["spotify_id"]) + } + if result["id"] != "3135556" { + t.Fatalf("unexpected id: %v", result["id"]) + } + if result["track_id"] != "3135556" { + t.Fatalf("unexpected track_id: %v", result["track_id"]) + } + if result["success"] != true { + t.Fatalf("expected success=true, got %v", result["success"]) + } +} diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 1866543f..b9f79bc4 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -1065,8 +1065,9 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro GoLog("[DownloadWithExtensionFallback] Metadata provider search failed (non-fatal): %v\n", searchErr) } - // Try Deezer extended metadata for genre/label if we have ISRC - if req.ISRC != "" && (req.Genre == "" || req.Label == "") { + // Try Deezer extended metadata if we have ISRC + if req.ISRC != "" && + (req.Genre == "" || req.Label == "" || req.Copyright == "") { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) extMeta, err := GetDeezerClient().GetExtendedMetadataByISRC(ctx, req.ISRC) cancel() @@ -1077,7 +1078,10 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro if req.Label == "" && extMeta.Label != "" { req.Label = extMeta.Label } - GoLog("[DownloadWithExtensionFallback] Extended metadata from Deezer: genre=%s, label=%s\n", req.Genre, req.Label) + if req.Copyright == "" && extMeta.Copyright != "" { + req.Copyright = extMeta.Copyright + } + GoLog("[DownloadWithExtensionFallback] Extended metadata from Deezer: genre=%s, label=%s, copyright=%s\n", req.Genre, req.Label, req.Copyright) } } } @@ -1249,7 +1253,8 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro GoLog("[DownloadWithExtensionFallback] Trying provider: %s\n", providerID) if isBuiltInProvider(providerIDNormalized) { - if (req.Genre == "" || req.Label == "") && req.ISRC != "" { + if (req.Genre == "" || req.Label == "" || req.Copyright == "") && + req.ISRC != "" { GoLog("[DownloadWithExtensionFallback] Enriching extended metadata from Deezer for ISRC: %s\n", req.ISRC) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) deezerClient := GetDeezerClient() @@ -1264,6 +1269,10 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro req.Label = extMeta.Label GoLog("[DownloadWithExtensionFallback] Label from Deezer: %s\n", req.Label) } + if req.Copyright == "" && extMeta.Copyright != "" { + req.Copyright = extMeta.Copyright + GoLog("[DownloadWithExtensionFallback] Copyright from Deezer: %s\n", req.Copyright) + } } else if err != nil { GoLog("[DownloadWithExtensionFallback] Failed to get extended metadata from Deezer: %v\n", err) } diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 5dde4282..85e803b6 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -307,11 +307,13 @@ class _TrackMetadataScreenState extends ConsumerState { ); // Fill in album name from file tags if stored value is empty - final needsAlbum = resolvedAlbum != null && + final needsAlbum = + resolvedAlbum != null && resolvedAlbum.isNotEmpty && (albumName.isEmpty); // Fill in duration from file if stored value is missing/zero - final needsDuration = resolvedDuration != null && + final needsDuration = + resolvedDuration != null && resolvedDuration > 0 && (duration == null || duration == 0); @@ -585,9 +587,9 @@ class _TrackMetadataScreenState extends ConsumerState { } void _showCueVirtualTrackSnackBar(BuildContext context) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(_cueVirtualTrackGuidance(context))), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(_cueVirtualTrackGuidance(context)))); } void _hideCurrentSnackBar() { @@ -606,17 +608,14 @@ class _TrackMetadataScreenState extends ConsumerState { } void _showSnackBarMessage(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); } void _showLongSnackBarMessage(String message) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - duration: const Duration(seconds: 60), - ), + SnackBar(content: Text(message), duration: const Duration(seconds: 60)), ); } @@ -1144,9 +1143,7 @@ class _TrackMetadataScreenState extends ConsumerState { _copyToClipboard(context, webUrl); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.snackbarUrlCopied(serviceName), - ), + content: Text(context.l10n.snackbarUrlCopied(serviceName)), ), ); } @@ -1879,16 +1876,18 @@ class _TrackMetadataScreenState extends ConsumerState { } else { setState(() => _isEmbedding = false); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(error ?? context.l10n.snackbarFailedToEmbedLyrics)), + SnackBar( + content: Text(error ?? context.l10n.snackbarFailedToEmbedLyrics), + ), ); } } } catch (e) { if (mounted) { setState(() => _isEmbedding = false); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), + ); } } finally { if (coverPath != null) { @@ -2568,8 +2567,11 @@ class _TrackMetadataScreenState extends ConsumerState { onTap: () { _closeOptionsMenuAndRun( sheetContext, - () => - _showEditMetadataSheet(screenContext, ref, colorScheme), + () => _showEditMetadataSheet( + screenContext, + ref, + colorScheme, + ), ); }, ), @@ -3008,31 +3010,29 @@ class _TrackMetadataScreenState extends ConsumerState { const SizedBox(height: 16), Text( sheetContext.l10n.cueSplitTitle, - style: Theme.of(sheetContext).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of(sheetContext).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), Text( sheetContext.l10n.cueSplitAlbum(album), - style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(sheetContext).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: 4), Text( sheetContext.l10n.cueSplitArtist(artist), - style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(sheetContext).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: 4), Text( sheetContext.l10n.cueSplitTrackCount(tracks.length), - style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( - color: colorScheme.primary, - fontWeight: FontWeight.w600, - ), + style: Theme.of(sheetContext).textTheme.bodyMedium + ?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), ), const SizedBox(height: 16), // Track list preview (scrollable, max 200px) @@ -3259,8 +3259,12 @@ class _TrackMetadataScreenState extends ConsumerState { // Determine output directory final String outputDir; - final treeUri = !_isLocalItem ? (_downloadItem?.downloadTreeUri ?? '') : ''; - final relativeDir = !_isLocalItem ? (_downloadItem?.safRelativeDir ?? '') : ''; + final treeUri = !_isLocalItem + ? (_downloadItem?.downloadTreeUri ?? '') + : ''; + final relativeDir = !_isLocalItem + ? (_downloadItem?.safRelativeDir ?? '') + : ''; final writeBackToSaf = isSafSource && treeUri.isNotEmpty; if (writeBackToSaf) { final tempDir = await getTemporaryDirectory(); @@ -3326,9 +3330,7 @@ class _TrackMetadataScreenState extends ConsumerState { // Read existing metadata first final metadata = await PlatformBridge.readFileMetadata(path); if (metadata['error'] == null) { - final fields = { - 'cover_path': coverPath, - }; + final fields = {'cover_path': coverPath}; // Preserve existing fields for (final entry in metadata.entries) { if (entry.key == 'error' || entry.value == null) continue; @@ -3706,6 +3708,7 @@ class _TrackMetadataScreenState extends ConsumerState { colorScheme: colorScheme, initialValues: initialValues, filePath: cleanFilePath, + sourceTrackId: _spotifyId, ), ); @@ -3789,10 +3792,7 @@ class _TrackMetadataScreenState extends ConsumerState { ); } - void _closeOptionsMenuAndRun( - BuildContext sheetContext, - VoidCallback action, - ) { + void _closeOptionsMenuAndRun(BuildContext sheetContext, VoidCallback action) { Navigator.pop(sheetContext); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; @@ -3919,15 +3919,24 @@ class _TrackMetadataScreenState extends ConsumerState { } } +class _ResolvedAutoFillTrack { + final Map track; + final String? deezerId; + + const _ResolvedAutoFillTrack({required this.track, this.deezerId}); +} + class _EditMetadataSheet extends StatefulWidget { final ColorScheme colorScheme; final Map initialValues; final String filePath; + final String? sourceTrackId; const _EditMetadataSheet({ required this.colorScheme, required this.initialValues, required this.filePath, + this.sourceTrackId, }); @override @@ -3935,6 +3944,12 @@ class _EditMetadataSheet extends StatefulWidget { } class _EditMetadataSheetState extends State<_EditMetadataSheet> { + static final RegExp _metadataCollapsePattern = RegExp(r'[^a-z0-9]+'); + static final RegExp _metadataWhitespacePattern = RegExp(r'\s+'); + static final RegExp _spotifyTrackIdPattern = RegExp(r'^[A-Za-z0-9]{22}$'); + static final RegExp _deezerTrackIdPattern = RegExp(r'^\d+$'); + static final RegExp _isrcPattern = RegExp(r'^[A-Z]{2}[A-Z0-9]{3}\d{7}$'); + bool _saving = false; bool _showAdvanced = false; bool _showAutoFill = false; @@ -4152,9 +4167,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { }); } catch (e) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), + ); } } @@ -4243,6 +4258,251 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { }); } + String _normalizeMetadataText(String value) { + final collapsed = value + .toLowerCase() + .replaceAll(_metadataCollapsePattern, ' ') + .trim(); + return collapsed.replaceAll(_metadataWhitespacePattern, ' '); + } + + bool _looksLikeIsrc(String value) { + return _isrcPattern.hasMatch(value.trim().toUpperCase()); + } + + String? _extractRawSpotifyTrackIdFromValue(Object? value) { + final raw = value?.toString().trim() ?? ''; + if (raw.isEmpty) return null; + + if (_spotifyTrackIdPattern.hasMatch(raw)) { + return raw; + } + + if (raw.startsWith('spotify:')) { + final parts = raw.split(':'); + final last = parts.isNotEmpty ? parts.last.trim() : ''; + if (_spotifyTrackIdPattern.hasMatch(last)) { + return last; + } + return null; + } + + final uri = Uri.tryParse(raw); + if (uri != null && + uri.host.contains('spotify.com') && + uri.pathSegments.length >= 2 && + uri.pathSegments.first == 'track') { + final candidate = uri.pathSegments[1].trim(); + if (_spotifyTrackIdPattern.hasMatch(candidate)) { + return candidate; + } + } + + return null; + } + + String? _extractRawDeezerTrackIdFromValue(Object? value) { + final raw = value?.toString().trim() ?? ''; + if (raw.isEmpty) return null; + + if (_deezerTrackIdPattern.hasMatch(raw)) { + return raw; + } + + if (raw.startsWith('deezer:')) { + final parts = raw.split(':'); + final last = parts.isNotEmpty ? parts.last.trim() : ''; + if (_deezerTrackIdPattern.hasMatch(last)) { + return last; + } + } + + final uri = Uri.tryParse(raw); + if (uri != null && uri.host.contains('deezer.com')) { + final trackIndex = uri.pathSegments.indexOf('track'); + if (trackIndex >= 0 && trackIndex + 1 < uri.pathSegments.length) { + final candidate = uri.pathSegments[trackIndex + 1].trim(); + if (_deezerTrackIdPattern.hasMatch(candidate)) { + return candidate; + } + } + } + + return null; + } + + String? _extractRawSpotifyTrackId(Map track) { + for (final candidate in [track['spotify_id'], track['id']]) { + final spotifyId = _extractRawSpotifyTrackIdFromValue(candidate); + if (spotifyId != null) return spotifyId; + } + + final externalLinks = track['external_links']; + if (externalLinks is Map) { + final spotifyId = _extractRawSpotifyTrackIdFromValue( + externalLinks['spotify'], + ); + if (spotifyId != null) return spotifyId; + } + + return null; + } + + String? _extractRawDeezerTrackId(Map track) { + for (final candidate in [ + track['deezer_id'], + track['spotify_id'], + track['id'], + ]) { + final deezerId = _extractRawDeezerTrackIdFromValue(candidate); + if (deezerId != null) return deezerId; + } + + final externalLinks = track['external_links']; + if (externalLinks is Map) { + final deezerId = _extractRawDeezerTrackIdFromValue( + externalLinks['deezer'], + ); + if (deezerId != null) return deezerId; + } + + return null; + } + + Map _unwrapTrackPayload(Map payload) { + final track = payload['track']; + if (track is Map) { + return track; + } + return payload; + } + + void _mergeOnlineTrackData( + Map enriched, + Map track, + ) { + void put(String key, Object? value) { + final text = value?.toString().trim() ?? ''; + if (text.isNotEmpty && text != 'null') { + enriched[key] = text; + } + } + + put('title', track['name'] ?? track['title']); + put('artist', track['artists'] ?? track['artist']); + put('album', track['album_name'] ?? track['album']); + put('album_artist', track['album_artist']); + put('date', track['release_date']); + put('track_number', track['track_number']); + put('disc_number', track['disc_number']); + put('isrc', track['isrc']); + put('genre', track['genre']); + put('label', track['label']); + put('copyright', track['copyright']); + } + + Future<_ResolvedAutoFillTrack?> _resolveAutoFillTrackFromIdentifiers( + String currentIsrc, + ) async { + if (_looksLikeIsrc(currentIsrc)) { + final deezerTrack = await PlatformBridge.searchDeezerByISRC(currentIsrc); + return _ResolvedAutoFillTrack( + track: _unwrapTrackPayload(deezerTrack), + deezerId: _extractRawDeezerTrackId(deezerTrack), + ); + } + + final sourceTrackId = widget.sourceTrackId?.trim() ?? ''; + if (sourceTrackId.isEmpty) { + return null; + } + + final deezerId = _extractRawDeezerTrackIdFromValue(sourceTrackId); + if (deezerId != null) { + final deezerTrack = await PlatformBridge.getDeezerMetadata( + 'track', + deezerId, + ); + return _ResolvedAutoFillTrack( + track: _unwrapTrackPayload(deezerTrack), + deezerId: deezerId, + ); + } + + final spotifyId = _extractRawSpotifyTrackIdFromValue(sourceTrackId); + if (spotifyId != null) { + final deezerTrack = await PlatformBridge.convertSpotifyToDeezer( + 'track', + spotifyId, + ); + final track = _unwrapTrackPayload(deezerTrack); + return _ResolvedAutoFillTrack( + track: track, + deezerId: + _extractRawDeezerTrackId(track) ?? + _extractRawDeezerTrackId(deezerTrack), + ); + } + + return null; + } + + int _metadataMatchScore( + Map track, { + required String currentTitle, + required String currentArtist, + required String currentAlbum, + required String currentIsrc, + }) { + var score = 0; + + final candidateIsrc = (track['isrc']?.toString() ?? '') + .trim() + .toUpperCase(); + if (currentIsrc.isNotEmpty && candidateIsrc == currentIsrc) { + score += 10000; + } + + final candidateTitle = _normalizeMetadataText( + (track['name'] ?? track['title'] ?? '').toString(), + ); + final candidateArtist = _normalizeMetadataText( + (track['artists'] ?? track['artist'] ?? '').toString(), + ); + final candidateAlbum = _normalizeMetadataText( + (track['album_name'] ?? track['album'] ?? '').toString(), + ); + + if (currentTitle.isNotEmpty && candidateTitle.isNotEmpty) { + if (candidateTitle == currentTitle) { + score += 400; + } else if (candidateTitle.contains(currentTitle) || + currentTitle.contains(candidateTitle)) { + score += 180; + } + } + + if (currentArtist.isNotEmpty && candidateArtist.isNotEmpty) { + if (candidateArtist == currentArtist) { + score += 320; + } else if (candidateArtist.contains(currentArtist) || + currentArtist.contains(candidateArtist)) { + score += 140; + } + } + + if (currentAlbum.isNotEmpty && candidateAlbum.isNotEmpty) { + if (candidateAlbum == currentAlbum) { + score += 120; + } else if (candidateAlbum.contains(currentAlbum) || + currentAlbum.contains(candidateAlbum)) { + score += 50; + } + } + + return score; + } + Future _fetchAndFill() async { if (_autoFillFields.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -4254,117 +4514,137 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { setState(() => _fetching = true); try { - // Build search query from current field values final title = _titleCtrl.text.trim(); final artist = _artistCtrl.text.trim(); final album = _albumCtrl.text.trim(); + final currentIsrc = _isrcCtrl.text.trim().toUpperCase(); + Map? best; + String? deezerId; + + try { + final resolved = await _resolveAutoFillTrackFromIdentifiers( + currentIsrc, + ); + if (resolved != null) { + best = resolved.track; + deezerId = resolved.deezerId; + } + } catch (e) { + _log.w('Identifier-first autofill lookup failed: $e'); + } + final queryParts = []; if (title.isNotEmpty) queryParts.add(title); if (artist.isNotEmpty) queryParts.add(artist); - if (album.isNotEmpty) queryParts.add(album); + if (queryParts.isEmpty && album.isNotEmpty) queryParts.add(album); - if (queryParts.isEmpty) { + if (best == null && queryParts.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.editMetadataAutoFillNoResults), - ), + SnackBar(content: Text(context.l10n.editMetadataAutoFillNoResults)), ); } return; } - final query = queryParts.join(' '); - final results = await PlatformBridge.searchTracksWithMetadataProviders( - query, - limit: 5, - ); + final normalizedTitle = _normalizeMetadataText(title); + final normalizedArtist = _normalizeMetadataText(artist); + final normalizedAlbum = _normalizeMetadataText(album); - if (!mounted) return; - - if (results.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.editMetadataAutoFillNoResults)), + if (best == null) { + final query = queryParts.join(' '); + final results = await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 5, ); - return; - } - // Pick best match: prefer ISRC match, then first result - final currentIsrc = _isrcCtrl.text.trim().toUpperCase(); - Map? best; - if (currentIsrc.isNotEmpty) { - for (final r in results) { - final candidateIsrc = - (r['isrc']?.toString() ?? '').trim().toUpperCase(); - if (candidateIsrc == currentIsrc) { - best = r; - break; + if (!mounted) return; + + if (results.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.editMetadataAutoFillNoResults)), + ); + return; + } + + // Pick best match using current metadata, not only provider order. + best = results.first; + var bestScore = -1; + for (final result in results) { + final score = _metadataMatchScore( + result, + currentTitle: normalizedTitle, + currentArtist: normalizedArtist, + currentAlbum: normalizedAlbum, + currentIsrc: currentIsrc, + ); + if (score > bestScore) { + bestScore = score; + best = result; } } } - best ??= results.first; + + final selectedBest = best; + if (selectedBest == null) { + throw StateError('No metadata match resolved for auto-fill'); + } // Extract basic metadata from search result final enriched = { - 'title': (best['name'] ?? '').toString(), - 'artist': (best['artists'] ?? best['artist'] ?? '').toString(), - 'album': (best['album_name'] ?? best['album'] ?? '').toString(), - 'album_artist': (best['album_artist'] ?? '').toString(), - 'date': (best['release_date'] ?? '').toString(), - 'track_number': (best['track_number'] ?? '').toString(), - 'disc_number': (best['disc_number'] ?? '').toString(), - 'isrc': (best['isrc'] ?? '').toString(), + 'title': (selectedBest['name'] ?? '').toString(), + 'artist': (selectedBest['artists'] ?? selectedBest['artist'] ?? '') + .toString(), + 'album': (selectedBest['album_name'] ?? selectedBest['album'] ?? '') + .toString(), + 'album_artist': (selectedBest['album_artist'] ?? '').toString(), + 'date': (selectedBest['release_date'] ?? '').toString(), + 'track_number': (selectedBest['track_number'] ?? '').toString(), + 'disc_number': (selectedBest['disc_number'] ?? '').toString(), + 'isrc': (selectedBest['isrc'] ?? '').toString(), }; + _mergeOnlineTrackData(enriched, selectedBest); - final needsIsrc = _autoFillFields.contains('isrc') && - enriched['isrc']!.isEmpty; - final needsExtended = _autoFillFields.contains('genre') || + final needsIsrc = + _autoFillFields.contains('isrc') && enriched['isrc']!.isEmpty; + final needsExtended = + _autoFillFields.contains('genre') || _autoFillFields.contains('label') || _autoFillFields.contains('copyright'); - final trackId = - (best['spotify_id'] ?? best['id'] ?? '').toString(); - final source = - (best['source'] ?? best['provider_id'] ?? '').toString(); - final isDeezerSource = source.toLowerCase().contains('deezer'); + final rawSpotifyId = _extractRawSpotifyTrackId(selectedBest); - // Resolve Deezer track ID for extended metadata + ISRC - String? deezerId; + deezerId ??= _extractRawDeezerTrackId(selectedBest); + final candidateIsrc = enriched['isrc']!.trim().toUpperCase(); + final deezerLookupIsrc = _looksLikeIsrc(currentIsrc) + ? currentIsrc + : (_looksLikeIsrc(candidateIsrc) ? candidateIsrc : ''); - if ((needsIsrc || needsExtended) && trackId.isNotEmpty) { + if (needsIsrc || needsExtended) { try { - if (isDeezerSource) { - // Source is Deezer — trackId is already a Deezer ID - deezerId = trackId; - } else { - // Source is Spotify/extension — convert to Deezer via SongLink + if (deezerId == null && deezerLookupIsrc.isNotEmpty) { + final deezerResult = await PlatformBridge.searchDeezerByISRC( + deezerLookupIsrc, + ); + deezerId = _extractRawDeezerTrackId(deezerResult); + _mergeOnlineTrackData(enriched, deezerResult); + } + + if (deezerId == null && rawSpotifyId != null) { + // Spotify IDs can be mapped through SongLink to a Deezer track. final deezerData = await PlatformBridge.convertSpotifyToDeezer( 'track', - trackId, + rawSpotifyId, ); final trackData = deezerData['track']; if (trackData is Map) { - final rawId = trackData['spotify_id'] as String?; - if (rawId != null && rawId.startsWith('deezer:')) { - deezerId = rawId.split(':')[1]; - } - // Also grab ISRC and release_date from the conversion response - final convIsrc = (trackData['isrc'] ?? '').toString().trim(); - if (convIsrc.isNotEmpty && enriched['isrc']!.isEmpty) { - enriched['isrc'] = convIsrc; - } - final convDate = - (trackData['release_date'] ?? '').toString().trim(); - if (convDate.isNotEmpty && enriched['date']!.isEmpty) { - enriched['date'] = convDate; - } + deezerId = _extractRawDeezerTrackId(trackData); + _mergeOnlineTrackData(enriched, trackData); } - // Fallback: legacy ID format - deezerId ??= (deezerData['id'] ?? '').toString(); - if (deezerId.isEmpty) deezerId = null; + deezerId ??= _extractRawDeezerTrackId(deezerData); } } catch (_) { - // SongLink conversion is best-effort + // Deezer resolution is best-effort } } @@ -4377,7 +4657,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { 'track', deezerId, ); - final deezerIsrc = (deezerMeta['isrc'] ?? '').toString().trim(); + final trackData = _unwrapTrackPayload(deezerMeta); + _mergeOnlineTrackData(enriched, trackData); + final deezerIsrc = (trackData['isrc'] ?? '').toString().trim(); if (deezerIsrc.isNotEmpty) { enriched['isrc'] = deezerIsrc; } @@ -4402,29 +4684,6 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } } - // Fallback: if still no Deezer ID but we have ISRC, try ISRC lookup - if (needsExtended && - deezerId == null && - enriched['isrc']!.isNotEmpty) { - try { - final deezerResult = await PlatformBridge.searchDeezerByISRC( - enriched['isrc']!, - ); - final fallbackId = - (deezerResult['id'] ?? deezerResult['track_id'] ?? '') - .toString(); - if (fallbackId.isNotEmpty) { - final extended = - await PlatformBridge.getDeezerExtendedMetadata(fallbackId); - if (extended != null) { - enriched['genre'] = extended['genre'] ?? ''; - enriched['label'] = extended['label'] ?? ''; - enriched['copyright'] = extended['copyright'] ?? ''; - } - } - } catch (_) {} - } - if (!mounted) return; // Apply selected fields to controllers @@ -4432,7 +4691,10 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { for (final key in _autoFillFields) { if (key == 'cover') continue; // Handle cover separately below final value = enriched[key]; - if (value != null && value.isNotEmpty && value != '0' && value != 'null') { + if (value != null && + value.isNotEmpty && + value != '0' && + value != 'null') { final ctrl = _controllerForKey(key); if (ctrl != null) { ctrl.text = value; @@ -4444,7 +4706,8 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { // Handle cover art download if (_autoFillFields.contains('cover')) { final coverUrl = - (best['cover_url'] ?? best['images'] ?? '').toString(); + (selectedBest['cover_url'] ?? selectedBest['images'] ?? '') + .toString(); if (coverUrl.isNotEmpty) { try { final tempDir = await Directory.systemTemp.createTemp( @@ -4494,9 +4757,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.snackbarError(e.toString())), - ), + SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), ); } } finally { @@ -4722,9 +4983,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } } catch (e) { if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), + ); } } finally { if (mounted) setState(() => _saving = false); @@ -4882,11 +5143,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ), child: Row( children: [ - Icon( - Icons.travel_explore, - size: 20, - color: cs.primary, - ), + Icon(Icons.travel_explore, size: 20, color: cs.primary), const SizedBox(width: 8), Expanded( child: Text( @@ -4911,9 +5168,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( context.l10n.editMetadataAutoFillDesc, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: cs.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant), ), ), const SizedBox(height: 8), @@ -4976,18 +5233,13 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { const SizedBox(height: 10), // Fetch button Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - bottom: 12, - ), + padding: const EdgeInsets.only(left: 12, right: 12, bottom: 12), child: SizedBox( width: double.infinity, child: FilledButton.icon( - onPressed: - (_fetching || _saving || _autoFillFields.isEmpty) - ? null - : _fetchAndFill, + onPressed: (_fetching || _saving || _autoFillFields.isEmpty) + ? null + : _fetchAndFill, icon: _fetching ? const SizedBox( width: 16, @@ -5029,9 +5281,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ), child: Text( label, - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: cs.primary, - ), + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: cs.primary), ), ), ); From b8af75bf6edaacff34ed4c93fb75fb6ba045ab63 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:13:45 +0700 Subject: [PATCH 068/145] feat: add FLAC/ALAC bidirectional lossless conversion support - Add _convertToAlac() and _convertToFlac() in ffmpeg_service with single-pass FFmpeg encoding, metadata tags, and cover art embedding - Wire lossless formats (ALAC, FLAC) into single-track convert sheet with dynamic format list based on source format, hidden bitrate for lossless targets, and lossless hint text - Add lossless conversion to batch convert UI in downloaded_album, local_album, and queue_tab screens with lossy-source filtering - Fix M4A quality probe in Go backend: increase audio sample entry buffer from 24 to 32 bytes, read sample rate from correct offset (bytes 28-29) and bit depth from samplesize field (bytes 22-23) - Add l10n keys for lossless confirm dialogs and hints (en, id) --- go_backend/metadata.go | 25 ++- lib/l10n/app_localizations.dart | 23 ++- lib/l10n/app_localizations_de.dart | 23 +++ lib/l10n/app_localizations_en.dart | 26 ++- lib/l10n/app_localizations_es.dart | 26 ++- lib/l10n/app_localizations_fr.dart | 23 +++ lib/l10n/app_localizations_hi.dart | 23 +++ lib/l10n/app_localizations_id.dart | 26 ++- lib/l10n/app_localizations_ja.dart | 23 +++ lib/l10n/app_localizations_ko.dart | 23 +++ lib/l10n/app_localizations_nl.dart | 23 +++ lib/l10n/app_localizations_pt.dart | 26 ++- lib/l10n/app_localizations_ru.dart | 23 +++ lib/l10n/app_localizations_tr.dart | 23 +++ lib/l10n/app_localizations_zh.dart | 26 ++- lib/l10n/arb/app_en.arb | 30 +++- lib/l10n/arb/app_id.arb | 18 +- lib/screens/downloaded_album_screen.dart | 152 ++++++++++------ lib/screens/local_album_screen.dart | 150 ++++++++++------ lib/screens/queue_tab.dart | 154 ++++++++++------ lib/screens/track_metadata_screen.dart | 174 ++++++++++++------ lib/services/ffmpeg_service.dart | 215 ++++++++++++++++++++++- 22 files changed, 1037 insertions(+), 218 deletions(-) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 29ab2d02..391b7e5d 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -743,15 +743,28 @@ func GetM4AQuality(filePath string) (AudioQuality, error) { return AudioQuality{}, err } - buf := make([]byte, 24) + buf := make([]byte, 32) if _, err := f.ReadAt(buf, sampleOffset); err != nil { return AudioQuality{}, fmt.Errorf("failed to read audio sample entry: %w", err) } - sampleRate := int(buf[22])<<8 | int(buf[23]) - bitDepth := 16 - if atomType == "alac" { - bitDepth = 24 + // AudioSampleEntry layout from the box type field: + // [0:4] type ("mp4a"/"alac") + // [4:10] SampleEntry.reserved + // [10:12] data_reference_index + // [12:20] reserved[8] + // [20:22] channelcount + // [22:24] samplesize (bit depth) + // [24:26] pre_defined + // [26:28] reserved + // [28:32] samplerate (16.16 fixed-point) + sampleRate := int(buf[28])<<8 | int(buf[29]) + bitDepth := int(buf[22])<<8 | int(buf[23]) + if bitDepth <= 0 { + bitDepth = 16 + if atomType == "alac" { + bitDepth = 24 + } } return AudioQuality{BitDepth: bitDepth, SampleRate: sampleRate}, nil @@ -874,7 +887,7 @@ func findAudioSampleEntry(f *os.File, start, end, fileSize int64) (int64, string if bestIdx >= 0 { absolute := readPos - int64(len(tail)) + int64(bestIdx) - if absolute+24 > fileSize { + if absolute+32 > fileSize { return 0, "", fmt.Errorf("audio info not found in M4A file") } return absolute, bestType, nil diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 01af801f..15451886 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3883,7 +3883,7 @@ abstract class AppLocalizations { /// Subtitle for convert format menu item /// /// In en, this message translates to: - /// **'Convert to MP3 or Opus'** + /// **'Convert to MP3, Opus, ALAC, or FLAC'** String get trackConvertFormatSubtitle; /// Title of convert bottom sheet @@ -3920,6 +3920,21 @@ abstract class AppLocalizations { String bitrate, ); + /// Confirmation dialog message for lossless-to-lossless conversion + /// + /// In en, this message translates to: + /// **'Convert from {sourceFormat} to {targetFormat}? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'** + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ); + + /// Hint shown when converting between lossless formats + /// + /// In en, this message translates to: + /// **'Lossless conversion — no quality loss'** + String get trackConvertLosslessHint; + /// Snackbar while converting /// /// In en, this message translates to: @@ -4290,6 +4305,12 @@ abstract class AppLocalizations { String bitrate, ); + /// Confirmation dialog message for lossless batch conversion + /// + /// In en, this message translates to: + /// **'Convert {count} {count, plural, =1{track} other{tracks}} to {format}? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'** + String selectionBatchConvertConfirmMessageLossless(int count, String format); + /// Snackbar during batch conversion progress /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c3ee5083..f0137de1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2226,6 +2226,18 @@ class AppLocalizationsDe extends AppLocalizations { return 'Konvertieren von $sourceFormat in $targetFormat bei $bitrate?\n\nDie Originaldatei wird nach der Konvertierung gelöscht.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Konvertiere Audio...'; @@ -2480,6 +2492,17 @@ class AppLocalizationsDe extends AppLocalizations { return 'Konvertiere $count $format $_temp0 zu $bitrate?\n\nOriginaldateien werden nach der Konvertierung gelöscht.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Konvertiere $current von $total...'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 180810f3..041c5bd1 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2176,7 +2176,8 @@ class AppLocalizationsEn extends AppLocalizations { String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => + 'Convert to MP3, Opus, ALAC, or FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -2199,6 +2200,18 @@ class AppLocalizationsEn extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2465,17 @@ class AppLocalizationsEn extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index cea3915b..99849885 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2176,7 +2176,8 @@ class AppLocalizationsEs extends AppLocalizations { String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => + 'Convert to MP3, Opus, ALAC, or FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -2199,6 +2200,18 @@ class AppLocalizationsEs extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2465,17 @@ class AppLocalizationsEs extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 8270c7c2..cec6eae9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2201,6 +2201,18 @@ class AppLocalizationsFr extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2454,6 +2466,17 @@ class AppLocalizationsFr extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 1767c438..9a9ad518 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2199,6 +2199,18 @@ class AppLocalizationsHi extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2464,17 @@ class AppLocalizationsHi extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 67a9c2fc..902d49cd 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2183,7 +2183,8 @@ class AppLocalizationsId extends AppLocalizations { String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => + 'Konversi ke MP3, Opus, ALAC, atau FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -2206,6 +2207,18 @@ class AppLocalizationsId extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Konversi dari $sourceFormat ke $targetFormat? (Lossless — tanpa kehilangan kualitas)\n\nFile asli akan dihapus setelah konversi.'; + } + + @override + String get trackConvertLosslessHint => + 'Konversi lossless — tanpa kehilangan kualitas'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2459,6 +2472,17 @@ class AppLocalizationsId extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index e406ba35..31270670 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2186,6 +2186,18 @@ class AppLocalizationsJa extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'オーディオを変換中...'; @@ -2439,6 +2451,17 @@ class AppLocalizationsJa extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index b8085a70..76f1b737 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2179,6 +2179,18 @@ class AppLocalizationsKo extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2432,6 +2444,17 @@ class AppLocalizationsKo extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index c54e044a..d509e1e7 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2199,6 +2199,18 @@ class AppLocalizationsNl extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2464,17 @@ class AppLocalizationsNl extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ac0f577d..ef87eca7 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2176,7 +2176,8 @@ class AppLocalizationsPt extends AppLocalizations { String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => + 'Convert to MP3, Opus, ALAC, or FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -2199,6 +2200,18 @@ class AppLocalizationsPt extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2465,17 @@ class AppLocalizationsPt extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 76814d38..8bbb8a74 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2252,6 +2252,18 @@ class AppLocalizationsRu extends AppLocalizations { return 'Конвертировать из $sourceFormat в $targetFormat $bitrate?\n\nОригинальный файл будет удален после конвертации.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Конвертация аудио...'; @@ -2511,6 +2523,17 @@ class AppLocalizationsRu extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Конвертация $current из $total...'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index f24d9f88..bbf6ef14 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2211,6 +2211,18 @@ class AppLocalizationsTr extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2464,6 +2476,17 @@ class AppLocalizationsTr extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index d9440cff..465f196c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2176,7 +2176,8 @@ class AppLocalizationsZh extends AppLocalizations { String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => + 'Convert to MP3, Opus, ALAC, or FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -2199,6 +2200,18 @@ class AppLocalizationsZh extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2465,17 @@ class AppLocalizationsZh extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6da406d0..7f4e1acb 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2873,7 +2873,7 @@ "@trackConvertFormat": { "description": "Menu item - convert audio format" }, - "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "trackConvertFormatSubtitle": "Convert to MP3, Opus, ALAC, or FLAC", "@trackConvertFormatSubtitle": { "description": "Subtitle for convert format menu item" }, @@ -2908,6 +2908,22 @@ } } }, + "trackConvertConfirmMessageLossless": "Convert from {sourceFormat} to {targetFormat}? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless-to-lossless conversion", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + } + } + }, + "trackConvertLosslessHint": "Lossless conversion — no quality loss", + "@trackConvertLosslessHint": { + "description": "Hint shown when converting between lossless formats" + }, "trackConvertConverting": "Converting audio...", "@trackConvertConverting": { "description": "Snackbar while converting" @@ -3259,6 +3275,18 @@ } } }, + "selectionBatchConvertConfirmMessageLossless": "Convert {count} {count, plural, =1{track} other{tracks}} to {format}? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.", + "@selectionBatchConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless batch conversion", + "placeholders": { + "count": { + "type": "int" + }, + "format": { + "type": "String" + } + } + }, "selectionBatchConvertProgress": "Converting {current} of {total}...", "@selectionBatchConvertProgress": { "description": "Snackbar during batch conversion progress", diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index bab66bf2..ff764ec8 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -2809,7 +2809,7 @@ "@trackConvertFormat": { "description": "Menu item - convert audio format" }, - "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "trackConvertFormatSubtitle": "Konversi ke MP3, Opus, ALAC, atau FLAC", "@trackConvertFormatSubtitle": { "description": "Subtitle for convert format menu item" }, @@ -2844,6 +2844,22 @@ } } }, + "trackConvertConfirmMessageLossless": "Konversi dari {sourceFormat} ke {targetFormat}? (Lossless — tanpa kehilangan kualitas)\n\nFile asli akan dihapus setelah konversi.", + "@trackConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless-to-lossless conversion", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + } + } + }, + "trackConvertLosslessHint": "Konversi lossless — tanpa kehilangan kualitas", + "@trackConvertLosslessHint": { + "description": "Hint shown when converting between lossless formats" + }, "trackConvertConverting": "Converting audio...", "@trackConvertConverting": { "description": "Snackbar while converting" diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 55b1b71c..f320aa40 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -912,6 +912,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { ) { String selectedFormat = 'MP3'; String selectedBitrate = '320k'; + bool isLosslessTarget = false; showModalBottomSheet( context: context, @@ -923,7 +924,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['MP3', 'Opus']; + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( @@ -960,51 +961,75 @@ class _DownloadedAlbumScreenState extends ConsumerState { ), ), const SizedBox(height: 8), - Row( - children: formats.map((format) { - final isSelected = format == selectedFormat; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(format), - selected: isSelected, - onSelected: (selected) { - if (selected) { - setSheetState(() { - selectedFormat = format; - selectedBitrate = format == 'Opus' - ? '128k' - : '320k'; - }); - } - }, - ), - ); - }).toList(), - ), - const SizedBox(height: 16), - Text( - context.l10n.trackConvertBitrate, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), Wrap( spacing: 8, - children: bitrates.map((br) { - final isSelected = br == selectedBitrate; + children: formats.map((format) { + final isSelected = format == selectedFormat; return ChoiceChip( - label: Text(br), + label: Text(format), selected: isSelected, onSelected: (selected) { if (selected) { - setSheetState(() => selectedBitrate = br); + setSheetState(() { + selectedFormat = format; + isLosslessTarget = + format == 'ALAC' || format == 'FLAC'; + if (!isLosslessTarget) { + selectedBitrate = + format == 'Opus' ? '128k' : '320k'; + } + }); } }, ); }).toList(), ), + if (!isLosslessTarget) ...[ + const SizedBox(height: 16), + Text( + context.l10n.trackConvertBitrate, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: bitrates.map((br) { + final isSelected = br == selectedBitrate; + return ChoiceChip( + label: Text(br), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setSheetState(() => selectedBitrate = br); + } + }, + ); + }).toList(), + ), + ], + if (isLosslessTarget) ...[ + const SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.verified, + size: 16, + color: colorScheme.primary, + ), + const SizedBox(width: 6), + Text( + context.l10n.trackConvertLosslessHint, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + ), + ), + ], + ), + ], const SizedBox(height: 24), SizedBox( width: double.infinity, @@ -1057,12 +1082,19 @@ class _DownloadedAlbumScreenState extends ConsumerState { : item.filePath.toLowerCase(); final ext = nameToCheck.endsWith('.flac') ? 'FLAC' + : nameToCheck.endsWith('.m4a') + ? 'M4A' : nameToCheck.endsWith('.mp3') ? 'MP3' : (nameToCheck.endsWith('.opus') || nameToCheck.endsWith('.ogg')) ? 'Opus' : null; - if (ext != null && ext != targetFormat) selected.add(item); + if (ext == null || ext == targetFormat) continue; + // Skip lossy sources when target is lossless (pointless re-encoding) + final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; + final isLosslessSource = ext == 'FLAC' || ext == 'M4A'; + if (isLosslessTarget && !isLosslessSource) continue; + selected.add(item); } if (selected.isEmpty) { @@ -1074,16 +1106,22 @@ class _DownloadedAlbumScreenState extends ConsumerState { return; } + final isLossless = targetFormat == 'ALAC' || targetFormat == 'FLAC'; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(context.l10n.selectionBatchConvertConfirmTitle), content: Text( - context.l10n.selectionBatchConvertConfirmMessage( - selected.length, - targetFormat, - bitrate, - ), + isLossless + ? context.l10n.selectionBatchConvertConfirmMessageLossless( + selected.length, + targetFormat, + ) + : context.l10n.selectionBatchConvertConfirmMessage( + selected.length, + targetFormat, + bitrate, + ), ), actions: [ TextButton( @@ -1103,8 +1141,10 @@ class _DownloadedAlbumScreenState extends ConsumerState { int successCount = 0; final total = selected.length; final historyDb = HistoryDatabase.instance; - final newQuality = - '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; + final newQuality = (targetFormat.toUpperCase() == 'ALAC' || + targetFormat.toUpperCase() == 'FLAC') + ? '${targetFormat.toUpperCase()} Lossless' + : '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; final settings = ref.read(settingsProvider); final shouldEmbedLyrics = settings.embedLyrics && settings.lyricsMode != 'external'; @@ -1207,13 +1247,27 @@ class _DownloadedAlbumScreenState extends ConsumerState { final baseName = dotIdx > 0 ? oldFileName.substring(0, dotIdx) : oldFileName; - final newExt = targetFormat.toLowerCase() == 'opus' - ? '.opus' - : '.mp3'; + String newExt; + String mimeType; + switch (targetFormat.toLowerCase()) { + case 'opus': + newExt = '.opus'; + mimeType = 'audio/opus'; + break; + case 'alac': + newExt = '.m4a'; + mimeType = 'audio/mp4'; + break; + case 'flac': + newExt = '.flac'; + mimeType = 'audio/flac'; + break; + default: + newExt = '.mp3'; + mimeType = 'audio/mpeg'; + break; + } final newFileName = '$baseName$newExt'; - final mimeType = targetFormat.toLowerCase() == 'opus' - ? 'audio/opus' - : 'audio/mpeg'; final safUri = await PlatformBridge.createSafFileFromPath( treeUri: treeUri, diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 122a4bab..1b662324 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -1131,6 +1131,7 @@ class _LocalAlbumScreenState extends ConsumerState { ) { String selectedFormat = 'MP3'; String selectedBitrate = '320k'; + bool isLosslessTarget = false; showModalBottomSheet( context: context, @@ -1142,7 +1143,7 @@ class _LocalAlbumScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['MP3', 'Opus']; + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( @@ -1179,51 +1180,75 @@ class _LocalAlbumScreenState extends ConsumerState { ), ), const SizedBox(height: 8), - Row( - children: formats.map((format) { - final isSelected = format == selectedFormat; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(format), - selected: isSelected, - onSelected: (selected) { - if (selected) { - setSheetState(() { - selectedFormat = format; - selectedBitrate = format == 'Opus' - ? '128k' - : '320k'; - }); - } - }, - ), - ); - }).toList(), - ), - const SizedBox(height: 16), - Text( - context.l10n.trackConvertBitrate, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), Wrap( spacing: 8, - children: bitrates.map((br) { - final isSelected = br == selectedBitrate; + children: formats.map((format) { + final isSelected = format == selectedFormat; return ChoiceChip( - label: Text(br), + label: Text(format), selected: isSelected, onSelected: (selected) { if (selected) { - setSheetState(() => selectedBitrate = br); + setSheetState(() { + selectedFormat = format; + isLosslessTarget = + format == 'ALAC' || format == 'FLAC'; + if (!isLosslessTarget) { + selectedBitrate = + format == 'Opus' ? '128k' : '320k'; + } + }); } }, ); }).toList(), ), + if (!isLosslessTarget) ...[ + const SizedBox(height: 16), + Text( + context.l10n.trackConvertBitrate, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: bitrates.map((br) { + final isSelected = br == selectedBitrate; + return ChoiceChip( + label: Text(br), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setSheetState(() => selectedBitrate = br); + } + }, + ); + }).toList(), + ), + ], + if (isLosslessTarget) ...[ + const SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.verified, + size: 16, + color: colorScheme.primary, + ), + const SizedBox(width: 6), + Text( + context.l10n.trackConvertLosslessHint, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + ), + ), + ], + ), + ], const SizedBox(height: 24), SizedBox( width: double.infinity, @@ -1276,6 +1301,8 @@ class _LocalAlbumScreenState extends ConsumerState { final fmt = item.format!.toLowerCase(); if (fmt == 'flac') { currentFormat = 'FLAC'; + } else if (fmt == 'm4a') { + currentFormat = 'M4A'; } else if (fmt == 'mp3') { currentFormat = 'MP3'; } else if (fmt == 'opus' || fmt == 'ogg') { @@ -1287,15 +1314,20 @@ class _LocalAlbumScreenState extends ConsumerState { final lower = item.filePath.toLowerCase(); if (lower.endsWith('.flac')) { currentFormat = 'FLAC'; + } else if (lower.endsWith('.m4a')) { + currentFormat = 'M4A'; } else if (lower.endsWith('.mp3')) { currentFormat = 'MP3'; } else if (lower.endsWith('.opus') || lower.endsWith('.ogg')) { currentFormat = 'Opus'; } } - if (currentFormat != null && currentFormat != targetFormat) { - selected.add(item); - } + if (currentFormat == null || currentFormat == targetFormat) continue; + // Skip lossy sources when target is lossless (pointless re-encoding) + final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; + final isLosslessSource = currentFormat == 'FLAC' || currentFormat == 'M4A'; + if (isLosslessTarget && !isLosslessSource) continue; + selected.add(item); } if (selected.isEmpty) { @@ -1307,16 +1339,22 @@ class _LocalAlbumScreenState extends ConsumerState { return; } + final isLossless = targetFormat == 'ALAC' || targetFormat == 'FLAC'; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(context.l10n.selectionBatchConvertConfirmTitle), content: Text( - context.l10n.selectionBatchConvertConfirmMessage( - selected.length, - targetFormat, - bitrate, - ), + isLossless + ? context.l10n.selectionBatchConvertConfirmMessageLossless( + selected.length, + targetFormat, + ) + : context.l10n.selectionBatchConvertConfirmMessage( + selected.length, + targetFormat, + bitrate, + ), ), actions: [ TextButton( @@ -1481,13 +1519,27 @@ class _LocalAlbumScreenState extends ConsumerState { final baseName = dotIdx > 0 ? oldFileName.substring(0, dotIdx) : oldFileName; - final newExt = targetFormat.toLowerCase() == 'opus' - ? '.opus' - : '.mp3'; + String newExt; + String mimeType; + switch (targetFormat.toLowerCase()) { + case 'opus': + newExt = '.opus'; + mimeType = 'audio/opus'; + break; + case 'alac': + newExt = '.m4a'; + mimeType = 'audio/mp4'; + break; + case 'flac': + newExt = '.flac'; + mimeType = 'audio/flac'; + break; + default: + newExt = '.mp3'; + mimeType = 'audio/mpeg'; + break; + } final newFileName = '$baseName$newExt'; - final mimeType = targetFormat.toLowerCase() == 'opus' - ? 'audio/opus' - : 'audio/mpeg'; final safUri = await PlatformBridge.createSafFileFromPath( treeUri: treeUri, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 52ca3309..64f9c0e3 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4757,6 +4757,7 @@ class _QueueTabState extends ConsumerState { ) async { String selectedFormat = 'MP3'; String selectedBitrate = '320k'; + bool isLosslessTarget = false; var didStartConversion = false; _hideSelectionOverlay(); @@ -4772,7 +4773,7 @@ class _QueueTabState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['MP3', 'Opus']; + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( @@ -4809,51 +4810,75 @@ class _QueueTabState extends ConsumerState { ), ), const SizedBox(height: 8), - Row( - children: formats.map((format) { - final isSelected = format == selectedFormat; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(format), - selected: isSelected, - onSelected: (selected) { - if (selected) { - setSheetState(() { - selectedFormat = format; - selectedBitrate = format == 'Opus' - ? '128k' - : '320k'; - }); - } - }, - ), - ); - }).toList(), - ), - const SizedBox(height: 16), - Text( - context.l10n.trackConvertBitrate, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), Wrap( spacing: 8, - children: bitrates.map((br) { - final isSelected = br == selectedBitrate; + children: formats.map((format) { + final isSelected = format == selectedFormat; return ChoiceChip( - label: Text(br), + label: Text(format), selected: isSelected, onSelected: (selected) { if (selected) { - setSheetState(() => selectedBitrate = br); + setSheetState(() { + selectedFormat = format; + isLosslessTarget = + format == 'ALAC' || format == 'FLAC'; + if (!isLosslessTarget) { + selectedBitrate = + format == 'Opus' ? '128k' : '320k'; + } + }); } }, ); }).toList(), ), + if (!isLosslessTarget) ...[ + const SizedBox(height: 16), + Text( + context.l10n.trackConvertBitrate, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: bitrates.map((br) { + final isSelected = br == selectedBitrate; + return ChoiceChip( + label: Text(br), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setSheetState(() => selectedBitrate = br); + } + }, + ); + }).toList(), + ), + ], + if (isLosslessTarget) ...[ + const SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.verified, + size: 16, + color: colorScheme.primary, + ), + const SizedBox(width: 6), + Text( + context.l10n.trackConvertLosslessHint, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + ), + ), + ], + ), + ], const SizedBox(height: 24), SizedBox( width: double.infinity, @@ -4929,14 +4954,19 @@ class _QueueTabState extends ConsumerState { } final ext = nameToCheck.endsWith('.flac') ? 'FLAC' + : nameToCheck.endsWith('.m4a') + ? 'M4A' : nameToCheck.endsWith('.mp3') ? 'MP3' : (nameToCheck.endsWith('.opus') || nameToCheck.endsWith('.ogg')) ? 'Opus' : null; - if (ext != null && ext != targetFormat) { - selectedItems.add(item); - } + if (ext == null || ext == targetFormat) continue; + // Skip lossy sources when target is lossless (pointless re-encoding) + final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; + final isLosslessSource = ext == 'FLAC' || ext == 'M4A'; + if (isLosslessTarget && !isLosslessSource) continue; + selectedItems.add(item); } if (selectedItems.isEmpty) { @@ -4949,16 +4979,22 @@ class _QueueTabState extends ConsumerState { } // Confirm + final isLossless = targetFormat == 'ALAC' || targetFormat == 'FLAC'; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(context.l10n.selectionBatchConvertConfirmTitle), content: Text( - context.l10n.selectionBatchConvertConfirmMessage( - selectedItems.length, - targetFormat, - bitrate, - ), + isLossless + ? context.l10n.selectionBatchConvertConfirmMessageLossless( + selectedItems.length, + targetFormat, + ) + : context.l10n.selectionBatchConvertConfirmMessage( + selectedItems.length, + targetFormat, + bitrate, + ), ), actions: [ TextButton( @@ -4978,8 +5014,10 @@ class _QueueTabState extends ConsumerState { int successCount = 0; final total = selectedItems.length; final historyDb = HistoryDatabase.instance; - final newQuality = - '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; + final newQuality = (targetFormat.toUpperCase() == 'ALAC' || + targetFormat.toUpperCase() == 'FLAC') + ? '${targetFormat.toUpperCase()} Lossless' + : '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; final settings = ref.read(settingsProvider); final shouldEmbedLyrics = settings.embedLyrics && settings.lyricsMode != 'external'; @@ -5093,13 +5131,27 @@ class _QueueTabState extends ConsumerState { final baseName = dotIdx > 0 ? oldFileName.substring(0, dotIdx) : oldFileName; - final newExt = targetFormat.toLowerCase() == 'opus' - ? '.opus' - : '.mp3'; + String newExt; + String mimeType; + switch (targetFormat.toLowerCase()) { + case 'opus': + newExt = '.opus'; + mimeType = 'audio/opus'; + break; + case 'alac': + newExt = '.m4a'; + mimeType = 'audio/mp4'; + break; + case 'flac': + newExt = '.flac'; + mimeType = 'audio/flac'; + break; + default: + newExt = '.mp3'; + mimeType = 'audio/mpeg'; + break; + } final newFileName = '$baseName$newExt'; - final mimeType = targetFormat.toLowerCase() == 'opus' - ? 'audio/opus' - : 'audio/mpeg'; final safUri = await PlatformBridge.createSafFileFromPath( treeUri: treeUri, diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 85e803b6..744acf54 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -2662,6 +2662,7 @@ class _TrackMetadataScreenState extends ConsumerState { bool get _isConvertibleFormat { final lower = cleanFilePath.toLowerCase(); return lower.endsWith('.flac') || + lower.endsWith('.m4a') || lower.endsWith('.mp3') || lower.endsWith('.opus') || lower.endsWith('.ogg'); @@ -2692,6 +2693,7 @@ class _TrackMetadataScreenState extends ConsumerState { } final lower = cleanFilePath.toLowerCase(); if (lower.endsWith('.flac')) return 'FLAC'; + if (lower.endsWith('.m4a')) return 'M4A'; if (lower.endsWith('.mp3')) return 'MP3'; if (lower.endsWith('.opus') || lower.endsWith('.ogg')) return 'Opus'; if (lower.endsWith('.cue')) return 'CUE'; @@ -2749,8 +2751,12 @@ class _TrackMetadataScreenState extends ConsumerState { } String _buildConvertedQualityLabel(String targetFormat, String bitrate) { + final upper = targetFormat.toUpperCase(); + if (upper == 'ALAC' || upper == 'FLAC') { + return '$upper Lossless'; + } final normalizedBitrate = bitrate.trim().toLowerCase(); - return '${targetFormat.toUpperCase()} $normalizedBitrate'; + return '$upper $normalizedBitrate'; } String? _extractLossyBitrateLabel(String? quality) { @@ -2790,17 +2796,27 @@ class _TrackMetadataScreenState extends ConsumerState { void _showConvertSheet(BuildContext context) { final currentFormat = _currentFileFormat; - // Available target formats (exclude current) - final formats = [ - 'MP3', - 'Opus', - ].where((f) => f != currentFormat).toList(); + final isLosslessSource = + currentFormat == 'FLAC' || currentFormat == 'M4A'; + + // Build available target formats based on source + final formats = []; if (currentFormat == 'FLAC') { - // FLAC can convert to both + formats.addAll(['ALAC', 'MP3', 'Opus']); + } else if (currentFormat == 'M4A') { + formats.addAll(['FLAC', 'MP3', 'Opus']); + } else if (currentFormat == 'MP3') { + formats.add('Opus'); + } else if (currentFormat == 'Opus') { + formats.add('MP3'); + } else { + formats.addAll(['MP3', 'Opus']); } String selectedFormat = formats.first; String selectedBitrate = selectedFormat == 'Opus' ? '128k' : '320k'; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; showModalBottomSheet( context: context, @@ -2849,53 +2865,79 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), const SizedBox(height: 8), - Row( - children: formats.map((format) { - final isSelected = format == selectedFormat; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(format), - selected: isSelected, - onSelected: (selected) { - if (selected) { - setSheetState(() { - selectedFormat = format; - // Reset bitrate to default for format - selectedBitrate = format == 'Opus' - ? '128k' - : '320k'; - }); - } - }, - ), - ); - }).toList(), - ), - const SizedBox(height: 16), - - Text( - context.l10n.trackConvertBitrate, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), Wrap( spacing: 8, - children: bitrates.map((br) { - final isSelected = br == selectedBitrate; + children: formats.map((format) { + final isSelected = format == selectedFormat; return ChoiceChip( - label: Text(br), + label: Text(format), selected: isSelected, onSelected: (selected) { if (selected) { - setSheetState(() => selectedBitrate = br); + setSheetState(() { + selectedFormat = format; + isLosslessTarget = + format == 'ALAC' || format == 'FLAC'; + if (!isLosslessTarget) { + selectedBitrate = + format == 'Opus' ? '128k' : '320k'; + } + }); } }, ); }).toList(), ), + + // Only show bitrate for lossy targets + if (!isLosslessTarget) ...[ + const SizedBox(height: 16), + Text( + context.l10n.trackConvertBitrate, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: bitrates.map((br) { + final isSelected = br == selectedBitrate; + return ChoiceChip( + label: Text(br), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setSheetState(() => selectedBitrate = br); + } + }, + ); + }).toList(), + ), + ], + + // Show lossless indicator + if (isLosslessTarget && isLosslessSource) ...[ + const SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.verified, + size: 16, + color: colorScheme.primary, + ), + const SizedBox(width: 6), + Text( + context.l10n.trackConvertLosslessHint, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + ), + ), + ], + ), + ], const SizedBox(height: 24), SizedBox( @@ -2917,7 +2959,9 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), child: Text( - '$currentFormat -> $selectedFormat @ $selectedBitrate', + isLosslessTarget + ? '$currentFormat -> $selectedFormat (Lossless)' + : '$currentFormat -> $selectedFormat @ $selectedBitrate', ), ), ), @@ -3402,17 +3446,25 @@ class _TrackMetadataScreenState extends ConsumerState { required String targetFormat, required String bitrate, }) { + final isLossless = + targetFormat.toUpperCase() == 'ALAC' || + targetFormat.toUpperCase() == 'FLAC'; showDialog( context: context, builder: (dialogContext) { return AlertDialog( title: Text(dialogContext.l10n.trackConvertConfirmTitle), content: Text( - dialogContext.l10n.trackConvertConfirmMessage( - sourceFormat, - targetFormat, - bitrate, - ), + isLossless + ? dialogContext.l10n.trackConvertConfirmMessageLossless( + sourceFormat, + targetFormat, + ) + : dialogContext.l10n.trackConvertConfirmMessage( + sourceFormat, + targetFormat, + bitrate, + ), ), actions: [ TextButton( @@ -3561,11 +3613,27 @@ class _TrackMetadataScreenState extends ConsumerState { final baseName = dotIdx > 0 ? oldFileName.substring(0, dotIdx) : oldFileName; - final newExt = targetFormat.toLowerCase() == 'opus' ? '.opus' : '.mp3'; + String newExt; + String mimeType; + switch (targetFormat.toLowerCase()) { + case 'opus': + newExt = '.opus'; + mimeType = 'audio/opus'; + break; + case 'alac': + newExt = '.m4a'; + mimeType = 'audio/mp4'; + break; + case 'flac': + newExt = '.flac'; + mimeType = 'audio/flac'; + break; + default: // mp3 + newExt = '.mp3'; + mimeType = 'audio/mpeg'; + break; + } final newFileName = '$baseName$newExt'; - final mimeType = targetFormat.toLowerCase() == 'opus' - ? 'audio/opus' - : 'audio/mpeg'; final safUri = await PlatformBridge.createSafFileFromPath( treeUri: treeUri, diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index b9aa6603..cb533cb0 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1209,7 +1209,8 @@ class FFmpegService { } /// Unified audio format conversion with full metadata + cover preservation. - /// Supports: FLAC/MP3/Opus -> MP3/Opus (any direction except same format). + /// Supports: FLAC/M4A/MP3/Opus -> MP3/Opus/ALAC/FLAC. + /// ALAC and FLAC targets are lossless (bitrate parameter is ignored). /// Returns the new file path on success, null on failure. static Future convertAudioFormat({ required String inputPath, @@ -1220,11 +1221,30 @@ class FFmpegService { bool deleteOriginal = true, }) async { final format = targetFormat.toLowerCase(); - if (format != 'mp3' && format != 'opus') { + if (!const {'mp3', 'opus', 'alac', 'flac'}.contains(format)) { _log.e('Unsupported target format: $targetFormat'); return null; } + // Lossless targets: dedicated single-pass methods + if (format == 'alac') { + return _convertToAlac( + inputPath: inputPath, + metadata: metadata, + coverPath: coverPath, + deleteOriginal: deleteOriginal, + ); + } + if (format == 'flac') { + return _convertToFlac( + inputPath: inputPath, + metadata: metadata, + coverPath: coverPath, + deleteOriginal: deleteOriginal, + ); + } + + // Lossy targets: MP3 / Opus final extension = format == 'opus' ? '.opus' : '.mp3'; final outputPath = _buildOutputPath(inputPath, extension); @@ -1296,6 +1316,197 @@ class FFmpegService { return outputPath; } + /// Convert any audio format to ALAC (Apple Lossless) in an M4A container. + /// Metadata and cover art are embedded in a single FFmpeg pass. + static Future _convertToAlac({ + required String inputPath, + required Map metadata, + String? coverPath, + bool deleteOriginal = true, + }) async { + final outputPath = _buildOutputPath(inputPath, '.m4a'); + + final cmdBuffer = StringBuffer(); + cmdBuffer.write('-i "$inputPath" '); + + // Cover art as second input for M4A attached picture + final hasCover = coverPath != null && + coverPath.trim().isNotEmpty && + await File(coverPath).exists(); + if (hasCover) { + cmdBuffer.write('-i "$coverPath" '); + } + + cmdBuffer.write('-map 0:a '); + if (hasCover) { + cmdBuffer.write('-map 1:v -c:v copy -disposition:v:0 attached_pic '); + } + cmdBuffer.write('-c:a alac '); + cmdBuffer.write('-map_metadata -1 '); + + // Embed M4A metadata tags + final m4aTags = _convertToM4aTags(metadata); + for (final entry in m4aTags.entries) { + final sanitized = entry.value.replaceAll('"', '\\"'); + cmdBuffer.write('-metadata ${entry.key}="$sanitized" '); + } + + cmdBuffer.write('"$outputPath" -y'); + + _log.i( + 'Converting ${inputPath.split(Platform.pathSeparator).last} to ALAC', + ); + final result = await _execute(cmdBuffer.toString()); + + if (!result.success) { + _log.e('ALAC conversion failed: ${result.output}'); + return null; + } + + if (deleteOriginal) { + try { + await File(inputPath).delete(); + _log.i( + 'Deleted original: ${inputPath.split(Platform.pathSeparator).last}', + ); + } catch (e) { + _log.w('Failed to delete original: $e'); + } + } + + return outputPath; + } + + /// Convert any audio format to FLAC. + /// Metadata (Vorbis comments) and cover art (METADATA_BLOCK_PICTURE) are + /// embedded in a single FFmpeg pass. + static Future _convertToFlac({ + required String inputPath, + required Map metadata, + String? coverPath, + bool deleteOriginal = true, + }) async { + final outputPath = _buildOutputPath(inputPath, '.flac'); + + final cmdBuffer = StringBuffer(); + cmdBuffer.write('-i "$inputPath" '); + cmdBuffer.write('-map 0:a '); + cmdBuffer.write('-c:a flac -compression_level 8 '); + cmdBuffer.write('-map_metadata -1 '); + + // Embed Vorbis comments + for (final entry in metadata.entries) { + if (entry.value.trim().isEmpty) continue; + final sanitized = entry.value.replaceAll('"', '\\"'); + cmdBuffer.write('-metadata ${entry.key}="$sanitized" '); + } + + // Embed cover art via METADATA_BLOCK_PICTURE (same approach as Opus) + if (coverPath != null && coverPath.trim().isNotEmpty) { + try { + if (await File(coverPath).exists()) { + final pictureBlock = await _createMetadataBlockPicture(coverPath); + if (pictureBlock != null) { + final escapedBlock = pictureBlock.replaceAll('"', '\\"'); + cmdBuffer.write( + '-metadata METADATA_BLOCK_PICTURE="$escapedBlock" ', + ); + _log.d( + 'Created METADATA_BLOCK_PICTURE for FLAC (${pictureBlock.length} chars)', + ); + } + } + } catch (e) { + _log.e('Error creating METADATA_BLOCK_PICTURE for FLAC: $e'); + } + } + + cmdBuffer.write('"$outputPath" -y'); + + _log.i( + 'Converting ${inputPath.split(Platform.pathSeparator).last} to FLAC', + ); + final result = await _execute(cmdBuffer.toString()); + + if (!result.success) { + _log.e('FLAC conversion failed: ${result.output}'); + return null; + } + + if (deleteOriginal) { + try { + await File(inputPath).delete(); + _log.i( + 'Deleted original: ${inputPath.split(Platform.pathSeparator).last}', + ); + } catch (e) { + _log.w('Failed to delete original: $e'); + } + } + + return outputPath; + } + + /// Map Vorbis comment keys to M4A/MP4 metadata tag names for FFmpeg. + static Map _convertToM4aTags( + Map metadata, + ) { + final m4aMap = {}; + + for (final entry in metadata.entries) { + final key = entry.key.toUpperCase().replaceAll(RegExp(r'[^A-Z0-9]'), ''); + final value = entry.value; + if (value.trim().isEmpty) continue; + + switch (key) { + case 'TITLE': + m4aMap['title'] = value; + break; + case 'ARTIST': + m4aMap['artist'] = value; + break; + case 'ALBUM': + m4aMap['album'] = value; + break; + case 'ALBUMARTIST': + m4aMap['album_artist'] = value; + break; + case 'TRACKNUMBER': + case 'TRACK': + case 'TRCK': + m4aMap['track'] = value; + break; + case 'DISCNUMBER': + case 'DISC': + case 'TPOS': + m4aMap['disc'] = value; + break; + case 'DATE': + case 'YEAR': + m4aMap['date'] = value; + break; + case 'GENRE': + m4aMap['genre'] = value; + break; + case 'COMPOSER': + m4aMap['composer'] = value; + break; + case 'COMMENT': + m4aMap['comment'] = value; + break; + case 'COPYRIGHT': + m4aMap['copyright'] = value; + break; + case 'LYRICS': + case 'UNSYNCEDLYRICS': + m4aMap['lyrics'] = value; + break; + } + } + + return m4aMap; + } + static Map _convertToId3Tags( Map vorbisMetadata, ) { From 554fe08fcd14107bd18138d8f062cc7a6c82acf6 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:26:53 +0700 Subject: [PATCH 069/145] fix: preserve metadata and cover art in ALAC/M4A to FLAC conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use -map_metadata 0 instead of -map_metadata -1 so FFmpeg copies and auto-remaps source tags (M4A/ID3 → Vorbis comments) as a base - Add _normalizeToVorbisComments() to filter technical fields (BIT_DEPTH, SAMPLE_RATE, DURATION) and normalize key variations to standard Vorbis comment names before applying overrides - Switch cover art embedding from METADATA_BLOCK_PICTURE base64 (unreliable on Android due to command-line length limits) to -i cover -map 1:v -disposition attached_pic (same proven approach as embedMetadata and _convertToAlac) - Drop zero-value track/disc numbers from override map to prevent clobbering source metadata with '0' from Go readFileMetadata --- lib/services/ffmpeg_service.dart | 129 ++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 28 deletions(-) diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index cb533cb0..dfb88847 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1378,8 +1378,11 @@ class FFmpegService { } /// Convert any audio format to FLAC. - /// Metadata (Vorbis comments) and cover art (METADATA_BLOCK_PICTURE) are - /// embedded in a single FFmpeg pass. + /// Source metadata is preserved via -map_metadata 0 (FFmpeg auto-remaps + /// tag names between container formats), then explicit Vorbis comment + /// overrides are applied from the [metadata] map. + /// Cover art is embedded via a second input stream (same approach as + /// [embedMetadata] and [_convertToAlac]). static Future _convertToFlac({ required String inputPath, required Map metadata, @@ -1390,35 +1393,32 @@ class FFmpegService { final cmdBuffer = StringBuffer(); cmdBuffer.write('-i "$inputPath" '); - cmdBuffer.write('-map 0:a '); - cmdBuffer.write('-c:a flac -compression_level 8 '); - cmdBuffer.write('-map_metadata -1 '); - // Embed Vorbis comments - for (final entry in metadata.entries) { - if (entry.value.trim().isEmpty) continue; - final sanitized = entry.value.replaceAll('"', '\\"'); - cmdBuffer.write('-metadata ${entry.key}="$sanitized" '); + // Cover art as second input for attached picture + final hasCover = coverPath != null && + coverPath.trim().isNotEmpty && + await File(coverPath).exists(); + if (hasCover) { + cmdBuffer.write('-i "$coverPath" '); } - // Embed cover art via METADATA_BLOCK_PICTURE (same approach as Opus) - if (coverPath != null && coverPath.trim().isNotEmpty) { - try { - if (await File(coverPath).exists()) { - final pictureBlock = await _createMetadataBlockPicture(coverPath); - if (pictureBlock != null) { - final escapedBlock = pictureBlock.replaceAll('"', '\\"'); - cmdBuffer.write( - '-metadata METADATA_BLOCK_PICTURE="$escapedBlock" ', - ); - _log.d( - 'Created METADATA_BLOCK_PICTURE for FLAC (${pictureBlock.length} chars)', - ); - } - } - } catch (e) { - _log.e('Error creating METADATA_BLOCK_PICTURE for FLAC: $e'); - } + cmdBuffer.write('-map 0:a '); + if (hasCover) { + cmdBuffer.write('-map 1:v -c:v copy -disposition:v:0 attached_pic '); + cmdBuffer.write('-metadata:s:v title="Album cover" '); + cmdBuffer.write('-metadata:s:v comment="Cover (front)" '); + } + cmdBuffer.write('-c:a flac -compression_level 8 '); + + // Copy source metadata as base (FFmpeg auto-remaps M4A/ID3 tags to + // Vorbis comment names), then override with our explicit values. + cmdBuffer.write('-map_metadata 0 '); + + // Apply normalized Vorbis comment overrides + final vorbisComments = _normalizeToVorbisComments(metadata); + for (final entry in vorbisComments.entries) { + final sanitized = entry.value.replaceAll('"', '\\"'); + cmdBuffer.write('-metadata ${entry.key}="$sanitized" '); } cmdBuffer.write('"$outputPath" -y'); @@ -1447,6 +1447,79 @@ class FFmpegService { return outputPath; } + /// Normalize metadata keys to standard Vorbis comment names and filter out + /// technical/non-tag fields (bit_depth, sample_rate, duration, etc.). + static Map _normalizeToVorbisComments( + Map metadata, + ) { + final vorbis = {}; + + for (final entry in metadata.entries) { + final key = entry.key.toUpperCase().replaceAll(RegExp(r'[^A-Z0-9]'), ''); + final value = entry.value; + if (value.trim().isEmpty) continue; + + switch (key) { + case 'TITLE': + vorbis['TITLE'] = value; + break; + case 'ARTIST': + vorbis['ARTIST'] = value; + break; + case 'ALBUM': + vorbis['ALBUM'] = value; + break; + case 'ALBUMARTIST': + vorbis['ALBUMARTIST'] = value; + break; + case 'TRACKNUMBER': + case 'TRACKNBR': + case 'TRACK': + case 'TRCK': + if (value != '0') vorbis['TRACKNUMBER'] = value; + break; + case 'DISCNUMBER': + case 'DISC': + case 'TPOS': + if (value != '0') vorbis['DISCNUMBER'] = value; + break; + case 'DATE': + case 'YEAR': + vorbis['DATE'] = value; + break; + case 'GENRE': + vorbis['GENRE'] = value; + break; + case 'ISRC': + vorbis['ISRC'] = value; + break; + case 'LABEL': + case 'ORGANIZATION': + vorbis['ORGANIZATION'] = value; + break; + case 'COPYRIGHT': + vorbis['COPYRIGHT'] = value; + break; + case 'COMPOSER': + vorbis['COMPOSER'] = value; + break; + case 'COMMENT': + vorbis['COMMENT'] = value; + break; + case 'LYRICS': + case 'UNSYNCEDLYRICS': + // Write both keys for compatibility with different FLAC readers + vorbis['LYRICS'] = value; + vorbis['UNSYNCEDLYRICS'] = value; + break; + // Technical fields (BIT_DEPTH, SAMPLE_RATE, DURATION, etc.) are + // intentionally dropped — they are not Vorbis comment tags. + } + } + + return vorbis; + } + /// Map Vorbis comment keys to M4A/MP4 metadata tag names for FFmpeg. static Map _convertToM4aTags( Map metadata, From 4adaed8da08ee9361ce21f31290850f2e1235051 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:39:11 +0700 Subject: [PATCH 070/145] fix: filter batch convert target formats based on source formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exclude same-format and lossy-to-lossless targets from the batch convert sheet so users cannot pick pointless conversions like FLAC→FLAC. Also clean up redundant inline comments. --- lib/screens/downloaded_album_screen.dart | 42 ++++++++++++++++-- lib/screens/local_album_screen.dart | 54 ++++++++++++++++++++++-- lib/screens/queue_tab.dart | 48 +++++++++++++++++++-- lib/services/ffmpeg_service.dart | 19 ++------- 4 files changed, 135 insertions(+), 28 deletions(-) diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index f320aa40..ecbdbbe7 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -910,9 +910,44 @@ class _DownloadedAlbumScreenState extends ConsumerState { BuildContext context, List allTracks, ) { - String selectedFormat = 'MP3'; - String selectedBitrate = '320k'; - bool isLosslessTarget = false; + final tracksById = {for (final t in allTracks) t.id: t}; + final sourceFormats = {}; + for (final id in _selectedIds) { + final item = tracksById[id]; + if (item == null) continue; + final nameToCheck = + (item.safFileName != null && item.safFileName!.isNotEmpty) + ? item.safFileName!.toLowerCase() + : item.filePath.toLowerCase(); + final ext = nameToCheck.endsWith('.flac') + ? 'FLAC' + : nameToCheck.endsWith('.m4a') + ? 'M4A' + : nameToCheck.endsWith('.mp3') + ? 'MP3' + : (nameToCheck.endsWith('.opus') || nameToCheck.endsWith('.ogg')) + ? 'Opus' + : null; + if (ext != null) sourceFormats.add(ext); + } + + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus'].where((target) { + return sourceFormats.any((src) { + if (src == target) return false; + final isLosslessTarget = target == 'ALAC' || target == 'FLAC'; + final isLosslessSource = src == 'FLAC' || src == 'M4A'; + if (isLosslessTarget && !isLosslessSource) return false; + return true; + }); + }).toList(); + + if (formats.isEmpty) return; + + String selectedFormat = formats.first; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; + String selectedBitrate = + isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); showModalBottomSheet( context: context, @@ -924,7 +959,6 @@ class _DownloadedAlbumScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 1b662324..42818a12 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -1129,9 +1129,56 @@ class _LocalAlbumScreenState extends ConsumerState { BuildContext context, List allTracks, ) { - String selectedFormat = 'MP3'; - String selectedBitrate = '320k'; - bool isLosslessTarget = false; + final tracksById = {for (final t in allTracks) t.id: t}; + final sourceFormats = {}; + for (final id in _selectedIds) { + final item = tracksById[id]; + if (item == null) continue; + String? ext; + if (item.format != null && item.format!.isNotEmpty) { + final fmt = item.format!.toLowerCase(); + if (fmt == 'flac') { + ext = 'FLAC'; + } else if (fmt == 'm4a') { + ext = 'M4A'; + } else if (fmt == 'mp3') { + ext = 'MP3'; + } else if (fmt == 'opus' || fmt == 'ogg') { + ext = 'Opus'; + } + } + if (ext == null) { + final lower = item.filePath.toLowerCase(); + if (lower.endsWith('.flac')) { + ext = 'FLAC'; + } else if (lower.endsWith('.m4a')) { + ext = 'M4A'; + } else if (lower.endsWith('.mp3')) { + ext = 'MP3'; + } else if (lower.endsWith('.opus') || lower.endsWith('.ogg')) { + ext = 'Opus'; + } + } + if (ext != null) sourceFormats.add(ext); + } + + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus'].where((target) { + return sourceFormats.any((src) { + if (src == target) return false; + final isLosslessTarget = target == 'ALAC' || target == 'FLAC'; + final isLosslessSource = src == 'FLAC' || src == 'M4A'; + if (isLosslessTarget && !isLosslessSource) return false; + return true; + }); + }).toList(); + + if (formats.isEmpty) return; + + String selectedFormat = formats.first; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; + String selectedBitrate = + isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); showModalBottomSheet( context: context, @@ -1143,7 +1190,6 @@ class _LocalAlbumScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 64f9c0e3..69b1aa44 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4755,9 +4755,50 @@ class _QueueTabState extends ConsumerState { BuildContext context, List allItems, ) async { - String selectedFormat = 'MP3'; - String selectedBitrate = '320k'; - bool isLosslessTarget = false; + final itemsById = {for (final item in allItems) item.id: item}; + final sourceFormats = {}; + for (final id in _selectedIds) { + final item = itemsById[id]; + if (item == null) continue; + String nameToCheck; + if (item.historyItem?.safFileName != null && + item.historyItem!.safFileName!.isNotEmpty) { + nameToCheck = item.historyItem!.safFileName!.toLowerCase(); + } else if (item.localItem?.format != null && + item.localItem!.format!.isNotEmpty) { + nameToCheck = '.${item.localItem!.format!.toLowerCase()}'; + } else { + nameToCheck = item.filePath.toLowerCase(); + } + final ext = nameToCheck.endsWith('.flac') + ? 'FLAC' + : nameToCheck.endsWith('.m4a') + ? 'M4A' + : nameToCheck.endsWith('.mp3') + ? 'MP3' + : (nameToCheck.endsWith('.opus') || nameToCheck.endsWith('.ogg')) + ? 'Opus' + : null; + if (ext != null) sourceFormats.add(ext); + } + + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus'].where((target) { + return sourceFormats.any((src) { + if (src == target) return false; + final isLosslessTarget = target == 'ALAC' || target == 'FLAC'; + final isLosslessSource = src == 'FLAC' || src == 'M4A'; + if (isLosslessTarget && !isLosslessSource) return false; + return true; + }); + }).toList(); + + if (formats.isEmpty) return; + + String selectedFormat = formats.first; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; + String selectedBitrate = + isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); var didStartConversion = false; _hideSelectionOverlay(); @@ -4773,7 +4814,6 @@ class _QueueTabState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index dfb88847..f5608c9f 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1377,12 +1377,7 @@ class FFmpegService { return outputPath; } - /// Convert any audio format to FLAC. - /// Source metadata is preserved via -map_metadata 0 (FFmpeg auto-remaps - /// tag names between container formats), then explicit Vorbis comment - /// overrides are applied from the [metadata] map. - /// Cover art is embedded via a second input stream (same approach as - /// [embedMetadata] and [_convertToAlac]). + /// Convert any audio format to FLAC with metadata and cover art preservation. static Future _convertToFlac({ required String inputPath, required Map metadata, @@ -1394,7 +1389,6 @@ class FFmpegService { final cmdBuffer = StringBuffer(); cmdBuffer.write('-i "$inputPath" '); - // Cover art as second input for attached picture final hasCover = coverPath != null && coverPath.trim().isNotEmpty && await File(coverPath).exists(); @@ -1409,12 +1403,8 @@ class FFmpegService { cmdBuffer.write('-metadata:s:v comment="Cover (front)" '); } cmdBuffer.write('-c:a flac -compression_level 8 '); - - // Copy source metadata as base (FFmpeg auto-remaps M4A/ID3 tags to - // Vorbis comment names), then override with our explicit values. cmdBuffer.write('-map_metadata 0 '); - // Apply normalized Vorbis comment overrides final vorbisComments = _normalizeToVorbisComments(metadata); for (final entry in vorbisComments.entries) { final sanitized = entry.value.replaceAll('"', '\\"'); @@ -1447,8 +1437,8 @@ class FFmpegService { return outputPath; } - /// Normalize metadata keys to standard Vorbis comment names and filter out - /// technical/non-tag fields (bit_depth, sample_rate, duration, etc.). + /// Normalize metadata keys to standard Vorbis comment names, filtering out + /// technical fields (bit_depth, sample_rate, duration, etc.). static Map _normalizeToVorbisComments( Map metadata, ) { @@ -1508,12 +1498,9 @@ class FFmpegService { break; case 'LYRICS': case 'UNSYNCEDLYRICS': - // Write both keys for compatibility with different FLAC readers vorbis['LYRICS'] = value; vorbis['UNSYNCEDLYRICS'] = value; break; - // Technical fields (BIT_DEPTH, SAMPLE_RATE, DURATION, etc.) are - // intentionally dropped — they are not Vorbis comment tags. } } From 7d8cf5f7ca8f9144c8b64bacca787b2ed1fa58d6 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:43:13 +0700 Subject: [PATCH 071/145] fix: detect embedded lyrics in M4A/ALAC files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add extractLyricsFromM4A() that walks the MP4 box tree (moov/udta/meta/ilst/©lyr) to read lyrics. Wire it into ExtractLyrics so the Embed Lyrics button is hidden when lyrics already exist in the file. --- go_backend/metadata.go | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 391b7e5d..19783cc2 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -552,6 +552,14 @@ func ExtractLyrics(filePath string) (string, error) { return extractLyricsFromSidecarLRC(filePath) } + if strings.HasSuffix(lower, ".m4a") || strings.HasSuffix(lower, ".aac") { + lyrics, err := extractLyricsFromM4A(filePath) + if err == nil && strings.TrimSpace(lyrics) != "" { + return lyrics, nil + } + return extractLyricsFromSidecarLRC(filePath) + } + if strings.HasSuffix(lower, ".mp3") { meta, err := ReadID3Tags(filePath) if err == nil && meta != nil { @@ -581,6 +589,80 @@ func ExtractLyrics(filePath string) (string, error) { return extractLyricsFromSidecarLRC(filePath) } +func extractLyricsFromM4A(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return "", err + } + fileSize := fi.Size() + + moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize) + if err != nil || !found { + return "", fmt.Errorf("moov not found") + } + + bodyStart := moov.offset + moov.headerSize + bodySize := moov.size - moov.headerSize + + udta, found, err := findAtomInRange(f, bodyStart, bodySize, "udta", fileSize) + if err != nil || !found { + return "", fmt.Errorf("udta not found") + } + + bodyStart = udta.offset + udta.headerSize + bodySize = udta.size - udta.headerSize + + meta, found, err := findAtomInRange(f, bodyStart, bodySize, "meta", fileSize) + if err != nil || !found { + return "", fmt.Errorf("meta not found") + } + + // meta atom has 4-byte version/flags after the header + bodyStart = meta.offset + meta.headerSize + 4 + bodySize = meta.size - meta.headerSize - 4 + + ilst, found, err := findAtomInRange(f, bodyStart, bodySize, "ilst", fileSize) + if err != nil || !found { + return "", fmt.Errorf("ilst not found") + } + + bodyStart = ilst.offset + ilst.headerSize + bodySize = ilst.size - ilst.headerSize + + lyr, found, err := findAtomInRange(f, bodyStart, bodySize, "\xa9lyr", fileSize) + if err != nil || !found { + return "", fmt.Errorf("lyrics atom not found") + } + + dataStart := lyr.offset + lyr.headerSize + dataSize := lyr.size - lyr.headerSize + + dataAtom, found, err := findAtomInRange(f, dataStart, dataSize, "data", fileSize) + if err != nil || !found { + return "", fmt.Errorf("data atom not found in lyrics") + } + + // data atom: 8 bytes header + 4 bytes type indicator + 4 bytes locale = skip 8 + textStart := dataAtom.offset + dataAtom.headerSize + 8 + textLen := dataAtom.size - dataAtom.headerSize - 8 + if textLen <= 0 { + return "", fmt.Errorf("empty lyrics") + } + + buf := make([]byte, textLen) + if _, err := f.ReadAt(buf, textStart); err != nil { + return "", err + } + + return string(buf), nil +} + func extractLyricsFromSidecarLRC(filePath string) (string, error) { ext := filepath.Ext(filePath) base := strings.TrimSuffix(filePath, ext) From 859b823e770606b986e2f6330c1e8c995242923e Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:49:48 +0700 Subject: [PATCH 072/145] fix: extract cover art from M4A/ALAC files for conversion Add extractCoverFromM4A() that reads the covr atom from the MP4 box tree (moov/udta/meta/ilst/covr/data). Wire it into ExtractCoverToFile so ALAC-to-FLAC conversion preserves cover art. --- go_backend/exports.go | 2 ++ go_backend/metadata.go | 73 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/go_backend/exports.go b/go_backend/exports.go index 1c5f84ed..e65b0197 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1694,6 +1694,8 @@ func ExtractCoverToFile(audioPath string, outputPath string) error { if strings.HasSuffix(lower, ".flac") { coverData, err = ExtractCoverArt(audioPath) + } else if strings.HasSuffix(lower, ".m4a") || strings.HasSuffix(lower, ".aac") { + coverData, err = extractCoverFromM4A(audioPath) } else if strings.HasSuffix(lower, ".mp3") { coverData, _, err = extractMP3CoverArt(audioPath) } else if strings.HasSuffix(lower, ".opus") || strings.HasSuffix(lower, ".ogg") { diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 19783cc2..d4d28e37 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -663,6 +663,79 @@ func extractLyricsFromM4A(filePath string) (string, error) { return string(buf), nil } +func extractCoverFromM4A(filePath string) ([]byte, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return nil, err + } + fileSize := fi.Size() + + moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("moov not found") + } + + bodyStart := moov.offset + moov.headerSize + bodySize := moov.size - moov.headerSize + + udta, found, err := findAtomInRange(f, bodyStart, bodySize, "udta", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("udta not found") + } + + bodyStart = udta.offset + udta.headerSize + bodySize = udta.size - udta.headerSize + + meta, found, err := findAtomInRange(f, bodyStart, bodySize, "meta", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("meta not found") + } + + bodyStart = meta.offset + meta.headerSize + 4 + bodySize = meta.size - meta.headerSize - 4 + + ilst, found, err := findAtomInRange(f, bodyStart, bodySize, "ilst", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("ilst not found") + } + + bodyStart = ilst.offset + ilst.headerSize + bodySize = ilst.size - ilst.headerSize + + covr, found, err := findAtomInRange(f, bodyStart, bodySize, "covr", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("cover atom not found") + } + + dataStart := covr.offset + covr.headerSize + dataSize := covr.size - covr.headerSize + + dataAtom, found, err := findAtomInRange(f, dataStart, dataSize, "data", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("data atom not found in cover") + } + + // data atom: header + 4 bytes type indicator + 4 bytes locale + imgStart := dataAtom.offset + dataAtom.headerSize + 8 + imgLen := dataAtom.size - dataAtom.headerSize - 8 + if imgLen <= 0 { + return nil, fmt.Errorf("empty cover data") + } + + buf := make([]byte, imgLen) + if _, err := f.ReadAt(buf, imgStart); err != nil { + return nil, err + } + + return buf, nil +} + func extractLyricsFromSidecarLRC(filePath string) (string, error) { ext := filepath.Ext(filePath) base := strings.TrimSuffix(filePath, ext) From ce5a9e0cff78f1c57d114fb2ece04c37a4e14a2f Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 03:28:27 +0700 Subject: [PATCH 073/145] fix: use album-level artist for Various Artists albums instead of first track's artist - Extension: fix extractSchemaOrg to find album-level schema (with numTracks) instead of per-track schema - Extension: add secondaryText2 fallback in parseDescriptiveRows for VA album track artists - Extension: use headerPrimaryText as primary album artist source, overriding schema.org - App: album_screen now uses widget.artistName (album-level) instead of tracks.first.artistName - App: home_tab _parseTrack now populates albumArtist from track data or album-level artist - Bump Amazon extension to v2.0.1 --- lib/screens/album_screen.dart | 7 +++++-- lib/screens/home_tab.dart | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index f498d834..139a5720 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -95,7 +95,7 @@ class _AlbumScreenState extends ConsumerState { .recordAlbumAccess( id: widget.albumId, name: widget.albumName, - artistName: widget.tracks?.firstOrNull?.artistName, + artistName: widget.artistName ?? widget.tracks?.firstOrNull?.albumArtist ?? widget.tracks?.firstOrNull?.artistName, imageUrl: widget.coverUrl, providerId: providerId, ); @@ -283,7 +283,10 @@ class _AlbumScreenState extends ConsumerState { ) { final expandedHeight = _calculateExpandedHeight(context); final tracks = _tracks ?? []; - final artistName = tracks.isNotEmpty ? tracks.first.artistName : null; + final artistName = widget.artistName ?? + (tracks.isNotEmpty + ? (tracks.first.albumArtist ?? tracks.first.artistName) + : null); final releaseDate = tracks.isNotEmpty ? tracks.first.releaseDate : null; return SliverAppBar( diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 142c59de..1cffcf2d 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -3909,6 +3909,7 @@ class _ExtensionAlbumScreenState extends ConsumerState { name: (data['name'] ?? '').toString(), artistName: (data['artists'] ?? data['artist'] ?? '').toString(), albumName: (data['album_name'] ?? widget.albumName).toString(), + albumArtist: (data['album_artist'] ?? _artistName)?.toString(), artistId: (data['artist_id'] ?? data['artistId'])?.toString() ?? _artistId, albumId: data['album_id']?.toString() ?? widget.albumId, From c7bef03ee38093a93520962e3d088520736ebc10 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 03:29:45 +0700 Subject: [PATCH 074/145] bump version to 3.8.5+111 --- lib/constants/app_info.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 03766dff..88a98c6e 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -3,8 +3,8 @@ import 'package:flutter/foundation.dart'; /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.8.0'; - static const String buildNumber = '106'; + static const String version = '3.8.5'; + static const String buildNumber = '111'; static const String fullVersion = '$version+$buildNumber'; /// Shows "Internal" in debug builds, actual version in release. diff --git a/pubspec.yaml b/pubspec.yaml index 1aa0fcf1..c54be740 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Deezer publish_to: "none" -version: 3.8.0+106 +version: 3.8.5+111 environment: sdk: ^3.10.0 From a4313cfe0fb51a218c3078820c0957843fbd5266 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 03:36:42 +0700 Subject: [PATCH 075/145] docs: add extension store URL setup guide to README --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 21feb955..f6d99306 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,15 @@ Extensions allow the community to add new music sources and features without wai ### Installing Extensions 1. Go to **Store** tab in the app -2. Browse and install extensions with one tap -3. Or download a `.spotiflac-ext` file and install manually via **Settings > Extensions** -4. Configure extension settings if needed -5. Set provider priority in **Settings > Extensions > Provider Priority** +2. When opening the Store for the first time, you will be asked to enter an **Extension Repository URL**. You can use this community repository: + ``` + https://github.com/zarzet/SpotiFLAC-Extension + ``` + > **Why is this needed?** SpotiFLAC uses a decentralized extension system — extensions are hosted on GitHub repositories rather than a built-in server. This allows anyone to create and host their own extension repository. You only need to enter this URL once; the app will remember it. +3. Browse and install extensions with one tap +4. Or download a `.spotiflac-ext` file and install manually via **Settings > Extensions** +5. Configure extension settings if needed +6. Set provider priority in **Settings > Extensions > Provider Priority** ### Developing Extensions Want to create your own extension? Check out the [Extension Development Guide](https://zarzet.github.io/SpotiFLAC-Mobile/docs) for complete documentation. @@ -55,6 +60,11 @@ Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music for Window ## FAQ +**Q: Why does the Store tab ask me to enter a URL?** +A: Starting from version 3.8.0, SpotiFLAC uses a decentralized extension system. Extensions (like Amazon Music, YouTube Music, etc.) are hosted on GitHub repositories instead of a built-in server. On first launch, enter an extension repository URL in the Store tab, for example: +`https://github.com/zarzet/SpotiFLAC-Extension` +You only need to do this once — the app will save it and load extensions automatically on future launches. + **Q: Why is my download failing with "Song not found"?** A: The track may not be available on the streaming services. Try enabling more download services in Settings > Download > Provider Priority, or install additional extensions like Amazon Music from the Store. From c0d72e89d752fc3c1152848f21dd740d59df04cb Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 03:48:40 +0700 Subject: [PATCH 076/145] fix: skip already-downloaded tracks in Download All for albums and playlists Album and playlist Download All buttons now check download history and local library before enqueuing, matching the existing behavior in artist discography and CSV import. Tracks already in library are skipped with a summary snackbar. --- lib/screens/album_screen.dart | 67 +++++++++++++++++++++++++------- lib/screens/playlist_screen.dart | 67 +++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 30 deletions(-) diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index 139a5720..9b8da6a4 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -574,37 +574,74 @@ class _AlbumScreenState extends ConsumerState { void _downloadAll(BuildContext context) { final tracks = _tracks; if (tracks == null || tracks.isEmpty) return; + + // Filter out tracks already in download history or local library + final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); + final localLibState = (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) + ? ref.read(localLibraryProvider) + : null; + final tracksToQueue = []; + int skippedCount = 0; + + for (final track in tracks) { + final isInHistory = historyState.isDownloaded(track.id) || + (track.isrc != null && historyState.getByIsrc(track.isrc!) != null) || + historyState.findByTrackAndArtist(track.name, track.artistName) != null; + final isInLocal = localLibState?.existsInLibrary( + isrc: track.isrc, + trackName: track.name, + artistName: track.artistName, + ) ?? + false; + + if (isInHistory || isInLocal) { + skippedCount++; + } else { + tracksToQueue.add(track); + } + } + + if (tracksToQueue.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.discographySkippedDownloaded(0, skippedCount), + ), + ), + ); + return; + } + if (settings.askQualityBeforeDownload) { DownloadServicePicker.show( context, - trackName: '${tracks.length} tracks', + trackName: '${tracksToQueue.length} tracks', artistName: widget.albumName, onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarAddedTracksToQueue(tracks.length), - ), - ), - ); + .addMultipleToQueue(tracksToQueue, service, qualityOverride: quality); + _showQueuedSnackbar(context, tracksToQueue.length, skippedCount); }, ); } else { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), - ), - ); + .addMultipleToQueue(tracksToQueue, settings.defaultService); + _showQueuedSnackbar(context, tracksToQueue.length, skippedCount); } } + void _showQueuedSnackbar(BuildContext context, int added, int skipped) { + final message = skipped > 0 + ? context.l10n.discographySkippedDownloaded(added, skipped) + : context.l10n.snackbarAddedTracksToQueue(added); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + Widget _buildLoveAllButton() { final collectionsState = ref.watch(libraryCollectionsProvider); final tracks = _tracks; diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 8a035def..67d377b9 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -608,45 +608,82 @@ class _PlaylistScreenState extends ConsumerState { void _downloadTracks(BuildContext context, List tracks) { if (tracks.isEmpty) return; + + // Filter out tracks already in download history or local library + final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); + final localLibState = (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) + ? ref.read(localLibraryProvider) + : null; + final tracksToQueue = []; + int skippedCount = 0; + + for (final track in tracks) { + final isInHistory = historyState.isDownloaded(track.id) || + (track.isrc != null && historyState.getByIsrc(track.isrc!) != null) || + historyState.findByTrackAndArtist(track.name, track.artistName) != null; + final isInLocal = localLibState?.existsInLibrary( + isrc: track.isrc, + trackName: track.name, + artistName: track.artistName, + ) ?? + false; + + if (isInHistory || isInLocal) { + skippedCount++; + } else { + tracksToQueue.add(track); + } + } + + if (tracksToQueue.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.discographySkippedDownloaded(0, skippedCount), + ), + ), + ); + return; + } + if (settings.askQualityBeforeDownload) { DownloadServicePicker.show( context, - trackName: '${tracks.length} tracks', + trackName: '${tracksToQueue.length} tracks', artistName: _playlistName, onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) .addMultipleToQueue( - tracks, + tracksToQueue, service, qualityOverride: quality, playlistName: _playlistName, ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarAddedTracksToQueue(tracks.length), - ), - ), - ); + _showQueuedSnackbar(context, tracksToQueue.length, skippedCount); }, ); } else { ref .read(downloadQueueProvider.notifier) .addMultipleToQueue( - tracks, + tracksToQueue, settings.defaultService, playlistName: _playlistName, ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), - ), - ); + _showQueuedSnackbar(context, tracksToQueue.length, skippedCount); } } + + void _showQueuedSnackbar(BuildContext context, int added, int skipped) { + final message = skipped > 0 + ? context.l10n.discographySkippedDownloaded(added, skipped) + : context.l10n.snackbarAddedTracksToQueue(added); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } } /// Separate Consumer widget for each track - only rebuilds when this specific track's status changes From ed47efed1746cbb778819a47de4ebc512b949bcb Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 04:12:14 +0700 Subject: [PATCH 077/145] fix: verify resolved Tidal/Deezer tracks match the download request before downloading SongLink can return incorrect track IDs (e.g. a different track from the same album). Qobuz already had verification via qobuzTrackMatchesRequest. This adds equivalent verification for Tidal and Deezer using a shared trackMatchesRequest() helper in title_match_utils.go that checks artist, title, and duration. Mismatched SongLink/ISRC results are now rejected so the wrong audio is never embedded with Spotify metadata. --- go_backend/deezer_download.go | 53 +++++++++++++++++++++++++++----- go_backend/tidal.go | 26 ++++++++++++++++ go_backend/title_match_utils.go | 42 +++++++++++++++++++++++++ lib/screens/album_screen.dart | 8 ++--- lib/screens/playlist_screen.dart | 3 +- 5 files changed, 117 insertions(+), 15 deletions(-) diff --git a/go_backend/deezer_download.go b/go_backend/deezer_download.go index bef0e2ff..974ffcf6 100644 --- a/go_backend/deezer_download.go +++ b/go_backend/deezer_download.go @@ -203,29 +203,48 @@ func resolveDeezerTrackURL(req DownloadRequest) (string, error) { } } if deezerID != "" { - return fmt.Sprintf("https://www.deezer.com/track/%s", deezerID), nil + trackURL := fmt.Sprintf("https://www.deezer.com/track/%s", deezerID) + if err := verifyDeezerTrack(req, deezerID); err != nil { + GoLog("[Deezer] Direct ID %s verification failed: %v\n", deezerID, err) + // Don't reject direct IDs from request payload — they're presumably correct. + } + return trackURL, nil } - // Try resolving Deezer ID from Spotify ID via SongLink + // Try SongLink spotifyID := strings.TrimSpace(req.SpotifyID) if spotifyID != "" && isLikelySpotifyTrackID(spotifyID) { songlink := NewSongLinkClient() availability, err := songlink.CheckTrackAvailability(spotifyID, "") if err == nil && availability.Deezer && availability.DeezerURL != "" { - return availability.DeezerURL, nil + resolvedID := extractDeezerIDFromURL(availability.DeezerURL) + if resolvedID != "" { + if verifyErr := verifyDeezerTrack(req, resolvedID); verifyErr != nil { + GoLog("[Deezer] SongLink ID %s rejected: %v\n", resolvedID, verifyErr) + // Fall through to ISRC search instead of using wrong track. + } else { + return availability.DeezerURL, nil + } + } else { + return availability.DeezerURL, nil + } } } - // Try resolving from ISRC + // Try ISRC isrc := strings.TrimSpace(req.ISRC) if isrc != "" { ctx, cancel := context.WithTimeout(context.Background(), SongLinkTimeout) defer cancel() track, err := GetDeezerClient().SearchByISRC(ctx, isrc) if err == nil && track != nil { - deezerID = songLinkExtractDeezerTrackID(track) - if deezerID != "" { - return fmt.Sprintf("https://www.deezer.com/track/%s", deezerID), nil + resolvedID := songLinkExtractDeezerTrackID(track) + if resolvedID != "" { + if verifyErr := verifyDeezerTrack(req, resolvedID); verifyErr != nil { + GoLog("[Deezer] ISRC-resolved ID %s rejected: %v\n", resolvedID, verifyErr) + return "", fmt.Errorf("deezer track resolved via ISRC does not match: %w", verifyErr) + } + return fmt.Sprintf("https://www.deezer.com/track/%s", resolvedID), nil } } } @@ -233,6 +252,26 @@ func resolveDeezerTrackURL(req DownloadRequest) (string, error) { return "", fmt.Errorf("could not resolve Deezer track URL") } +func verifyDeezerTrack(req DownloadRequest, deezerID string) error { + ctx, cancel := context.WithTimeout(context.Background(), SongLinkTimeout) + defer cancel() + trackResp, err := GetDeezerClient().GetTrack(ctx, deezerID) + if err != nil { + return nil // Can't verify — don't block the download. + } + resolved := resolvedTrackInfo{ + Title: trackResp.Track.Name, + ArtistName: trackResp.Track.Artists, + Duration: trackResp.Track.DurationMS / 1000, + } + if !trackMatchesRequest(req, resolved, "Deezer") { + return fmt.Errorf("expected '%s - %s', got '%s - %s'", + req.ArtistName, req.TrackName, resolved.ArtistName, resolved.Title) + } + GoLog("[Deezer] Track %s verified: '%s - %s' ✓\n", deezerID, resolved.ArtistName, resolved.Title) + return nil +} + type deezerMusicDLRequest struct { Platform string `json:"platform"` URL string `json:"url"` diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 582e4ff6..15f17b1c 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1911,6 +1911,32 @@ func resolveTidalTrackForRequest(req DownloadRequest, downloader *TidalDownloade return nil, fmt.Errorf("failed to find tidal track id from request/cache/songlink") } + // Verify the resolved track matches the request. + actualTrack, fetchErr := downloader.getPublicTrack(strconv.FormatInt(trackID, 10)) + if fetchErr != nil { + GoLog("[%s] Warning: could not fetch Tidal track %d for verification: %v\n", logPrefix, trackID, fetchErr) + // Continue without verification — better than failing entirely. + } else { + providerArtist := actualTrack.Artist.Name + if providerArtist == "" && len(actualTrack.Artists) > 0 { + providerArtist = actualTrack.Artists[0].Name + } + resolved := resolvedTrackInfo{ + Title: actualTrack.Title, + ArtistName: providerArtist, + Duration: actualTrack.Duration, + } + if !trackMatchesRequest(req, resolved, logPrefix) { + // Invalidate the cached ID so future requests don't reuse it. + if req.ISRC != "" { + GetTrackIDCache().SetTidal(req.ISRC, 0) + } + return nil, fmt.Errorf("tidal track %d does not match request: expected '%s - %s', got '%s - %s'", + trackID, req.ArtistName, req.TrackName, resolved.ArtistName, resolved.Title) + } + GoLog("[%s] Track %d verified: '%s - %s' ✓\n", logPrefix, trackID, resolved.ArtistName, resolved.Title) + } + track := &TidalTrack{ ID: trackID, Title: strings.TrimSpace(req.TrackName), diff --git a/go_backend/title_match_utils.go b/go_backend/title_match_utils.go index 039ff434..22b0eebd 100644 --- a/go_backend/title_match_utils.go +++ b/go_backend/title_match_utils.go @@ -68,3 +68,45 @@ func normalizeSymbolOnlyTitle(title string) string { return b.String() } + +// ==================== Shared Track Verification ==================== + +// resolvedTrackInfo holds the metadata fetched from a provider for verification. +type resolvedTrackInfo struct { + Title string + ArtistName string + Duration int // seconds +} + +// trackMatchesRequest checks whether a resolved track from a provider matches +// the original download request. Returns true if the track is a plausible match. +func trackMatchesRequest(req DownloadRequest, resolved resolvedTrackInfo, logPrefix string) bool { + if req.ArtistName != "" && resolved.ArtistName != "" && + !artistsMatch(req.ArtistName, resolved.ArtistName) { + GoLog("[%s] Verification failed: artist mismatch — expected '%s', got '%s'\n", + logPrefix, req.ArtistName, resolved.ArtistName) + return false + } + + if req.TrackName != "" && resolved.Title != "" && + !titlesMatch(req.TrackName, resolved.Title) { + GoLog("[%s] Verification failed: title mismatch — expected '%s', got '%s'\n", + logPrefix, req.TrackName, resolved.Title) + return false + } + + expectedDurationSec := req.DurationMS / 1000 + if expectedDurationSec > 0 && resolved.Duration > 0 { + diff := expectedDurationSec - resolved.Duration + if diff < 0 { + diff = -diff + } + if diff > 10 { + GoLog("[%s] Verification failed: duration mismatch — expected %ds, got %ds\n", + logPrefix, expectedDurationSec, resolved.Duration) + return false + } + } + + return true +} diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index 9b8da6a4..b54fb92a 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -81,7 +81,6 @@ class _AlbumScreenState extends ConsumerState { _scrollController.addListener(_onScroll); WidgetsBinding.instance.addPostFrameCallback((_) { - // Use extensionId if available, otherwise detect from albumId prefix final providerId = widget.extensionId ?? (() { @@ -134,9 +133,7 @@ class _AlbumScreenState extends ConsumerState { return (mediaSize.height * 0.55).clamp(360.0, 520.0); } - /// Upgrade cover URL to a reasonable resolution for full-screen display. - /// Spotify CDN only has 300, 640, ~2000 — we stay at 640 (no intermediate). - /// Deezer CDN: upgrade to 1000x1000 (available: 56, 250, 500, 1000, 1400, 1800). + /// Upgrade cover URL to a higher resolution for full-screen display. String? _highResCoverUrl(String? url) { if (url == null) return null; // Spotify CDN: upgrade 300 → 640 only (no intermediate between 640 and 2000) @@ -519,7 +516,6 @@ class _AlbumScreenState extends ConsumerState { } Widget _buildInfoCard(BuildContext context, ColorScheme colorScheme) { - // Info is now displayed in the full-screen cover overlay return const SliverToBoxAdapter(child: SizedBox.shrink()); } @@ -575,7 +571,7 @@ class _AlbumScreenState extends ConsumerState { final tracks = _tracks; if (tracks == null || tracks.isEmpty) return; - // Filter out tracks already in download history or local library + // Skip already-downloaded tracks final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); final localLibState = (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 67d377b9..c2d9bff3 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -350,7 +350,6 @@ class _PlaylistScreenState extends ConsumerState { } Widget _buildInfoCard(BuildContext context, ColorScheme colorScheme) { - // Info is now displayed in the full-screen cover overlay return const SliverToBoxAdapter(child: SizedBox.shrink()); } @@ -609,7 +608,7 @@ class _PlaylistScreenState extends ConsumerState { void _downloadTracks(BuildContext context, List tracks) { if (tracks.isEmpty) return; - // Filter out tracks already in download history or local library + // Skip already-downloaded tracks final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); final localLibState = (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) From 3747ffff6470fc3c2d2f19f93730fa6a5eacf0a4 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 04:24:18 +0700 Subject: [PATCH 078/145] docs: update readme --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f6d99306..bfffd8ea 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,7 @@ Extensions allow the community to add new music sources and features without wai ### Installing Extensions 1. Go to **Store** tab in the app -2. When opening the Store for the first time, you will be asked to enter an **Extension Repository URL**. You can use this community repository: - ``` - https://github.com/zarzet/SpotiFLAC-Extension - ``` - > **Why is this needed?** SpotiFLAC uses a decentralized extension system — extensions are hosted on GitHub repositories rather than a built-in server. This allows anyone to create and host their own extension repository. You only need to enter this URL once; the app will remember it. +2. When opening the Store for the first time, you will be asked to enter an **Extension Repository URL** 3. Browse and install extensions with one tap 4. Or download a `.spotiflac-ext` file and install manually via **Settings > Extensions** 5. Configure extension settings if needed @@ -61,9 +57,7 @@ Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music for Window ## FAQ **Q: Why does the Store tab ask me to enter a URL?** -A: Starting from version 3.8.0, SpotiFLAC uses a decentralized extension system. Extensions (like Amazon Music, YouTube Music, etc.) are hosted on GitHub repositories instead of a built-in server. On first launch, enter an extension repository URL in the Store tab, for example: -`https://github.com/zarzet/SpotiFLAC-Extension` -You only need to do this once — the app will save it and load extensions automatically on future launches. +A: Starting from version 3.8.0, SpotiFLAC uses a decentralized extension repository system — extensions are hosted on GitHub repositories rather than a built-in server, so anyone can create and host their own. Enter a repository URL in the Store tab to browse and install extensions. **Q: Why is my download failing with "Song not found"?** A: The track may not be available on the streaming services. Try enabling more download services in Settings > Download > Provider Priority, or install additional extensions like Amazon Music from the Store. From f67f52eba9c5dc510c33461d420de6f2a82e9b64 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 15 Mar 2026 21:35:25 +0000 Subject: [PATCH 079/145] chore: update AltStore source to v3.8.5 --- apps.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps.json b/apps.json index 2e4c6718..81613e65 100644 --- a/apps.json +++ b/apps.json @@ -7,12 +7,12 @@ "name": "SpotiFLAC", "bundleIdentifier": "com.zarzet.spotiflac", "developerName": "zarzet", - "version": "3.8.0", - "versionDate": "2026-03-14", - "downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v3.8.0/SpotiFLAC-v3.8.0-ios-unsigned.ipa", + "version": "3.8.5", + "versionDate": "2026-03-15", + "downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v3.8.5/SpotiFLAC-v3.8.5-ios-unsigned.ipa", "localizedDescription": "Mobile version of SpotiFLAC written in Flutter. Download Tracks in true FLAC from Tidal, Qobuz, & Amazon Music.", "iconURL": "https://raw.githubusercontent.com/zarzet/SpotiFLAC-Mobile/main/assets/images/logo.png", - "size": 33659289 + "size": 33673615 } ] } From 84381d142a444f2fa6d0bdb0f57ce20d4a3b7f2f Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:17:00 +0700 Subject: [PATCH 080/145] fix: delay iOS folder picker after sheet dismiss and update Afkar hosts --- CHANGELOG.md | 4 ++-- go_backend/qobuz.go | 2 +- go_backend/spotfetch_api.go | 2 +- .../settings/download_settings_page.dart | 21 ++++++++++++++++++- lib/screens/setup_screen.dart | 21 ++++++++++++++++++- 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8b7254..9916536f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -334,7 +334,7 @@ Thank you for your understanding and continued support. This decision was made t - Routing priority: YouTube service -> extension fallback -> built-in fallback -> direct service - New Android method channel handler: `"downloadByStrategy"` -> `Gobackend.downloadByStrategy(...)` - SpotFetch metadata fallback integration for Spotify-blocked regions - - New backend client for `spotify.afkarxyz.fun/api` + - New backend client for `sp.afkarxyz.qzz.io/api` - Automatic fallback in Spotify metadata fetch path when primary source fails - Lyrics extraction now supports MP3 (ID3v2) and Opus/OGG (Vorbis comments) in addition to FLAC - Includes heuristic detection of lyrics stored in Comment fields @@ -349,7 +349,7 @@ Thank you for your understanding and continued support. This decision was made t - Legacy Dart bridge methods (`downloadTrack`, `downloadWithFallback`, `downloadWithExtensions`, `downloadFromYouTube`) are now thin wrappers and marked `@Deprecated` - Qobuz downloader updated to latest Jumo API contract (`/get` endpoint, required headers) - Amazon download flow now returns `decryption_key` from Go and performs decryption in Flutter (local file + SAF paths) -- Amazon now uses the new `amazon.afkarxyz.fun` API flow (ASIN-based track endpoint + legacy fallback) with encrypted stream support +- Amazon now uses the new `amzn.afkarxyz.qzz.io` API flow (ASIN-based track endpoint + legacy fallback) with encrypted stream support - Amazon ASIN extraction rewritten with robust URL/query-param parsing and regex fallback - Amazon provider re-enabled in download service picker and download settings (alongside Tidal, Qobuz, and YouTube picker flow) - Track Metadata cover UI now refreshes from the embedded file after Edit Metadata/Re-enrich, so the displayed art matches actual file tags diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index ec0489ff..e354f479 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -54,7 +54,7 @@ const ( qobuzDownloadAPIURL = "https://www.musicdl.me/api/qobuz/download" qobuzDabMusicAPIURL = "https://dabmusic.xyz/api/stream?trackId=" qobuzDeebAPIURL = "https://dab.yeet.su/api/stream?trackId=" - qobuzAfkarAPIURL = "https://qbz.afkarxyz.fun/api/track/" + qobuzAfkarAPIURL = "https://qbz.afkarxyz.qzz.io/api/track/" qobuzSquidAPIURL = "https://qobuz.squid.wtf/api/download-music?country=US&track_id=" qobuzDebugKeyXORMask = byte(0x5A) ) diff --git a/go_backend/spotfetch_api.go b/go_backend/spotfetch_api.go index d6bfab19..e12804a4 100644 --- a/go_backend/spotfetch_api.go +++ b/go_backend/spotfetch_api.go @@ -10,7 +10,7 @@ import ( "time" ) -const DefaultSpotFetchAPIBaseURL = "https://spotify.afkarxyz.fun/api" +const DefaultSpotFetchAPIBaseURL = "https://sp.afkarxyz.qzz.io/api" // GetSpotifyDataWithAPI fetches Spotify metadata through SpotFetch-compatible API. // This is used as a fallback when direct Spotify API access is blocked/limited. diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index b4a6d1a7..fb583df7 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -1310,8 +1310,27 @@ class _DownloadSettingsPageState extends ConsumerState { subtitle: Text(context.l10n.setupChooseFromFilesSubtitle), onTap: () async { Navigator.pop(ctx); + if (Platform.isIOS) { + await Future.delayed(const Duration(milliseconds: 250)); + } + // Note: iOS requires folder to have at least one file to be selectable - final result = await FilePicker.platform.getDirectoryPath(); + String? result; + try { + result = await FilePicker.platform.getDirectoryPath(); + } catch (e) { + if (ctx.mounted) { + ScaffoldMessenger.of(ctx).showSnackBar( + SnackBar( + content: Text('Failed to open folder picker: $e'), + backgroundColor: Theme.of(ctx).colorScheme.error, + duration: const Duration(seconds: 4), + ), + ); + } + return; + } + if (result != null) { // iOS: Validate the selected path is writable (not iCloud or container root) if (Platform.isIOS) { diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index 7c74d92e..6d12825d 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -321,7 +321,26 @@ class _SetupScreenState extends ConsumerState { title: Text(context.l10n.setupChooseFromFiles), onTap: () async { Navigator.pop(ctx); - final result = await FilePicker.platform.getDirectoryPath(); + if (Platform.isIOS) { + await Future.delayed(const Duration(milliseconds: 250)); + } + + String? result; + try { + result = await FilePicker.platform.getDirectoryPath(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to open folder picker: $e'), + backgroundColor: Theme.of(context).colorScheme.error, + duration: const Duration(seconds: 4), + ), + ); + } + return; + } + if (result != null) { // iOS: Validate the selected path is writable if (Platform.isIOS) { From 75abc03a4f22f59510704d426e37105897336959 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:28:25 +0700 Subject: [PATCH 081/145] feat: add mc nuggets jimmy, CJBGR and michahRicie as supporters Add new supporters to the donate page. michahRicie is highlighted as a gold supporter. --- lib/screens/settings/donate_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index f822f5d3..e1f76a6d 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -164,7 +164,7 @@ class _RecentDonorsCard extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - const donorNames = ['a fan']; + const donorNames = ['a fan', 'mc nuggets jimmy', 'CJBGR', 'michahRicie']; // Match SettingsGroup color logic final cardColor = isDark @@ -479,8 +479,8 @@ int _cr(String v) { return r; } -// Highlighted supporters (hashes of names): none for now. -const _cv = {}; +// Highlighted supporters (hashes of names). +const _cv = {1365043105}; class _SupporterChip extends StatelessWidget { final String name; From f58005f406eb00581929465ce36210cd1665eb98 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:28:31 +0700 Subject: [PATCH 082/145] docs: add contributors section to README Add auto-generated contributor avatars via contrib.rocks with a link to the GitHub contributors page. Include acknowledgement for translators and bug reporters. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index bfffd8ea..4ad91b55 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,18 @@ _If this software is useful and brings you value, consider supporting the projec [![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/zarzet) +## Contributors + +Thanks to all the amazing people who have contributed to SpotiFLAC Mobile! + + + + + +We also appreciate everyone who has helped with [translations on Crowdin](https://crowdin.com/project/spotiflac-mobile), reported bugs, suggested features, and spread the word about SpotiFLAC Mobile. + +Interested in contributing? Check out our [Contributing Guide](CONTRIBUTING.md) to get started! + ## API Credits [hifi-api](https://github.com/binimum/hifi-api) · [music.binimum.org](https://music.binimum.org) · [qqdl.site](https://qqdl.site) · [squid.wtf](https://squid.wtf) · [spotisaver.net](https://spotisaver.net) · [dabmusic.xyz](https://dabmusic.xyz) · [AfkarXYZ](https://github.com/afkarxyz) · [LRCLib](https://lrclib.net) · [Paxsenix](https://lyrics.paxsenix.org) · [Cobalt](https://cobalt.tools) · [qwkuns.me](https://qwkuns.me) · [SpotubeDL](https://spotubedl.com) · [Song.link](https://song.link) · [IDHS](https://github.com/sjdonado/idonthavespotify) From 2e8fe348245bed3b44422e076d4b801a3dd53b60 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:28:37 +0700 Subject: [PATCH 083/145] fix: remove double horizontal padding in store tab extension list The extension list was wrapped in an extra Padding(horizontal: 16) on top of SettingsGroup's default 16px margin, resulting in 32px total inset. Remove the outer wrapper to match settings tab width. --- lib/screens/store_tab.dart | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index 72c91444..63256832 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -269,22 +269,19 @@ class _StoreTabState extends ConsumerState { ), SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SettingsGroup( - children: filteredExtensions.asMap().entries.map((entry) { - final index = entry.key; - final ext = entry.value; - return _ExtensionItem( - extension: ext, - showDivider: index < filteredExtensions.length - 1, - isDownloading: downloadingId == ext.id, - onInstall: () => _installExtension(ext), - onUpdate: () => _updateExtension(ext), - onTap: () => _showExtensionDetails(ext), - ); - }).toList(), - ), + child: SettingsGroup( + children: filteredExtensions.asMap().entries.map((entry) { + final index = entry.key; + final ext = entry.value; + return _ExtensionItem( + extension: ext, + showDivider: index < filteredExtensions.length - 1, + isDownloading: downloadingId == ext.id, + onInstall: () => _installExtension(ext), + onUpdate: () => _updateExtension(ext), + onTap: () => _showExtensionDetails(ext), + ); + }).toList(), ), ), From feff985439a436086b0b6e88100cdc250e153849 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:28:45 +0700 Subject: [PATCH 084/145] feat: add auto-scan option for local library Add a new 'Auto Scan' setting under Local Library with four modes: off, every app open (10min cooldown), daily, and weekly. The app uses WidgetsBindingObserver to trigger incremental scans on launch and when resuming from background, respecting the configured cooldown based on the last scan timestamp. --- lib/l10n/app_localizations.dart | 36 +++++ lib/l10n/app_localizations_de.dart | 19 +++ lib/l10n/app_localizations_en.dart | 19 +++ lib/l10n/app_localizations_es.dart | 19 +++ lib/l10n/app_localizations_fr.dart | 19 +++ lib/l10n/app_localizations_hi.dart | 19 +++ lib/l10n/app_localizations_id.dart | 19 +++ lib/l10n/app_localizations_ja.dart | 19 +++ lib/l10n/app_localizations_ko.dart | 19 +++ lib/l10n/app_localizations_nl.dart | 19 +++ lib/l10n/app_localizations_pt.dart | 19 +++ lib/l10n/app_localizations_ru.dart | 19 +++ lib/l10n/app_localizations_tr.dart | 19 +++ lib/l10n/app_localizations_zh.dart | 19 +++ lib/l10n/arb/app_en.arb | 24 ++++ lib/main.dart | 75 +++++++++- lib/models/settings.dart | 6 + lib/models/settings.g.dart | 2 + lib/providers/settings_provider.dart | 5 + .../settings/library_settings_page.dart | 134 +++++++++++++++++- 20 files changed, 526 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 15451886..0ea69f2b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3106,6 +3106,42 @@ abstract class AppLocalizations { /// **'Show when searching for existing tracks'** String get libraryShowDuplicateIndicatorSubtitle; + /// Setting for automatic library scanning + /// + /// In en, this message translates to: + /// **'Auto Scan'** + String get libraryAutoScan; + + /// Subtitle for auto scan setting + /// + /// In en, this message translates to: + /// **'Automatically scan your library for new files'** + String get libraryAutoScanSubtitle; + + /// Auto scan disabled + /// + /// In en, this message translates to: + /// **'Off'** + String get libraryAutoScanOff; + + /// Auto scan when app opens + /// + /// In en, this message translates to: + /// **'Every app open'** + String get libraryAutoScanOnOpen; + + /// Auto scan once per day + /// + /// In en, this message translates to: + /// **'Daily'** + String get libraryAutoScanDaily; + + /// Auto scan once per week + /// + /// In en, this message translates to: + /// **'Weekly'** + String get libraryAutoScanWeekly; + /// Section header for library actions /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f0137de1..7ad45e21 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1719,6 +1719,25 @@ class AppLocalizationsDe extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Bei der Suche nach vorhandenen Titeln anzeigen'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Aktionen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 041c5bd1..13208d5b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsEn extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 99849885..113c08f1 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsEs extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index cec6eae9..6521f755 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1697,6 +1697,25 @@ class AppLocalizationsFr extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 9a9ad518..04227142 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsHi extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 902d49cd..b23768e3 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1702,6 +1702,25 @@ class AppLocalizationsId extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 31270670..caf7d16e 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1682,6 +1682,25 @@ class AppLocalizationsJa extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'アクション'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 76f1b737..1cac05e7 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1675,6 +1675,25 @@ class AppLocalizationsKo extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index d509e1e7..ac2c8b57 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsNl extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ef87eca7..3c5c8896 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsPt extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 8bbb8a74..31a1a1dc 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1731,6 +1731,25 @@ class AppLocalizationsRu extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Показать при поиске существующих треков'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Действия'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index bbf6ef14..518bcf77 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1707,6 +1707,25 @@ class AppLocalizationsTr extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 465f196c..d36cf154 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsZh extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7f4e1acb..7055840d 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2242,6 +2242,30 @@ "@libraryShowDuplicateIndicatorSubtitle": { "description": "Subtitle for duplicate indicator toggle" }, + "libraryAutoScan": "Auto Scan", + "@libraryAutoScan": { + "description": "Setting for automatic library scanning" + }, + "libraryAutoScanSubtitle": "Automatically scan your library for new files", + "@libraryAutoScanSubtitle": { + "description": "Subtitle for auto scan setting" + }, + "libraryAutoScanOff": "Off", + "@libraryAutoScanOff": { + "description": "Auto scan disabled" + }, + "libraryAutoScanOnOpen": "Every app open", + "@libraryAutoScanOnOpen": { + "description": "Auto scan when app opens" + }, + "libraryAutoScanDaily": "Daily", + "@libraryAutoScanDaily": { + "description": "Auto scan once per day" + }, + "libraryAutoScanWeekly": "Weekly", + "@libraryAutoScanWeekly": { + "description": "Auto scan once per week" + }, "libraryActions": "Actions", "@libraryActions": { "description": "Section header for library actions" diff --git a/lib/main.dart b/lib/main.dart index 63d74d3a..7ead4a81 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotiflac_android/app.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; @@ -90,16 +91,21 @@ class _EagerInitialization extends ConsumerStatefulWidget { _EagerInitializationState(); } -class _EagerInitializationState extends ConsumerState<_EagerInitialization> { +class _EagerInitializationState extends ConsumerState<_EagerInitialization> + with WidgetsBindingObserver { ProviderSubscription? _localLibraryEnabledSub; Timer? _downloadHistoryWarmupTimer; Timer? _libraryCollectionsWarmupTimer; Timer? _localLibraryWarmupTimer; bool _localLibraryWarmupScheduled = false; + bool _autoScanTriggeredOnLaunch = false; + + static const _lastScannedAtKey = 'local_library_last_scanned_at'; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _initializeAppServices(); @@ -110,6 +116,7 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _localLibraryEnabledSub?.close(); _downloadHistoryWarmupTimer?.cancel(); _libraryCollectionsWarmupTimer?.cancel(); @@ -117,6 +124,13 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _maybeAutoScanLocalLibrary(); + } + } + void _initializeDeferredProviders() { _downloadHistoryWarmupTimer = _scheduleProviderWarmup( const Duration(milliseconds: 400), @@ -155,7 +169,64 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { _localLibraryWarmupScheduled = true; _localLibraryWarmupTimer = _scheduleProviderWarmup( const Duration(milliseconds: 1600), - () => ref.read(localLibraryProvider), + () { + ref.read(localLibraryProvider); + // Trigger auto-scan after initial warmup on first app launch. + if (!_autoScanTriggeredOnLaunch) { + _autoScanTriggeredOnLaunch = true; + // Give the provider a moment to load existing data before scanning. + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) _maybeAutoScanLocalLibrary(); + }); + } + }, + ); + } + + /// Checks whether an automatic incremental scan should be triggered based on + /// the user's auto-scan preference and the time since the last scan. + Future _maybeAutoScanLocalLibrary() async { + if (!mounted) return; + + final settings = ref.read(settingsProvider); + if (!settings.localLibraryEnabled) return; + if (settings.localLibraryPath.isEmpty) return; + if (settings.localLibraryAutoScan == 'off') return; + + // Don't start a scan if one is already running. + final libraryState = ref.read(localLibraryProvider); + if (libraryState.isScanning) return; + + // Determine cooldown based on auto-scan mode. + final now = DateTime.now(); + final prefs = await SharedPreferences.getInstance(); + final lastScannedMs = prefs.getInt(_lastScannedAtKey); + + if (lastScannedMs != null) { + final lastScanned = DateTime.fromMillisecondsSinceEpoch(lastScannedMs); + final elapsed = now.difference(lastScanned); + + switch (settings.localLibraryAutoScan) { + case 'on_open': + // Cooldown of 10 minutes to prevent rapid re-scans. + if (elapsed.inMinutes < 10) return; + break; + case 'daily': + if (elapsed.inHours < 24) return; + break; + case 'weekly': + if (elapsed.inDays < 7) return; + break; + default: + return; + } + } + + // All checks passed -- start an incremental scan. + final iosBookmark = settings.localLibraryBookmark; + ref.read(localLibraryProvider.notifier).startScan( + settings.localLibraryPath, + iosBookmark: iosBookmark.isNotEmpty ? iosBookmark : null, ); } diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 9bf2ee10..87c31e1b 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -59,6 +59,8 @@ class AppSettings { localLibraryBookmark; // Base64-encoded iOS security-scoped bookmark final bool localLibraryShowDuplicates; // Show indicator when searching for existing tracks + final String + localLibraryAutoScan; // Auto-scan mode: 'off', 'on_open', 'daily', 'weekly' final bool hasCompletedTutorial; // Track if user has completed the app tutorial @@ -123,6 +125,7 @@ class AppSettings { this.localLibraryPath = '', this.localLibraryBookmark = '', this.localLibraryShowDuplicates = true, + this.localLibraryAutoScan = 'off', this.hasCompletedTutorial = false, this.lyricsProviders = const [ 'lrclib', @@ -186,6 +189,7 @@ class AppSettings { String? localLibraryPath, String? localLibraryBookmark, bool? localLibraryShowDuplicates, + String? localLibraryAutoScan, bool? hasCompletedTutorial, List? lyricsProviders, bool? lyricsIncludeTranslationNetease, @@ -251,6 +255,8 @@ class AppSettings { localLibraryBookmark: localLibraryBookmark ?? this.localLibraryBookmark, localLibraryShowDuplicates: localLibraryShowDuplicates ?? this.localLibraryShowDuplicates, + localLibraryAutoScan: + localLibraryAutoScan ?? this.localLibraryAutoScan, hasCompletedTutorial: hasCompletedTutorial ?? this.hasCompletedTutorial, lyricsProviders: lyricsProviders ?? this.lyricsProviders, lyricsIncludeTranslationNetease: diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 99a5114d..2b6a35cf 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -57,6 +57,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( localLibraryBookmark: json['localLibraryBookmark'] as String? ?? '', localLibraryShowDuplicates: json['localLibraryShowDuplicates'] as bool? ?? true, + localLibraryAutoScan: json['localLibraryAutoScan'] as String? ?? 'off', hasCompletedTutorial: json['hasCompletedTutorial'] as bool? ?? false, lyricsProviders: (json['lyricsProviders'] as List?) @@ -129,6 +130,7 @@ Map _$AppSettingsToJson( 'localLibraryPath': instance.localLibraryPath, 'localLibraryBookmark': instance.localLibraryBookmark, 'localLibraryShowDuplicates': instance.localLibraryShowDuplicates, + 'localLibraryAutoScan': instance.localLibraryAutoScan, 'hasCompletedTutorial': instance.hasCompletedTutorial, 'lyricsProviders': instance.lyricsProviders, 'lyricsIncludeTranslationNetease': instance.lyricsIncludeTranslationNetease, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index e98a70e0..33706c57 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -518,6 +518,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setLocalLibraryAutoScan(String mode) { + state = state.copyWith(localLibraryAutoScan: mode); + _saveSettings(); + } + void setTutorialComplete() { state = state.copyWith(hasCompletedTutorial: true); _saveSettings(); diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index e71c1046..608eb4bf 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -241,6 +241,99 @@ class _LibrarySettingsPageState extends ConsumerState { } } + String _getAutoScanLabel(BuildContext context, String mode) { + switch (mode) { + case 'on_open': + return context.l10n.libraryAutoScanOnOpen; + case 'daily': + return context.l10n.libraryAutoScanDaily; + case 'weekly': + return context.l10n.libraryAutoScanWeekly; + default: + return context.l10n.libraryAutoScanOff; + } + } + + void _showAutoScanPicker(BuildContext context, String current) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + context.l10n.libraryAutoScan, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.libraryAutoScanSubtitle, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + _AutoScanOption( + icon: Icons.block, + title: context.l10n.libraryAutoScanOff, + selected: current == 'off', + colorScheme: colorScheme, + onTap: () { + ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('off'); + Navigator.pop(context); + }, + ), + _AutoScanOption( + icon: Icons.open_in_new, + title: context.l10n.libraryAutoScanOnOpen, + selected: current == 'on_open', + colorScheme: colorScheme, + onTap: () { + ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('on_open'); + Navigator.pop(context); + }, + ), + _AutoScanOption( + icon: Icons.today, + title: context.l10n.libraryAutoScanDaily, + selected: current == 'daily', + colorScheme: colorScheme, + onTap: () { + ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('daily'); + Navigator.pop(context); + }, + ), + _AutoScanOption( + icon: Icons.date_range, + title: context.l10n.libraryAutoScanWeekly, + selected: current == 'weekly', + colorScheme: colorScheme, + onTap: () { + ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('weekly'); + Navigator.pop(context); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { final settings = ref.watch(settingsProvider); @@ -344,7 +437,18 @@ class _LibrarySettingsPageState extends ConsumerState { onChanged: (value) => ref .read(settingsProvider.notifier) .setLocalLibraryShowDuplicates(value), - showDivider: false, + ), + Opacity( + opacity: settings.localLibraryEnabled ? 1.0 : 0.5, + child: SettingsItem( + icon: Icons.autorenew_rounded, + title: context.l10n.libraryAutoScan, + subtitle: _getAutoScanLabel(context, settings.localLibraryAutoScan), + onTap: settings.localLibraryEnabled + ? () => _showAutoScanPicker(context, settings.localLibraryAutoScan) + : null, + showDivider: false, + ), ), ], ), @@ -825,3 +929,31 @@ class _ScanProgressTile extends StatelessWidget { ); } } + +class _AutoScanOption extends StatelessWidget { + final IconData icon; + final String title; + final bool selected; + final ColorScheme colorScheme; + final VoidCallback onTap; + + const _AutoScanOption({ + required this.icon, + required this.title, + required this.selected, + required this.colorScheme, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon), + title: Text(title), + trailing: selected + ? Icon(Icons.check, color: colorScheme.primary) + : null, + onTap: onTap, + ); + } +} From 6ecb69feaeec8be237e8bd16a9b1e1bb186ab599 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:28:53 +0700 Subject: [PATCH 085/145] fix: prevent re-download of tracks converted to a different format When a file is converted externally (e.g. FLAC to OPUS), the orphan cleanup would delete the history entry because the original path no longer exists. Now it checks for sibling files with other audio extensions and updates the stored path instead of deleting. Also add extension-stripped keys to path_match_keys so that paths differing only by audio extension still match during local library scan exclusion and queue deduplication. --- lib/providers/download_queue_provider.dart | 47 +++++++++++++++++++++- lib/utils/path_match_keys.dart | 39 ++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index a01b8e88..6f983bf4 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -770,6 +770,37 @@ class DownloadHistoryNotifier extends Notifier { /// Remove history entries where the file no longer exists on disk /// Returns the number of orphaned entries removed + /// Audio file extensions that the app commonly produces or converts between. + static const _audioExtensions = [ + '.flac', + '.m4a', + '.mp3', + '.opus', + '.ogg', + '.wav', + '.aac', + ]; + + /// When the original file is missing, check whether a sibling with a + /// different audio extension exists (e.g. the user converted .flac → .opus). + /// Returns the path of the first match found, or `null` if none exist. + Future _findConvertedSibling(String originalPath) async { + // Strip the current extension to get the base path. + final dotIndex = originalPath.lastIndexOf('.'); + if (dotIndex < 0) return null; + final basePath = originalPath.substring(0, dotIndex); + final originalExt = originalPath.substring(dotIndex).toLowerCase(); + + for (final ext in _audioExtensions) { + if (ext == originalExt) continue; + final candidatePath = '$basePath$ext'; + try { + if (await fileExists(candidatePath)) return candidatePath; + } catch (_) {} + } + return null; + } + Future cleanupOrphanedDownloads() async { _historyLog.i('Starting orphaned downloads cleanup...'); @@ -791,7 +822,21 @@ class DownloadHistoryNotifier extends Notifier { if (filePath == null || filePath.isEmpty) return null; pathById[id] = filePath; try { - return MapEntry(id, await fileExists(filePath)); + if (await fileExists(filePath)) return MapEntry(id, true); + + // Original file missing -- check for a converted sibling. + final sibling = await _findConvertedSibling(filePath); + if (sibling != null) { + _historyLog.i( + 'Found converted sibling for $id: $filePath → $sibling', + ); + // Update the stored path so future checks succeed immediately. + await _db.updateFilePath(id, sibling); + pathById[id] = sibling; + return MapEntry(id, true); + } + + return MapEntry(id, false); } catch (e) { _historyLog.w('Error checking file existence for $id: $e'); return MapEntry(id, false); diff --git a/lib/utils/path_match_keys.dart b/lib/utils/path_match_keys.dart index ace140dd..0df1c023 100644 --- a/lib/utils/path_match_keys.dart +++ b/lib/utils/path_match_keys.dart @@ -8,6 +8,33 @@ const _androidStoragePathAliases = [ '/mnt/sdcard', ]; +/// Audio file extensions that the app commonly produces or converts between. +/// Used to generate extension-stripped match keys so that a file converted from +/// one format to another (e.g. .flac → .opus) is still recognised as the same +/// track. +const _audioExtensions = [ + '.flac', + '.m4a', + '.mp3', + '.opus', + '.ogg', + '.wav', + '.aac', +]; + +/// Strips a trailing audio extension from [path] if present. +/// Returns the path without extension, or `null` if no known audio extension +/// was found. +String? _stripAudioExtension(String path) { + final lower = path.toLowerCase(); + for (final ext in _audioExtensions) { + if (lower.endsWith(ext)) { + return path.substring(0, path.length - ext.length); + } + } + return null; +} + Set buildPathMatchKeys(String? filePath) { final raw = filePath?.trim() ?? ''; if (raw.isEmpty) return const {}; @@ -79,6 +106,18 @@ Set buildPathMatchKeys(String? filePath) { } addNormalized(cleaned); + + // Add extension-stripped variants so that a file converted from one audio + // format to another (e.g. Song.flac → Song.opus) still matches. + final extensionStrippedKeys = {}; + for (final key in keys) { + final stripped = _stripAudioExtension(key); + if (stripped != null && stripped.isNotEmpty) { + extensionStrippedKeys.add(stripped); + } + } + keys.addAll(extensionStrippedKeys); + return keys; } From 387dd47374a971712f281272a55571f3699fc9ce Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 18:52:41 +0700 Subject: [PATCH 086/145] feat: add Qobuz Afkar API provider and prefer request metadata for consistent album grouping --- go_backend/exports.go | 59 +++++++++++++++++--------- go_backend/exports_test.go | 86 ++++++++++++++++++++++++++++++++++++++ go_backend/qobuz.go | 29 ++++++++++--- go_backend/qobuz_test.go | 6 ++- go_backend/tidal.go | 16 +++++-- pubspec.lock | 32 ++++++-------- 6 files changed, 178 insertions(+), 50 deletions(-) create mode 100644 go_backend/exports_test.go diff --git a/go_backend/exports.go b/go_backend/exports.go index 5a8a7840..b8178d77 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -135,6 +135,36 @@ type DownloadResult struct { DecryptionKey string } +func preferredReleaseMetadata( + req DownloadRequest, + album string, + releaseDate string, + trackNumber int, + discNumber int, +) (string, string, int, int) { + preferredAlbum := strings.TrimSpace(req.AlbumName) + if preferredAlbum == "" { + preferredAlbum = album + } + + preferredReleaseDate := strings.TrimSpace(req.ReleaseDate) + if preferredReleaseDate == "" { + preferredReleaseDate = releaseDate + } + + preferredTrackNumber := req.TrackNumber + if preferredTrackNumber == 0 { + preferredTrackNumber = trackNumber + } + + preferredDiscNumber := req.DiscNumber + if preferredDiscNumber == 0 { + preferredDiscNumber = discNumber + } + + return preferredAlbum, preferredReleaseDate, preferredTrackNumber, preferredDiscNumber +} + func buildDownloadSuccessResponse( req DownloadRequest, result DownloadResult, @@ -153,25 +183,16 @@ func buildDownloadSuccessResponse( artist = req.ArtistName } - album := result.Album - if album == "" { - album = req.AlbumName - } - - releaseDate := result.ReleaseDate - if releaseDate == "" { - releaseDate = req.ReleaseDate - } - - trackNumber := result.TrackNumber - if trackNumber == 0 { - trackNumber = req.TrackNumber - } - - discNumber := result.DiscNumber - if discNumber == 0 { - discNumber = req.DiscNumber - } + // Preserve requested release metadata when available so mixed-provider + // fallback downloads from the same source album do not get split into + // different albums just because Tidal/Qobuz report variant titles/dates. + album, releaseDate, trackNumber, discNumber := preferredReleaseMetadata( + req, + result.Album, + result.ReleaseDate, + result.TrackNumber, + result.DiscNumber, + ) isrc := result.ISRC if isrc == "" { diff --git a/go_backend/exports_test.go b/go_backend/exports_test.go new file mode 100644 index 00000000..dbce4278 --- /dev/null +++ b/go_backend/exports_test.go @@ -0,0 +1,86 @@ +package gobackend + +import "testing" + +func TestBuildDownloadSuccessResponsePrefersRequestedAlbumMetadata(t *testing.T) { + req := DownloadRequest{ + TrackName: "Bonus Track", + ArtistName: "Artist", + AlbumName: "Album (Deluxe)", + AlbumArtist: "Artist", + ReleaseDate: "2024-01-01", + TrackNumber: 14, + DiscNumber: 1, + ISRC: "REQ123", + CoverURL: "https://example.com/cover.jpg", + Genre: "Pop", + Label: "Label", + Copyright: "Copyright", + } + + result := DownloadResult{ + Title: "Bonus Track", + Artist: "Artist", + Album: "Album", + ReleaseDate: "2023-12-01", + TrackNumber: 2, + DiscNumber: 9, + ISRC: "RES456", + } + + resp := buildDownloadSuccessResponse( + req, + result, + "tidal", + "ok", + "/tmp/test.flac", + false, + ) + + if resp.Album != req.AlbumName { + t.Fatalf("album = %q, want %q", resp.Album, req.AlbumName) + } + if resp.ReleaseDate != req.ReleaseDate { + t.Fatalf("release date = %q, want %q", resp.ReleaseDate, req.ReleaseDate) + } + if resp.TrackNumber != req.TrackNumber { + t.Fatalf("track number = %d, want %d", resp.TrackNumber, req.TrackNumber) + } + if resp.DiscNumber != req.DiscNumber { + t.Fatalf("disc number = %d, want %d", resp.DiscNumber, req.DiscNumber) + } + if resp.Artist != result.Artist { + t.Fatalf("artist = %q, want provider artist %q", resp.Artist, result.Artist) + } + if resp.ISRC != result.ISRC { + t.Fatalf("isrc = %q, want provider isrc %q", resp.ISRC, result.ISRC) + } +} + +func TestPreferredReleaseMetadataPrefersRequestValues(t *testing.T) { + album, releaseDate, trackNumber, discNumber := preferredReleaseMetadata( + DownloadRequest{ + AlbumName: "Album (Deluxe Edition)", + ReleaseDate: "2024-01-01", + TrackNumber: 13, + DiscNumber: 2, + }, + "Album", + "2023-01-01", + 3, + 1, + ) + + if album != "Album (Deluxe Edition)" { + t.Fatalf("album = %q", album) + } + if releaseDate != "2024-01-01" { + t.Fatalf("release date = %q", releaseDate) + } + if trackNumber != 13 { + t.Fatalf("track number = %d", trackNumber) + } + if discNumber != 2 { + t.Fatalf("disc number = %d", discNumber) + } +} diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 788f816f..ec0489ff 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -54,6 +54,7 @@ const ( qobuzDownloadAPIURL = "https://www.musicdl.me/api/qobuz/download" qobuzDabMusicAPIURL = "https://dabmusic.xyz/api/stream?trackId=" qobuzDeebAPIURL = "https://dab.yeet.su/api/stream?trackId=" + qobuzAfkarAPIURL = "https://qbz.afkarxyz.fun/api/track/" qobuzSquidAPIURL = "https://qobuz.squid.wtf/api/download-music?country=US&track_id=" qobuzDebugKeyXORMask = byte(0x5A) ) @@ -1019,6 +1020,10 @@ func (q *QobuzDownloader) GetArtistMetadata(resourceID string) (*ArtistResponseP func (q *QobuzDownloader) GetAvailableAPIs() []string { return []string{ qobuzDownloadAPIURL, + qobuzDabMusicAPIURL, + qobuzDeebAPIURL, + qobuzAfkarAPIURL, + qobuzSquidAPIURL, } } @@ -1039,6 +1044,8 @@ func (q *QobuzDownloader) GetAvailableProviders() []qobuzAPIProvider { {Name: "dabmusic", URL: qobuzDabMusicAPIURL, Kind: qobuzAPIKindStandard}, // "deeb" is mapped from the legacy reference fallback endpoint. {Name: "deeb", URL: qobuzDeebAPIURL, Kind: qobuzAPIKindStandard}, + // "qbz" comes from the desktop reference app and uses /api/track/{id}?quality=... + {Name: "qbz", URL: qobuzAfkarAPIURL, Kind: qobuzAPIKindStandard}, {Name: "squid", URL: qobuzSquidAPIURL, Kind: qobuzAPIKindStandard}, } } @@ -2156,6 +2163,10 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { if req.AlbumName != "" { albumName = req.AlbumName } + releaseDate := track.Album.ReleaseDate + if req.ReleaseDate != "" { + releaseDate = req.ReleaseDate + } actualTrackNumber := req.TrackNumber if actualTrackNumber == 0 { @@ -2167,7 +2178,7 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { Artist: track.Performer.Name, Album: albumName, AlbumArtist: req.AlbumArtist, - Date: track.Album.ReleaseDate, + Date: releaseDate, TrackNumber: actualTrackNumber, TotalTracks: req.TotalTracks, DiscNumber: req.DiscNumber, @@ -2231,16 +2242,24 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { lyricsLRC = parallelResult.LyricsLRC } + resultAlbum, resultReleaseDate, resultTrackNumber, resultDiscNumber := preferredReleaseMetadata( + req, + track.Album.Title, + track.Album.ReleaseDate, + actualTrackNumber, + req.DiscNumber, + ) + return QobuzDownloadResult{ FilePath: outputPath, BitDepth: actualBitDepth, SampleRate: actualSampleRate, Title: track.Title, Artist: track.Performer.Name, - Album: track.Album.Title, - ReleaseDate: track.Album.ReleaseDate, - TrackNumber: actualTrackNumber, - DiscNumber: req.DiscNumber, + Album: resultAlbum, + ReleaseDate: resultReleaseDate, + TrackNumber: resultTrackNumber, + DiscNumber: resultDiscNumber, ISRC: track.ISRC, LyricsLRC: lyricsLRC, }, nil diff --git a/go_backend/qobuz_test.go b/go_backend/qobuz_test.go index d19629b6..9d15d318 100644 --- a/go_backend/qobuz_test.go +++ b/go_backend/qobuz_test.go @@ -213,14 +213,16 @@ func TestExtractQobuzAlbumIDsFromArtistHTML(t *testing.T) { func TestQobuzAvailableProviders(t *testing.T) { providers := NewQobuzDownloader().GetAvailableProviders() - if len(providers) != 3 { - t.Fatalf("expected 3 Qobuz providers, got %d", len(providers)) + if len(providers) != 5 { + t.Fatalf("expected 5 Qobuz providers, got %d", len(providers)) } want := map[string]string{ "musicdl": qobuzAPIKindMusicDL, "dabmusic": qobuzAPIKindStandard, "deeb": qobuzAPIKindStandard, + "qbz": qobuzAPIKindStandard, + "squid": qobuzAPIKindStandard, } for _, provider := range providers { diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 8a68826d..a837380d 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -2189,16 +2189,24 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { lyricsLRC = parallelResult.LyricsLRC } + resultAlbum, resultReleaseDate, resultTrackNumber, resultDiscNumber := preferredReleaseMetadata( + req, + track.Album.Title, + track.Album.ReleaseDate, + actualTrackNumber, + actualDiscNumber, + ) + return TidalDownloadResult{ FilePath: actualOutputPath, BitDepth: bitDepth, SampleRate: sampleRate, Title: track.Title, Artist: track.Artist.Name, - Album: track.Album.Title, - ReleaseDate: track.Album.ReleaseDate, - TrackNumber: actualTrackNumber, - DiscNumber: actualDiscNumber, + Album: resultAlbum, + ReleaseDate: resultReleaseDate, + TrackNumber: resultTrackNumber, + DiscNumber: resultDiscNumber, ISRC: track.ISRC, LyricsLRC: lyricsLRC, }, nil diff --git a/pubspec.lock b/pubspec.lock index 4fc7941f..0370159a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -557,14 +557,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: "direct main" description: @@ -633,18 +625,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -1166,26 +1158,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.30.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.16" timezone: dependency: transitive description: From 16100aa0fdd980994cffde82fe208bb0db97e3b8 Mon Sep 17 00:00:00 2001 From: ViscousPot Date: Sat, 14 Mar 2026 01:39:16 +0000 Subject: [PATCH 087/145] add fvm --- .fvmrc | 3 +++ .gitignore | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 .fvmrc diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..5c08b3bc --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.38.1" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 699e248c..517be8ed 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,6 @@ flutter_*.log # Development tools tool/ .claude/settings.local.json + +# FVM Version Cache +.fvm/ \ No newline at end of file From a6d9849468baa5c21b01b2d754aa118fc24412d0 Mon Sep 17 00:00:00 2001 From: ViscousPot Date: Sun, 15 Mar 2026 04:37:00 +0000 Subject: [PATCH 088/145] Update CONTRIBUTING.md --- CONTRIBUTING.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1750732..2c1e7b6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,17 +86,31 @@ Translation files are located in `lib/l10n/arb/`. git remote add upstream https://github.com/zarzet/SpotiFLAC-Mobile.git ``` -3. **Install dependencies** +3. **Use FVM (Flutter Version: 3.38.1)** + ```bash + fvm use + ``` + +4. **Install dependencies** ```bash flutter pub get ``` -4. **Generate code** (for Riverpod, JSON serialization, etc.) +5. **Generate code** (for Riverpod, JSON serialization, etc.) ```bash dart run build_runner build --delete-conflicting-outputs ``` -5. **Run the app** +6. **Set up Go environment (Go Version: 1.25.7)** + ```bash + cd go_backend + mkdir -p ../android/app/libs + gomobile init + gomobile bind -target=android -androidapi 24 -o ../android/app/libs/gobackend.aar . + cd .. + ``` + +7. **Run the app** ```bash flutter run ``` From 49ea84384d4f05889873ab329704c6d624fabb75 Mon Sep 17 00:00:00 2001 From: ViscousPot Date: Sat, 14 Mar 2026 02:08:49 +0000 Subject: [PATCH 089/145] feat: auto fill playlist name during import --- lib/screens/playlist_screen.dart | 2 +- lib/widgets/playlist_picker_sheet.dart | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index ba16a01f..8a035def 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -533,7 +533,7 @@ class _PlaylistScreenState extends ConsumerState { tooltip: context.l10n.tooltipAddToPlaylist, onPressed: _tracks.isEmpty ? null - : () => showAddTracksToPlaylistSheet(context, ref, _tracks), + : () => showAddTracksToPlaylistSheet(context, ref, _tracks, playlistNamePrefill: widget.playlistName), ); } diff --git a/lib/widgets/playlist_picker_sheet.dart b/lib/widgets/playlist_picker_sheet.dart index 2b6fd162..238b5350 100644 --- a/lib/widgets/playlist_picker_sheet.dart +++ b/lib/widgets/playlist_picker_sheet.dart @@ -20,6 +20,7 @@ Future showAddTracksToPlaylistSheet( BuildContext context, WidgetRef ref, List tracks, + {String? playlistNamePrefill} ) async { if (tracks.isEmpty) return; @@ -31,15 +32,16 @@ Future showAddTracksToPlaylistSheet( showDragHandle: true, isScrollControlled: true, builder: (sheetContext) { - return _PlaylistPickerSheetContent(tracks: tracks); + return _PlaylistPickerSheetContent(tracks: tracks, playlistNamePrefill: playlistNamePrefill); }, ); } class _PlaylistPickerSheetContent extends ConsumerStatefulWidget { final List tracks; + final String? playlistNamePrefill; - const _PlaylistPickerSheetContent({required this.tracks}); + const _PlaylistPickerSheetContent({required this.tracks, this.playlistNamePrefill}); @override ConsumerState<_PlaylistPickerSheetContent> createState() => @@ -130,7 +132,7 @@ class _PlaylistPickerSheetContentState leading: const Icon(Icons.add_circle_outline), title: Text(context.l10n.collectionCreatePlaylist), onTap: () async { - final name = await _promptPlaylistName(context); + final name = await _promptPlaylistName(context, widget.playlistNamePrefill); if (name == null || name.trim().isEmpty || !context.mounted) { return; } @@ -221,8 +223,8 @@ class _PlaylistPickerSheetContentState } } -Future _promptPlaylistName(BuildContext context) async { - final controller = TextEditingController(); +Future _promptPlaylistName(BuildContext context, String? playlistNamePrefill) async { + final controller = TextEditingController(text: playlistNamePrefill); final formKey = GlobalKey(); final result = await showDialog( From d4178ad0369935d7fbc750f1035ba17506f8bbd5 Mon Sep 17 00:00:00 2001 From: ViscousPot Date: Sat, 14 Mar 2026 01:38:32 +0000 Subject: [PATCH 090/145] feat: add option to download multiple selected playlists --- lib/screens/queue_tab.dart | 117 +++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 0a56f7a1..7e3f96f9 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -31,6 +31,7 @@ import 'package:spotiflac_android/screens/local_album_screen.dart'; import 'package:spotiflac_android/utils/clickable_metadata.dart'; import 'package:spotiflac_android/utils/path_match_keys.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; +import 'package:spotiflac_android/widgets/download_service_picker.dart'; enum LibraryItemSource { downloaded, local } @@ -1314,6 +1315,93 @@ class _QueueTabState extends ConsumerState { }); } + Future _downloadAllSelectedPlaylists(BuildContext context) async { + final collectionsState = ref.read(libraryCollectionsProvider); + final selectedPlaylists = collectionsState.playlists + .where((p) => _selectedPlaylistIds.contains(p.id)) + .toList(); + + final totalTracks = + selectedPlaylists.fold(0, (sum, p) => sum + p.tracks.length); + + if (totalTracks == 0) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Selected playlists have no tracks')), + ); + return; + } + + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Download All'), + content: Text( + 'Download $totalTracks ${totalTracks == 1 ? 'track' : 'tracks'} ' + 'from ${selectedPlaylists.length} ' + '${selectedPlaylists.length == 1 ? 'playlist' : 'playlists'}?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text(ctx.l10n.dialogCancel), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Download'), + ), + ], + ), + ); + + if (confirmed != true || !context.mounted) return; + + final settings = ref.read(settingsProvider); + final queueNotifier = ref.read(downloadQueueProvider.notifier); + + void enqueueAll({String? qualityOverride, String? service}) { + final svc = service ?? settings.defaultService; + for (final playlist in selectedPlaylists) { + final tracks = playlist.tracks.map((e) => e.track).toList(); + queueNotifier.addMultipleToQueue( + tracks, + svc, + qualityOverride: qualityOverride, + playlistName: playlist.name, + ); + } + } + + if (settings.askQualityBeforeDownload) { + DownloadServicePicker.show( + context, + trackName: '$totalTracks tracks', + artistName: '${selectedPlaylists.length} playlists', + onSelect: (quality, service) { + enqueueAll(qualityOverride: quality, service: service); + if (!mounted) return; + _exitPlaylistSelectionMode(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.snackbarAddedTracksToQueue(totalTracks), + ), + ), + ); + }, + ); + } else { + enqueueAll(); + _exitPlaylistSelectionMode(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.snackbarAddedTracksToQueue(totalTracks), + ), + ), + ); + } + } + Future _deleteSelectedPlaylists(BuildContext context) async { final count = _selectedPlaylistIds.length; final confirmed = await showDialog( @@ -1452,6 +1540,35 @@ class _QueueTabState extends ConsumerState { const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: selectedCount > 0 + ? () => _downloadAllSelectedPlaylists(context) + : null, + icon: const Icon(Icons.download_rounded), + label: Text( + selectedCount > 0 + ? 'Download $selectedCount ${selectedCount == 1 ? 'playlist' : 'playlists'}' + : 'Select playlists to download', + ), + style: FilledButton.styleFrom( + backgroundColor: selectedCount > 0 + ? colorScheme.primary + : colorScheme.surfaceContainerHighest, + foregroundColor: selectedCount > 0 + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ), + + const SizedBox(height: 8), + SizedBox( width: double.infinity, child: FilledButton.icon( From 12779778d39c5fce85986050d12ca3ccd8005801 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 19:01:45 +0700 Subject: [PATCH 091/145] fix(i18n): localize hardcoded strings in bulk playlist download and fix trailing newlines --- .fvmrc | 2 +- .gitignore | 2 +- lib/l10n/app_localizations.dart | 44 +++++++++++++++++++++---- lib/l10n/app_localizations_de.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_en.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_es.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_fr.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_hi.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_id.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_ja.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_ko.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_nl.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_pt.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_ru.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_tr.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/app_localizations_zh.dart | 52 ++++++++++++++++++++++++++++-- lib/l10n/arb/app_en.arb | 46 ++++++++++++++++++++++++++ lib/screens/queue_tab.dart | 18 +++++------ 18 files changed, 730 insertions(+), 58 deletions(-) diff --git a/.fvmrc b/.fvmrc index 5c08b3bc..3a62807c 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { "flutter": "3.38.1" -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index 517be8ed..b24e9371 100644 --- a/.gitignore +++ b/.gitignore @@ -79,4 +79,4 @@ tool/ .claude/settings.local.json # FVM Version Cache -.fvm/ \ No newline at end of file +.fvm/ diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 53bcba7f..ce8174e2 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1066,6 +1066,12 @@ abstract class AppLocalizations { /// **'Import'** String get dialogImport; + /// Confirm button in Download All dialog + /// + /// In en, this message translates to: + /// **'Download'** + String get dialogDownload; + /// Dialog button - discard changes /// /// In en, this message translates to: @@ -4434,7 +4440,7 @@ abstract class AppLocalizations { /// **'Added {count} tracks to Loved'** String snackbarAddedTracksToLoved(int count); - /// Title of the Download All confirmation dialog + /// Dialog title for bulk download confirmation /// /// In en, this message translates to: /// **'Download All'** @@ -4446,12 +4452,6 @@ abstract class AppLocalizations { /// **'Download {count} tracks?'** String dialogDownloadAllMessage(int count); - /// Confirm button in Download All dialog - /// - /// In en, this message translates to: - /// **'Download'** - String get dialogDownload; - /// Checkbox label in import dialog to skip already-downloaded songs /// /// In en, this message translates to: @@ -4817,6 +4817,36 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Refresh'** String get cacheRefresh; + + /// Dialog message for bulk playlist download confirmation + /// + /// In en, this message translates to: + /// **'Download {trackCount} {trackCount, plural, =1{track} other{tracks}} from {playlistCount} {playlistCount, plural, =1{playlist} other{playlists}}?'** + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount); + + /// Button label for bulk downloading selected playlists + /// + /// In en, this message translates to: + /// **'Download {count} {count, plural, =1{playlist} other{playlists}}'** + String bulkDownloadPlaylistsButton(int count); + + /// Button label when no playlists are selected for download + /// + /// In en, this message translates to: + /// **'Select playlists to download'** + String get bulkDownloadSelectPlaylists; + + /// Snackbar when selected playlists contain no tracks + /// + /// In en, this message translates to: + /// **'Selected playlists have no tracks'** + String get snackbarSelectedPlaylistsEmpty; + + /// Playlist count display + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 playlist} other{{count} playlists}}'** + String playlistsCount(int count); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index a3d43076..b913d844 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -536,6 +536,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get dialogImport => 'Importieren'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Verwerfen'; @@ -2580,9 +2583,6 @@ class AppLocalizationsDe extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2790,4 +2790,50 @@ class AppLocalizationsDe extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 03adf67d..07e3ca2d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -525,6 +525,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsEn extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,4 +2762,50 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c953dbc8..8a5a974b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -525,6 +525,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsEs extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,6 +2762,52 @@ class AppLocalizationsEs extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } /// The translations for Spanish Castilian, as used in Spain (`es_ES`). diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4baf9cba..68d677e1 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -527,6 +527,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2554,9 +2557,6 @@ class AppLocalizationsFr extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2764,4 +2764,50 @@ class AppLocalizationsFr extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 46d6572a..5da28983 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -525,6 +525,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsHi extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,4 +2762,50 @@ class AppLocalizationsHi extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 6aa97510..7fc8d1de 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -528,6 +528,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get dialogImport => 'Impor'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Buang'; @@ -2559,9 +2562,6 @@ class AppLocalizationsId extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2769,4 +2769,50 @@ class AppLocalizationsId extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 92184e0b..04a7a0c2 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -521,6 +521,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get dialogImport => 'インポート'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => '破棄'; @@ -2539,9 +2542,6 @@ class AppLocalizationsJa extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2749,4 +2749,50 @@ class AppLocalizationsJa extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index aed486d5..80e8a1df 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -510,6 +510,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get dialogImport => '불러오기'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => '취소'; @@ -2532,9 +2535,6 @@ class AppLocalizationsKo extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2742,4 +2742,50 @@ class AppLocalizationsKo extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 87c5385a..af603f60 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -525,6 +525,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsNl extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,4 +2762,50 @@ class AppLocalizationsNl extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index d45df4f9..fc40c186 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -525,6 +525,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsPt extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,6 +2762,52 @@ class AppLocalizationsPt extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } /// The translations for Portuguese, as used in Portugal (`pt_PT`). diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 3e9d7e73..7a542295 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -534,6 +534,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get dialogImport => 'Импорт'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Отменить'; @@ -2611,9 +2614,6 @@ class AppLocalizationsRu extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2821,4 +2821,50 @@ class AppLocalizationsRu extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 48a9a7d2..1e5727bf 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -530,6 +530,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get dialogImport => 'İçe aktar'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Vazgeç'; @@ -2564,9 +2567,6 @@ class AppLocalizationsTr extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2774,4 +2774,50 @@ class AppLocalizationsTr extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 2287f76a..68381882 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -525,6 +525,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get dialogImport => 'Import'; + @override + String get dialogDownload => 'Download'; + @override String get dialogDiscard => 'Discard'; @@ -2552,9 +2555,6 @@ class AppLocalizationsZh extends AppLocalizations { return 'Download $count tracks?'; } - @override - String get dialogDownload => 'Download'; - @override String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs'; @@ -2762,6 +2762,52 @@ class AppLocalizationsZh extends AppLocalizations { @override String get cacheRefresh => 'Refresh'; + + @override + String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) { + String _temp0 = intl.Intl.pluralLogic( + trackCount, + locale: localeName, + other: 'tracks', + one: 'track', + ); + String _temp1 = intl.Intl.pluralLogic( + playlistCount, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $trackCount $_temp0 from $playlistCount $_temp1?'; + } + + @override + String bulkDownloadPlaylistsButton(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Download $count $_temp0'; + } + + @override + String get bulkDownloadSelectPlaylists => 'Select playlists to download'; + + @override + String get snackbarSelectedPlaylistsEmpty => + 'Selected playlists have no tracks'; + + @override + String playlistsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 10bf4938..bc3f2e13 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -671,6 +671,10 @@ "@dialogImport": { "description": "Dialog button - import data" }, + "dialogDownload": "Download", + "@dialogDownload": { + "description": "Dialog button - download action" + }, "dialogDiscard": "Discard", "@dialogDiscard": { "description": "Dialog button - discard changes" @@ -3657,5 +3661,47 @@ "cacheRefresh": "Refresh", "@cacheRefresh": { "description": "Tooltip for refresh button on cache management page" + }, + "dialogDownloadAllTitle": "Download All", + "@dialogDownloadAllTitle": { + "description": "Dialog title for bulk download confirmation" + }, + "dialogDownloadPlaylistsMessage": "Download {trackCount} {trackCount, plural, =1{track} other{tracks}} from {playlistCount} {playlistCount, plural, =1{playlist} other{playlists}}?", + "@dialogDownloadPlaylistsMessage": { + "description": "Dialog message for bulk playlist download confirmation", + "placeholders": { + "trackCount": { + "type": "int" + }, + "playlistCount": { + "type": "int" + } + } + }, + "bulkDownloadPlaylistsButton": "Download {count} {count, plural, =1{playlist} other{playlists}}", + "@bulkDownloadPlaylistsButton": { + "description": "Button label for bulk downloading selected playlists", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "bulkDownloadSelectPlaylists": "Select playlists to download", + "@bulkDownloadSelectPlaylists": { + "description": "Button label when no playlists are selected for download" + }, + "snackbarSelectedPlaylistsEmpty": "Selected playlists have no tracks", + "@snackbarSelectedPlaylistsEmpty": { + "description": "Snackbar when selected playlists contain no tracks" + }, + "playlistsCount": "{count, plural, =1{1 playlist} other{{count} playlists}}", + "@playlistsCount": { + "description": "Playlist count display", + "placeholders": { + "count": { + "type": "int" + } + } } } diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 7e3f96f9..5b14ff21 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -1326,7 +1326,7 @@ class _QueueTabState extends ConsumerState { if (totalTracks == 0) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Selected playlists have no tracks')), + SnackBar(content: Text(context.l10n.snackbarSelectedPlaylistsEmpty)), ); return; } @@ -1334,11 +1334,9 @@ class _QueueTabState extends ConsumerState { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Download All'), + title: Text(ctx.l10n.dialogDownloadAllTitle), content: Text( - 'Download $totalTracks ${totalTracks == 1 ? 'track' : 'tracks'} ' - 'from ${selectedPlaylists.length} ' - '${selectedPlaylists.length == 1 ? 'playlist' : 'playlists'}?', + ctx.l10n.dialogDownloadPlaylistsMessage(totalTracks, selectedPlaylists.length), ), actions: [ TextButton( @@ -1347,7 +1345,7 @@ class _QueueTabState extends ConsumerState { ), FilledButton( onPressed: () => Navigator.pop(ctx, true), - child: const Text('Download'), + child: Text(ctx.l10n.dialogDownload), ), ], ), @@ -1374,8 +1372,8 @@ class _QueueTabState extends ConsumerState { if (settings.askQualityBeforeDownload) { DownloadServicePicker.show( context, - trackName: '$totalTracks tracks', - artistName: '${selectedPlaylists.length} playlists', + trackName: context.l10n.tracksCount(totalTracks), + artistName: context.l10n.playlistsCount(selectedPlaylists.length), onSelect: (quality, service) { enqueueAll(qualityOverride: quality, service: service); if (!mounted) return; @@ -1549,8 +1547,8 @@ class _QueueTabState extends ConsumerState { icon: const Icon(Icons.download_rounded), label: Text( selectedCount > 0 - ? 'Download $selectedCount ${selectedCount == 1 ? 'playlist' : 'playlists'}' - : 'Select playlists to download', + ? context.l10n.bulkDownloadPlaylistsButton(selectedCount) + : context.l10n.bulkDownloadSelectPlaylists, ), style: FilledButton.styleFrom( backgroundColor: selectedCount > 0 From 13845eea04bd56ee6cddd2acbcd4fb773624b6b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 12:02:29 +0000 Subject: [PATCH 092/145] chore(deps): update dependency flutter to v3.41.4 --- .fvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.fvmrc b/.fvmrc index 3a62807c..c8437d38 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.38.1" + "flutter": "3.41.4" } From 67737467e0e9085620e2816619422d356cf0f41c Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 19:11:29 +0700 Subject: [PATCH 093/145] ci: auto-update AltStore source (apps.json) on release --- .github/workflows/release.yml | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57772b30..b45e04e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -393,6 +393,63 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + update-altstore: + runs-on: ubuntu-latest + needs: [get-version, build-ios, create-release] + if: ${{ needs.get-version.outputs.is_prerelease != 'true' }} + permissions: + contents: write + + steps: + - name: Checkout main branch + uses: actions/checkout@v6 + with: + ref: main + + - name: Download iOS IPA + uses: actions/download-artifact@v7 + with: + name: ios-ipa + path: ./release + + - name: Update apps.json + run: | + VERSION="${{ needs.get-version.outputs.version }}" + VERSION_NUM="${VERSION#v}" + DATE=$(date -u +%Y-%m-%d) + IPA_FILE=$(find ./release -name "*ios*.ipa" | head -1) + + if [ -z "$IPA_FILE" ]; then + echo "WARNING: IPA file not found, skipping apps.json update" + exit 0 + fi + + IPA_SIZE=$(stat -c%s "$IPA_FILE" 2>/dev/null || stat -f%z "$IPA_FILE") + + if [ ! -f apps.json ]; then + echo "WARNING: apps.json not found on main, skipping" + exit 0 + fi + + jq --arg ver "$VERSION_NUM" \ + --arg date "$DATE" \ + --arg url "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/${VERSION}/SpotiFLAC-${VERSION}-ios-unsigned.ipa" \ + --argjson size "$IPA_SIZE" \ + '.apps[0].version = $ver | .apps[0].versionDate = $date | .apps[0].downloadURL = $url | .apps[0].size = $size' \ + apps.json > apps.json.tmp && mv apps.json.tmp apps.json + + echo "Updated apps.json:" + cat apps.json + + - name: Commit and push + run: | + VERSION="${{ needs.get-version.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add apps.json + git diff --cached --quiet && echo "No changes to commit" || \ + (git commit -m "chore: update AltStore source to ${VERSION}" && git push) + notify-telegram: runs-on: ubuntu-latest needs: [get-version, create-release] From 4495d4bf4e34c334549361c2e7f7f9833eea92a0 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:16:44 +0700 Subject: [PATCH 094/145] feat: add Opus 320kbps quality, remove Tidal HIGH tier - Add YouTubeQualityOpus320 constant and opus_320 parser case in Go backend - Expand opus supported bitrates to [128, 256, 320] across Go, Dart settings, and UI - Update default YouTube Opus option from 256 to 320kbps - Remove Tidal HIGH (lossy 320kbps) quality from Go backend, settings model, settings provider, download queue provider (both SAF and non-SAF paths), settings UI (quality option, format picker, helper methods), and l10n keys - Add settings migration v6: auto-migrate users with audioQuality=HIGH to LOSSLESS - Update and add Go test cases for opus_320 and adjusted max bitrate - Regenerate l10n files, remove 10 unused downloadLossy* l10n keys --- go_backend/tidal.go | 40 +- go_backend/youtube.go | 5 +- go_backend/youtube_quality_test.go | 17 +- lib/l10n/app_localizations.dart | 90 ++--- lib/l10n/app_localizations_de.dart | 54 +-- lib/l10n/app_localizations_en.dart | 54 +-- lib/l10n/app_localizations_es.dart | 54 +-- lib/l10n/app_localizations_fr.dart | 54 +-- lib/l10n/app_localizations_hi.dart | 54 +-- lib/l10n/app_localizations_id.dart | 54 +-- lib/l10n/app_localizations_ja.dart | 54 +-- lib/l10n/app_localizations_ko.dart | 54 +-- lib/l10n/app_localizations_nl.dart | 54 +-- lib/l10n/app_localizations_pt.dart | 54 +-- lib/l10n/app_localizations_ru.dart | 54 +-- lib/l10n/app_localizations_tr.dart | 54 +-- lib/l10n/app_localizations_zh.dart | 54 +-- lib/l10n/arb/app_en.arb | 83 ++-- lib/models/settings.dart | 7 +- lib/models/settings.g.dart | 2 - lib/providers/download_queue_provider.dart | 381 +++++------------- lib/providers/settings_provider.dart | 30 +- .../settings/download_settings_page.dart | 132 +----- lib/widgets/download_service_picker.dart | 10 +- 24 files changed, 513 insertions(+), 986 deletions(-) diff --git a/go_backend/tidal.go b/go_backend/tidal.go index a837380d..582e4ff6 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1961,11 +1961,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { outputExt := strings.TrimSpace(req.OutputExt) if outputExt == "" { - if quality == "HIGH" { - outputExt = ".m4a" - } else { - outputExt = ".flac" - } + outputExt = ".flac" } else if !strings.HasPrefix(outputExt, ".") { outputExt = "." + outputExt } @@ -1979,7 +1975,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { } m4aPath = outputPath } else { - if outputExt == ".m4a" || quality == "HIGH" { + if outputExt == ".m4a" { filename = sanitizeFilename(filename) + ".m4a" outputPath = filepath.Join(req.OutputDir, filename) m4aPath = outputPath @@ -1992,10 +1988,8 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { if fileInfo, statErr := os.Stat(outputPath); statErr == nil && fileInfo.Size() > 0 { return TidalDownloadResult{FilePath: "EXISTS:" + outputPath}, nil } - if quality != "HIGH" { - if fileInfo, statErr := os.Stat(m4aPath); statErr == nil && fileInfo.Size() > 0 { - return TidalDownloadResult{FilePath: "EXISTS:" + m4aPath}, nil - } + if fileInfo, statErr := os.Stat(m4aPath); statErr == nil && fileInfo.Size() > 0 { + return TidalDownloadResult{FilePath: "EXISTS:" + m4aPath}, nil } } @@ -2151,27 +2145,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { fmt.Println("[Tidal] No lyrics available from parallel fetch") } } else if (isSafOutput && actualExt == ".m4a") || (!isSafOutput && strings.HasSuffix(actualOutputPath, ".m4a")) { - if quality == "HIGH" { - GoLog("[Tidal] HIGH quality M4A - skipping metadata embedding (file from server is already valid)\n") - - if req.EmbedMetadata && req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { - lyricsMode := req.LyricsMode - if lyricsMode == "" { - lyricsMode = "embed" - } - - if !isSafOutput && (lyricsMode == "external" || lyricsMode == "both") { - GoLog("[Tidal] Saving external LRC file for M4A (mode: %s)...\n", lyricsMode) - if lrcPath, lrcErr := SaveLRCFile(actualOutputPath, parallelResult.LyricsLRC); lrcErr != nil { - GoLog("[Tidal] Warning: failed to save LRC file: %v\n", lrcErr) - } else { - GoLog("[Tidal] LRC file saved: %s\n", lrcPath) - } - } - } - } else { - fmt.Println("[Tidal] Skipping metadata embedding for M4A file (will be handled after FFmpeg conversion)") - } + fmt.Println("[Tidal] Skipping metadata embedding for M4A file (will be handled after FFmpeg conversion)") } if !isSafOutput { @@ -2181,10 +2155,6 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { bitDepth := downloadInfo.BitDepth sampleRate := downloadInfo.SampleRate lyricsLRC := "" - if quality == "HIGH" { - bitDepth = 0 - sampleRate = 44100 - } if req.EmbedMetadata && req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { lyricsLRC = parallelResult.LyricsLRC } diff --git a/go_backend/youtube.go b/go_backend/youtube.go index 58e5d751..3bb65f5a 100644 --- a/go_backend/youtube.go +++ b/go_backend/youtube.go @@ -29,6 +29,7 @@ var ( type YouTubeQuality string const ( + YouTubeQualityOpus320 YouTubeQuality = "opus_320" YouTubeQualityOpus256 YouTubeQuality = "opus_256" YouTubeQualityOpus128 YouTubeQuality = "opus_128" YouTubeQualityMP3128 YouTubeQuality = "mp3_128" @@ -37,7 +38,7 @@ const ( ) var ( - youtubeOpusSupportedBitrates = []int{128, 256} + youtubeOpusSupportedBitrates = []int{128, 256, 320} youtubeMp3SupportedBitrates = []int{128, 256, 320} ) @@ -146,6 +147,8 @@ func parseYouTubeQualityInput(raw string) (format string, bitrate int, normalize switch normalizedRaw { case "opus_256", "opus256", "opus": return "opus", 256, YouTubeQualityOpus256 + case "opus_320", "opus320": + return "opus", 320, YouTubeQualityOpus320 case "opus_128", "opus128": return "opus", 128, YouTubeQualityOpus128 case "mp3_320", "mp3320", "mp3", "": diff --git a/go_backend/youtube_quality_test.go b/go_backend/youtube_quality_test.go index cb77e5bb..e0f2ebbf 100644 --- a/go_backend/youtube_quality_test.go +++ b/go_backend/youtube_quality_test.go @@ -30,8 +30,8 @@ func TestParseYouTubeQualityInput_Mp3NormalizesToSupportedBitrates(t *testing.T) func TestParseYouTubeQualityInput_PicksNearestSupportedBitrate(t *testing.T) { _, opusBitrate, _ := parseYouTubeQualityInput("opus_999") - if opusBitrate != 256 { - t.Fatalf("expected opus normalization to 256, got %d", opusBitrate) + if opusBitrate != 320 { + t.Fatalf("expected opus normalization to 320, got %d", opusBitrate) } _, mp3Bitrate, _ := parseYouTubeQualityInput("mp3_1") @@ -39,3 +39,16 @@ func TestParseYouTubeQualityInput_PicksNearestSupportedBitrate(t *testing.T) { t.Fatalf("expected mp3 normalization to 128, got %d", mp3Bitrate) } } + +func TestParseYouTubeQualityInput_Opus320(t *testing.T) { + format, bitrate, normalized := parseYouTubeQualityInput("opus_320") + if format != "opus" { + t.Fatalf("expected opus format, got %s", format) + } + if bitrate != 320 { + t.Fatalf("expected 320 bitrate, got %d", bitrate) + } + if normalized != YouTubeQualityOpus320 { + t.Fatalf("expected %s normalized, got %s", YouTubeQualityOpus320, normalized) + } +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ce8174e2..ae9a4cc0 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3838,6 +3838,36 @@ abstract class AppLocalizations { /// **'FFmpeg metadata embed failed'** String get trackReEnrichFfmpegFailed; + /// Action/button label for queueing FLAC redownloads for local tracks + /// + /// In en, this message translates to: + /// **'Queue FLAC'** + String get queueFlacAction; + + /// Confirmation dialog body before queueing FLAC redownloads for local tracks + /// + /// In en, this message translates to: + /// **'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n{count} selected'** + String queueFlacConfirmMessage(int count); + + /// Snackbar while resolving remote matches for local FLAC redownloads + /// + /// In en, this message translates to: + /// **'Finding FLAC matches... ({current}/{total})'** + String queueFlacFindingProgress(int current, int total); + + /// Snackbar when no safe FLAC redownload matches were found + /// + /// In en, this message translates to: + /// **'No reliable online matches found for the selection'** + String get queueFlacNoReliableMatches; + + /// Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped + /// + /// In en, this message translates to: + /// **'Added {addedCount} tracks to queue, skipped {skippedCount}'** + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount); + /// Snackbar when save operation fails /// /// In en, this message translates to: @@ -4602,18 +4632,6 @@ abstract class AppLocalizations { /// **'Select a built-in service to enable'** String get downloadSelectServiceToEnable; - /// Quality option label for Tidal lossy 320kbps - /// - /// In en, this message translates to: - /// **'Lossy 320kbps'** - String get downloadLossy320; - - /// Setting title to pick output format for Tidal lossy downloads - /// - /// In en, this message translates to: - /// **'Lossy Format'** - String get downloadLossyFormat; - /// Info hint when non-Tidal/Qobuz service is selected /// /// In en, this message translates to: @@ -4740,54 +4758,6 @@ abstract class AppLocalizations { /// **'Auto'** String get downloadMusixmatchAuto; - /// Title of the Tidal lossy format picker bottom sheet - /// - /// In en, this message translates to: - /// **'Lossy 320kbps Format'** - String get downloadLossy320Format; - - /// Description in the Tidal lossy format picker - /// - /// In en, this message translates to: - /// **'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'** - String get downloadLossy320FormatDesc; - - /// Tidal lossy format option - MP3 320kbps - /// - /// In en, this message translates to: - /// **'MP3 320kbps'** - String get downloadLossyMp3; - - /// Subtitle for MP3 320kbps option - /// - /// In en, this message translates to: - /// **'Best compatibility, ~10MB per track'** - String get downloadLossyMp3Subtitle; - - /// Tidal lossy format option - Opus 256kbps - /// - /// In en, this message translates to: - /// **'Opus 256kbps'** - String get downloadLossyOpus256; - - /// Subtitle for Opus 256kbps option - /// - /// In en, this message translates to: - /// **'Best quality Opus, ~8MB per track'** - String get downloadLossyOpus256Subtitle; - - /// Tidal lossy format option - Opus 128kbps - /// - /// In en, this message translates to: - /// **'Opus 128kbps'** - String get downloadLossyOpus128; - - /// Subtitle for Opus 128kbps option - /// - /// In en, this message translates to: - /// **'Smallest size, ~4MB per track'** - String get downloadLossyOpus128Subtitle; - /// Subtitle for 'Any' network mode option /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b913d844..ce489e3d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2172,6 +2172,28 @@ class AppLocalizationsDe extends AppLocalizations { String get trackReEnrichFfmpegFailed => 'FFmpeg Metadaten-Einbettung fehlgeschlagen'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Fehler: $error'; @@ -2666,12 +2688,6 @@ class AppLocalizationsDe extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2748,32 +2764,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 07e3ca2d..bd280e94 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsEn extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 8a5a974b..81ce15ff 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsEs extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsEs extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 68d677e1..b35b22d0 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2147,6 +2147,28 @@ class AppLocalizationsFr extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2640,12 +2662,6 @@ class AppLocalizationsFr extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2722,32 +2738,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 5da28983..e36316e9 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsHi extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsHi extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 7fc8d1de..68bffbe5 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2152,6 +2152,28 @@ class AppLocalizationsId extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Antrekan FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Cari kecocokan online untuk track yang dipilih lalu antrekan download FLAC.\n\nFile yang sudah ada tidak akan diubah atau dihapus.\n\nHanya kecocokan dengan keyakinan tinggi yang akan diantrikan otomatis.\n\n$count dipilih'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Mencari kecocokan FLAC... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'Tidak ada kecocokan online yang cukup meyakinkan untuk pilihan ini'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Menambahkan $addedCount track ke antrean, melewati $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2645,12 +2667,6 @@ class AppLocalizationsId extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2727,32 +2743,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 04a7a0c2..8145c794 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2132,6 +2132,28 @@ class AppLocalizationsJa extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return '失敗: $error'; @@ -2625,12 +2647,6 @@ class AppLocalizationsJa extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2707,32 +2723,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 80e8a1df..66d21aec 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2125,6 +2125,28 @@ class AppLocalizationsKo extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2618,12 +2640,6 @@ class AppLocalizationsKo extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2700,32 +2716,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index af603f60..e87906ac 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsNl extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsNl extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index fc40c186..466572ce 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsPt extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsPt extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 7a542295..dd460698 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2198,6 +2198,28 @@ class AppLocalizationsRu extends AppLocalizations { String get trackReEnrichFfmpegFailed => 'Ошибка встраивания метаданных FFmpeg'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Ошибка: $error'; @@ -2697,12 +2719,6 @@ class AppLocalizationsRu extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2779,32 +2795,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 1e5727bf..6e1a37f1 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2157,6 +2157,28 @@ class AppLocalizationsTr extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2650,12 +2672,6 @@ class AppLocalizationsTr extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2732,32 +2748,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 68381882..9248d997 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsZh extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsZh extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index bc3f2e13..3de4ec31 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2819,6 +2819,47 @@ "@trackReEnrichFfmpegFailed": { "description": "Snackbar when FFmpeg embed fails for MP3/Opus" }, + "queueFlacAction": "Queue FLAC", + "@queueFlacAction": { + "description": "Action/button label for queueing FLAC redownloads for local tracks" + }, + "queueFlacConfirmMessage": "Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n{count} selected", + "@queueFlacConfirmMessage": { + "description": "Confirmation dialog body before queueing FLAC redownloads for local tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueFlacFindingProgress": "Finding FLAC matches... ({current}/{total})", + "@queueFlacFindingProgress": { + "description": "Snackbar while resolving remote matches for local FLAC redownloads", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "queueFlacNoReliableMatches": "No reliable online matches found for the selection", + "@queueFlacNoReliableMatches": { + "description": "Snackbar when no safe FLAC redownload matches were found" + }, + "queueFlacQueuedWithSkipped": "Added {addedCount} tracks to queue, skipped {skippedCount}", + "@queueFlacQueuedWithSkipped": { + "description": "Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped", + "placeholders": { + "addedCount": { + "type": "int" + }, + "skippedCount": { + "type": "int" + } + } + }, "trackSaveFailed": "Failed: {error}", "@trackSaveFailed": { "description": "Snackbar when save operation fails", @@ -3513,14 +3554,7 @@ "@downloadSelectServiceToEnable": { "description": "Hint shown instead of Ask-quality subtitle when no built-in service selected" }, - "downloadLossy320": "Lossy 320kbps", - "@downloadLossy320": { - "description": "Quality option label for Tidal lossy 320kbps" - }, - "downloadLossyFormat": "Lossy Format", - "@downloadLossyFormat": { - "description": "Setting title to pick output format for Tidal lossy downloads" - }, + "downloadSelectTidalQobuz": "Select Tidal or Qobuz above to configure quality", "@downloadSelectTidalQobuz": { "description": "Info hint when non-Tidal/Qobuz service is selected" @@ -3606,38 +3640,7 @@ "@downloadMusixmatchAuto": { "description": "Button to reset Musixmatch language to automatic" }, - "downloadLossy320Format": "Lossy 320kbps Format", - "@downloadLossy320Format": { - "description": "Title of the Tidal lossy format picker bottom sheet" - }, - "downloadLossy320FormatDesc": "Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.", - "@downloadLossy320FormatDesc": { - "description": "Description in the Tidal lossy format picker" - }, - "downloadLossyMp3": "MP3 320kbps", - "@downloadLossyMp3": { - "description": "Tidal lossy format option - MP3 320kbps" - }, - "downloadLossyMp3Subtitle": "Best compatibility, ~10MB per track", - "@downloadLossyMp3Subtitle": { - "description": "Subtitle for MP3 320kbps option" - }, - "downloadLossyOpus256": "Opus 256kbps", - "@downloadLossyOpus256": { - "description": "Tidal lossy format option - Opus 256kbps" - }, - "downloadLossyOpus256Subtitle": "Best quality Opus, ~8MB per track", - "@downloadLossyOpus256Subtitle": { - "description": "Subtitle for Opus 256kbps option" - }, - "downloadLossyOpus128": "Opus 128kbps", - "@downloadLossyOpus128": { - "description": "Tidal lossy format option - Opus 128kbps" - }, - "downloadLossyOpus128Subtitle": "Smallest size, ~4MB per track", - "@downloadLossyOpus128Subtitle": { - "description": "Subtitle for Opus 128kbps option" - }, + "downloadNetworkAnySubtitle": "WiFi + Mobile Data", "@downloadNetworkAnySubtitle": { "description": "Subtitle for 'Any' network mode option" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 67e3e9ac..9bf2ee10 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -38,10 +38,8 @@ class AppSettings { final bool showExtensionStore; final String locale; final String lyricsMode; - final String - tidalHighFormat; // Format for Tidal HIGH quality: 'mp3_320', 'opus_256', or 'opus_128' final int - youtubeOpusBitrate; // YouTube Opus bitrate (supported: 128/256 kbps) + youtubeOpusBitrate; // YouTube Opus bitrate (supported: 128/256/320 kbps) final int youtubeMp3Bitrate; // YouTube MP3 bitrate (supported: 128/256/320 kbps) final bool @@ -114,7 +112,6 @@ class AppSettings { this.showExtensionStore = true, this.locale = 'system', this.lyricsMode = 'embed', - this.tidalHighFormat = 'mp3_320', this.youtubeOpusBitrate = 256, this.youtubeMp3Bitrate = 320, this.useAllFilesAccess = false, @@ -178,7 +175,6 @@ class AppSettings { bool? showExtensionStore, String? locale, String? lyricsMode, - String? tidalHighFormat, int? youtubeOpusBitrate, int? youtubeMp3Bitrate, bool? useAllFilesAccess, @@ -241,7 +237,6 @@ class AppSettings { showExtensionStore: showExtensionStore ?? this.showExtensionStore, locale: locale ?? this.locale, lyricsMode: lyricsMode ?? this.lyricsMode, - tidalHighFormat: tidalHighFormat ?? this.tidalHighFormat, youtubeOpusBitrate: youtubeOpusBitrate ?? this.youtubeOpusBitrate, youtubeMp3Bitrate: youtubeMp3Bitrate ?? this.youtubeMp3Bitrate, useAllFilesAccess: useAllFilesAccess ?? this.useAllFilesAccess, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index c3eecb50..99a5114d 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -44,7 +44,6 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( showExtensionStore: json['showExtensionStore'] as bool? ?? true, locale: json['locale'] as String? ?? 'system', lyricsMode: json['lyricsMode'] as String? ?? 'embed', - tidalHighFormat: json['tidalHighFormat'] as String? ?? 'mp3_320', youtubeOpusBitrate: (json['youtubeOpusBitrate'] as num?)?.toInt() ?? 256, youtubeMp3Bitrate: (json['youtubeMp3Bitrate'] as num?)?.toInt() ?? 320, useAllFilesAccess: json['useAllFilesAccess'] as bool? ?? false, @@ -119,7 +118,6 @@ Map _$AppSettingsToJson( 'showExtensionStore': instance.showExtensionStore, 'locale': instance.locale, 'lyricsMode': instance.lyricsMode, - 'tidalHighFormat': instance.tidalHighFormat, 'youtubeOpusBitrate': instance.youtubeOpusBitrate, 'youtubeMp3Bitrate': instance.youtubeMp3Bitrate, 'useAllFilesAccess': instance.useAllFilesAccess, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index df46e084..a01b8e88 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1840,7 +1840,7 @@ class DownloadQueueNotifier extends Notifier { return '.opus'; } if (service.toLowerCase() == 'tidal' && quality == 'HIGH') { - return '.m4a'; + return '.flac'; // HIGH quality no longer available; fallback to FLAC } return '.flac'; } @@ -2383,7 +2383,8 @@ class DownloadQueueNotifier extends Notifier { backendResult['album_artist'] as String?, ); - final hasOverrides = backendTrackNum != null || + final hasOverrides = + backendTrackNum != null || backendDiscNum != null || backendYear != null || backendAlbum != null || @@ -3612,6 +3613,11 @@ class DownloadQueueNotifier extends Notifier { 'Quality: $quality${item.qualityOverride != null ? ' (override)' : ''}', ); } + + if (!useSaf) { + await _ensureDirExists(outputDir, label: 'Output folder'); + } + _log.d('Output dir: $outputDir'); final normalizedTrackNumber = @@ -3903,7 +3909,6 @@ class DownloadQueueNotifier extends Notifier { isContentUriPath && effectiveSafMode && actualService == 'tidal' && - quality != 'HIGH' && filePath.endsWith('.flac') && (mimeType == null || mimeType.contains('flac')); @@ -3918,73 +3923,50 @@ class DownloadQueueNotifier extends Notifier { final currentFilePath = filePath; if (isContentUriPath && effectiveSafMode) { - if (quality == 'HIGH') { - final tidalHighFormat = settings.tidalHighFormat; - _log.i( - 'Tidal HIGH quality (SAF), converting M4A to $tidalHighFormat...', - ); - - final tempPath = await _copySafToTemp(currentFilePath); - if (tempPath != null) { - String? convertedPath; - try { + _log.d('M4A file detected (SAF), converting to FLAC...'); + final tempPath = await _copySafToTemp(currentFilePath); + if (tempPath != null) { + String? flacPath; + try { + final length = await File(tempPath).length(); + if (length < 1024) { + _log.w('Temp M4A is too small (<1KB), skipping conversion'); + } else { updateItemStatus( item.id, DownloadStatus.downloading, progress: 0.95, ); - - final format = tidalHighFormat.startsWith('opus') - ? 'opus' - : 'mp3'; - convertedPath = await FFmpegService.convertM4aToLossy( - tempPath, - format: format, - bitrate: tidalHighFormat, - deleteOriginal: false, - ); - - if (convertedPath != null) { - _log.i( - 'Successfully converted M4A to $format (temp): $convertedPath', - ); - _log.i('Embedding metadata to $format...'); - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.99, + flacPath = await FFmpegService.convertM4aToFlac(tempPath); + if (flacPath != null) { + _log.d('Converted to FLAC (temp): $flacPath'); + _log.d('Embedding metadata and cover to converted FLAC...'); + final finalTrack = _buildTrackForMetadataEmbedding( + trackToDownload, + result, + resolvedAlbumArtist, ); final backendGenre = result['genre'] as String?; final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; - if (format == 'mp3') { - await _embedMetadataToMp3( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - } else { - await _embedMetadataToOpus( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - } + await _embedMetadataAndCover( + flacPath, + finalTrack, + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + writeExternalLrc: false, + ); - final newExt = format == 'opus' ? '.opus' : '.mp3'; - final newFileName = '${safBaseName ?? 'track'}$newExt'; + final newFileName = '${safBaseName ?? 'track'}.flac'; final newUri = await _writeTempToSaf( treeUri: settings.downloadTreeUri, relativeDir: effectiveOutputDir, fileName: newFileName, - mimeType: _mimeTypeForExt(newExt), - srcPath: convertedPath, + mimeType: _mimeTypeForExt('.flac'), + srcPath: flacPath, ); if (newUri != null) { @@ -3993,58 +3975,60 @@ class DownloadQueueNotifier extends Notifier { } filePath = newUri; finalSafFileName = newFileName; - final bitrateDisplay = tidalHighFormat.contains('_') - ? '${tidalHighFormat.split('_').last}kbps' - : '320kbps'; - actualQuality = '${format.toUpperCase()} $bitrateDisplay'; } else { - _log.w( - 'Failed to write converted $format to SAF, keeping M4A', - ); - actualQuality = 'AAC 320kbps'; + _log.w('Failed to write FLAC to SAF, keeping M4A'); } } else { - _log.w( - 'M4A to $format conversion failed, keeping M4A file', - ); - actualQuality = 'AAC 320kbps'; - } - } catch (e) { - _log.w('SAF M4A conversion failed: $e'); - actualQuality = 'AAC 320kbps'; - } finally { - // Clean up temp files - try { - await File(tempPath).delete(); - } catch (_) {} - if (convertedPath != null) { - try { - await File(convertedPath).delete(); - } catch (_) {} + _log.w('FFmpeg conversion returned null, keeping M4A file'); } } - } - } else { - _log.d('M4A file detected (SAF), converting to FLAC...'); - final tempPath = await _copySafToTemp(currentFilePath); - if (tempPath != null) { - String? flacPath; + } catch (e) { + _log.w('SAF M4A->FLAC conversion failed: $e'); + } finally { + // Clean up temp files try { - final length = await File(tempPath).length(); - if (length < 1024) { - _log.w('Temp M4A is too small (<1KB), skipping conversion'); - } else { - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.95, - ); - flacPath = await FFmpegService.convertM4aToFlac(tempPath); - if (flacPath != null) { - _log.d('Converted to FLAC (temp): $flacPath'); - _log.d( - 'Embedding metadata and cover to converted FLAC...', - ); + await File(tempPath).delete(); + } catch (_) {} + if (flacPath != null) { + try { + await File(flacPath).delete(); + } catch (_) {} + } + } + } + } else { + _log.d( + 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', + ); + + try { + final file = File(currentFilePath); + if (!await file.exists()) { + _log.e('File does not exist at path: $filePath'); + } else { + final length = await file.length(); + _log.i('File size before conversion: ${length / 1024} KB'); + + if (length < 1024) { + _log.w( + 'File is too small (<1KB), skipping conversion. Download might be corrupt.', + ); + } else { + updateItemStatus( + item.id, + DownloadStatus.downloading, + progress: 0.95, + ); + final flacPath = await FFmpegService.convertM4aToFlac( + currentFilePath, + ); + + if (flacPath != null) { + filePath = flacPath; + _log.d('Converted to FLAC: $flacPath'); + + _log.d('Embedding metadata and cover to converted FLAC...'); + try { final finalTrack = _buildTrackForMetadataEmbedding( trackToDownload, result, @@ -4055,201 +4039,32 @@ class DownloadQueueNotifier extends Notifier { final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; + if (backendGenre != null || + backendLabel != null || + backendCopyright != null) { + _log.d( + 'Extended metadata from backend - Genre: $backendGenre, Label: $backendLabel, Copyright: $backendCopyright', + ); + } + await _embedMetadataAndCover( flacPath, finalTrack, genre: backendGenre ?? genre, label: backendLabel ?? label, copyright: backendCopyright, - writeExternalLrc: false, - ); - - final newFileName = '${safBaseName ?? 'track'}.flac'; - final newUri = await _writeTempToSaf( - treeUri: settings.downloadTreeUri, - relativeDir: effectiveOutputDir, - fileName: newFileName, - mimeType: _mimeTypeForExt('.flac'), - srcPath: flacPath, - ); - - if (newUri != null) { - if (newUri != currentFilePath) { - await _deleteSafFile(currentFilePath); - } - filePath = newUri; - finalSafFileName = newFileName; - } else { - _log.w('Failed to write FLAC to SAF, keeping M4A'); - } - } else { - _log.w( - 'FFmpeg conversion returned null, keeping M4A file', ); + _log.d('Metadata and cover embedded successfully'); + } catch (e) { + _log.w('Warning: Failed to embed metadata/cover: $e'); } - } - } catch (e) { - _log.w('SAF M4A->FLAC conversion failed: $e'); - } finally { - // Clean up temp files - try { - await File(tempPath).delete(); - } catch (_) {} - if (flacPath != null) { - try { - await File(flacPath).delete(); - } catch (_) {} - } - } - } - } - } else { - if (quality == 'HIGH') { - final tidalHighFormat = settings.tidalHighFormat; - _log.i( - 'Tidal HIGH quality download, converting M4A to $tidalHighFormat...', - ); - - try { - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.95, - ); - - final format = tidalHighFormat.startsWith('opus') - ? 'opus' - : 'mp3'; - final convertedPath = await FFmpegService.convertM4aToLossy( - currentFilePath, - format: format, - bitrate: tidalHighFormat, - deleteOriginal: true, - ); - - if (convertedPath != null) { - filePath = convertedPath; - final bitrateDisplay = tidalHighFormat.contains('_') - ? '${tidalHighFormat.split('_').last}kbps' - : '320kbps'; - actualQuality = '${format.toUpperCase()} $bitrateDisplay'; - _log.i( - 'Successfully converted M4A to $format: $convertedPath', - ); - - _log.i('Embedding metadata to $format...'); - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.99, - ); - - final backendGenre = result['genre'] as String?; - final backendLabel = result['label'] as String?; - final backendCopyright = result['copyright'] as String?; - - if (format == 'mp3') { - await _embedMetadataToMp3( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); } else { - await _embedMetadataToOpus( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - } - _log.d('Metadata embedded successfully'); - } else { - _log.w('M4A to $format conversion failed, keeping M4A file'); - actualQuality = 'AAC 320kbps'; - } - } catch (e) { - _log.w('M4A conversion process failed: $e, keeping M4A file'); - actualQuality = 'AAC 320kbps'; - } - } else { - _log.d( - 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', - ); - - try { - final file = File(currentFilePath); - if (!await file.exists()) { - _log.e('File does not exist at path: $filePath'); - } else { - final length = await file.length(); - _log.i('File size before conversion: ${length / 1024} KB'); - - if (length < 1024) { - _log.w( - 'File is too small (<1KB), skipping conversion. Download might be corrupt.', - ); - } else { - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.95, - ); - final flacPath = await FFmpegService.convertM4aToFlac( - currentFilePath, - ); - - if (flacPath != null) { - filePath = flacPath; - _log.d('Converted to FLAC: $flacPath'); - - _log.d( - 'Embedding metadata and cover to converted FLAC...', - ); - try { - final finalTrack = _buildTrackForMetadataEmbedding( - trackToDownload, - result, - resolvedAlbumArtist, - ); - - final backendGenre = result['genre'] as String?; - final backendLabel = result['label'] as String?; - final backendCopyright = result['copyright'] as String?; - - if (backendGenre != null || - backendLabel != null || - backendCopyright != null) { - _log.d( - 'Extended metadata from backend - Genre: $backendGenre, Label: $backendLabel, Copyright: $backendCopyright', - ); - } - - await _embedMetadataAndCover( - flacPath, - finalTrack, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - _log.d('Metadata and cover embedded successfully'); - } catch (e) { - _log.w('Warning: Failed to embed metadata/cover: $e'); - } - } else { - _log.w( - 'FFmpeg conversion returned null, keeping M4A file', - ); - } + _log.w('FFmpeg conversion returned null, keeping M4A file'); } } - } catch (e) { - _log.w( - 'FFmpeg conversion process failed: $e, keeping M4A file', - ); } + } catch (e) { + _log.w('FFmpeg conversion process failed: $e, keeping M4A file'); } } } else if (metadataEmbeddingEnabled && diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index ed44fea4..e98a70e0 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -1,20 +1,22 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/logger.dart'; const _settingsKey = 'app_settings'; const _migrationVersionKey = 'settings_migration_version'; -const _currentMigrationVersion = 5; +const _currentMigrationVersion = 6; const _spotifyClientSecretKey = 'spotify_client_secret'; final _log = AppLogger('SettingsProvider'); class SettingsNotifier extends Notifier { - static const List _youtubeOpusSupportedBitrates = [128, 256]; + static const List _youtubeOpusSupportedBitrates = [128, 256, 320]; static const List _youtubeMp3SupportedBitrates = [128, 256, 320]; static final RegExp _isoRegionPattern = RegExp(r'^[A-Z]{2}$'); @@ -37,6 +39,7 @@ class SettingsNotifier extends Notifier { state = AppSettings.fromJson(jsonDecode(json)); await _runMigrations(prefs); + await _normalizeIosDownloadDirectoryIfNeeded(); await _normalizeYouTubeBitratesIfNeeded(); await _normalizeSongLinkRegionIfNeeded(); } @@ -114,6 +117,10 @@ class SettingsNotifier extends Notifier { useCustomSpotifyCredentials: false, ); } + // Migration 6: Tidal HIGH quality removed — migrate to LOSSLESS + if (state.audioQuality == 'HIGH') { + state = state.copyWith(audioQuality: 'LOSSLESS'); + } state = state.copyWith(lastSeenVersion: AppInfo.version); await prefs.setInt(_migrationVersionKey, _currentMigrationVersion); await _saveSettings(); @@ -189,6 +196,20 @@ class SettingsNotifier extends Notifier { await _saveSettings(); } + Future _normalizeIosDownloadDirectoryIfNeeded() async { + if (!Platform.isIOS) return; + + final currentDir = state.downloadDirectory.trim(); + if (currentDir.isEmpty) return; + + final normalizedDir = await validateOrFixIosPath(currentDir); + if (normalizedDir == currentDir) return; + + _log.i('Normalized iOS download directory: $currentDir -> $normalizedDir'); + state = state.copyWith(downloadDirectory: normalizedDir); + await _saveSettings(); + } + String _normalizeSongLinkRegion(String region) { final normalized = region.trim().toUpperCase(); if (_isoRegionPattern.hasMatch(normalized)) return normalized; @@ -430,11 +451,6 @@ class SettingsNotifier extends Notifier { _saveSettings(); } - void setTidalHighFormat(String format) { - state = state.copyWith(tidalHighFormat: format); - _saveSettings(); - } - void setYoutubeOpusBitrate(int bitrate) { final normalized = _normalizeYouTubeOpusBitrate(bitrate); state = state.copyWith(youtubeOpusBitrate: normalized); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index c502d142..b4a6d1a7 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -300,7 +300,6 @@ class _DownloadSettingsPageState extends ConsumerState { final topPadding = normalizedHeaderTopPadding(context); final isBuiltInService = _builtInServices.contains(settings.defaultService); - final isTidalService = settings.defaultService == 'tidal'; return PopScope( canPop: true, @@ -408,35 +407,8 @@ class _DownloadSettingsPageState extends ConsumerState { onTap: () => ref .read(settingsProvider.notifier) .setAudioQuality('HI_RES_LOSSLESS'), - showDivider: isTidalService, + showDivider: false, ), - // Lossy 320kbps option (Tidal only) - downloads M4A, converts to MP3/Opus - if (isTidalService) - _QualityOption( - title: context.l10n.downloadLossy320, - subtitle: _getTidalHighFormatLabel( - settings.tidalHighFormat, - ), - isSelected: settings.audioQuality == 'HIGH', - onTap: () => ref - .read(settingsProvider.notifier) - .setAudioQuality('HIGH'), - showDivider: false, - ), - if (isTidalService && settings.audioQuality == 'HIGH') - SettingsItem( - icon: Icons.tune, - title: context.l10n.downloadLossyFormat, - subtitle: _getTidalHighFormatLabel( - settings.tidalHighFormat, - ), - onTap: () => _showTidalHighFormatPicker( - context, - ref, - settings.tidalHighFormat, - ), - showDivider: false, - ), ], if (!isBuiltInService) ...[ Padding( @@ -464,12 +436,12 @@ class _DownloadSettingsPageState extends ConsumerState { ], SettingsItem( title: context.l10n.youtubeOpusBitrateTitle, - subtitle: '${settings.youtubeOpusBitrate}kbps (128/256)', + subtitle: '${settings.youtubeOpusBitrate}kbps (128/256/320)', onTap: () => _showYoutubeBitratePicker( context: context, title: context.l10n.youtubeOpusBitrateTitle, currentValue: settings.youtubeOpusBitrate, - options: const [128, 256], + options: const [128, 256, 320], onSave: (value) => ref .read(settingsProvider.notifier) .setYoutubeOpusBitrate(value), @@ -1691,104 +1663,6 @@ class _DownloadSettingsPageState extends ConsumerState { ); } - String _getTidalHighFormatLabel(String format) { - switch (format) { - case 'mp3_320': - return 'MP3 320kbps'; - case 'opus_256': - return 'Opus 256kbps'; - case 'opus_128': - return 'Opus 128kbps'; - default: - return 'MP3 320kbps'; - } - } - - void _showTidalHighFormatPicker( - BuildContext context, - WidgetRef ref, - String current, - ) { - final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, - useRootNavigator: true, - backgroundColor: colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - builder: (context) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - context.l10n.downloadLossy320Format, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - context.l10n.downloadLossy320FormatDesc, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ListTile( - leading: const Icon(Icons.audiotrack), - title: Text(context.l10n.downloadLossyMp3), - subtitle: Text(context.l10n.downloadLossyMp3Subtitle), - trailing: current == 'mp3_320' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('mp3_320'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.graphic_eq), - title: Text(context.l10n.downloadLossyOpus256), - subtitle: Text(context.l10n.downloadLossyOpus256Subtitle), - trailing: current == 'opus_256' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('opus_256'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.graphic_eq), - title: Text(context.l10n.downloadLossyOpus128), - subtitle: Text(context.l10n.downloadLossyOpus128Subtitle), - trailing: current == 'opus_128' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('opus_128'); - Navigator.pop(context); - }, - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } - void _showNetworkModePicker( BuildContext context, WidgetRef ref, diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index 41c8c330..977a31b6 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -23,7 +23,7 @@ class BuiltInService { } /// Default quality options for built-in services -/// Note: Tidal lossy (HIGH) removed - use YouTube for lossy downloads +/// Default quality options for each built-in service const _builtInServices = [ BuiltInService( id: 'tidal', @@ -83,9 +83,9 @@ const _builtInServices = [ label: 'YouTube', qualityOptions: [ QualityOption( - id: 'opus_256', - label: 'Opus 256kbps', - description: 'Best quality lossy (~8MB per track)', + id: 'opus_320', + label: 'Opus 320kbps', + description: 'Best quality lossy (~10MB per track)', ), QualityOption( id: 'mp3_320', @@ -146,7 +146,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget { } class _DownloadServicePickerState extends ConsumerState { - static const List _youtubeOpusSupportedBitrates = [128, 256]; + static const List _youtubeOpusSupportedBitrates = [128, 256, 320]; static const List _youtubeMp3SupportedBitrates = [128, 256, 320]; late String _selectedService; From 29d8a185f96adfc8b9f4ea771d687ef0613a865a Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:18:29 +0700 Subject: [PATCH 095/145] fix: handle nested legacy iOS Documents path in validation Detect and recover from stale sandbox container paths embedded inside the current Documents directory. Extracts helper functions for path suffix normalization and joining to reduce duplication. --- lib/utils/file_access.dart | 50 +++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/lib/utils/file_access.dart b/lib/utils/file_access.dart index 026fa57e..f686bc46 100644 --- a/lib/utils/file_access.dart +++ b/lib/utils/file_access.dart @@ -20,6 +20,22 @@ final _iosLegacyRelativeDocumentsPattern = RegExp( r'^Data/Application/[A-F0-9\-]+/Documents(?:/(.*))?$', caseSensitive: false, ); +final _iosNestedLegacyDocumentsPattern = RegExp( + r'/Documents/Data/Application/[A-F0-9\-]+/Documents(?:/(.*))?$', + caseSensitive: false, +); + +String _normalizeRecoveredIosSuffix(String suffix) { + final trimmed = suffix.trim(); + if (trimmed.isEmpty) return ''; + return trimmed.startsWith('/') ? trimmed.substring(1) : trimmed; +} + +String _joinRecoveredIosPath(String documentsPath, String suffix) { + final normalizedSuffix = _normalizeRecoveredIosSuffix(suffix); + if (normalizedSuffix.isEmpty) return documentsPath; + return '$documentsPath/$normalizedSuffix'; +} /// Checks if a path is a valid writable directory on iOS. /// Returns false if: @@ -43,6 +59,12 @@ bool isValidIosWritablePath(String path) { return false; } + // Reject stale paths where an old sandbox container path has been embedded + // inside the current Documents directory. + if (_iosNestedLegacyDocumentsPattern.hasMatch(path)) { + return false; + } + // Ensure path contains a valid subdirectory (Documents, tmp, Library, etc.) // This handles cases where FilePicker returns container root final containerPattern = RegExp( @@ -70,11 +92,19 @@ Future validateOrFixIosPath( if (!Platform.isIOS) return path; final trimmed = path.trim(); + final docDir = await getApplicationDocumentsDirectory(); + + final nestedLegacyMatch = _iosNestedLegacyDocumentsPattern.firstMatch( + trimmed, + ); + if (nestedLegacyMatch != null) { + return _joinRecoveredIosPath(docDir.path, nestedLegacyMatch.group(1) ?? ''); + } + if (isValidIosWritablePath(trimmed)) { return trimmed; } - final docDir = await getApplicationDocumentsDirectory(); final candidates = []; if (trimmed.isNotEmpty) { @@ -92,14 +122,8 @@ Future validateOrFixIosPath( trimmed, ); if (legacyRelativeMatch != null) { - final suffix = (legacyRelativeMatch.group(1) ?? '').trim(); - final normalizedSuffix = suffix.startsWith('/') - ? suffix.substring(1) - : suffix; candidates.add( - normalizedSuffix.isEmpty - ? docDir.path - : '${docDir.path}/$normalizedSuffix', + _joinRecoveredIosPath(docDir.path, legacyRelativeMatch.group(1) ?? ''), ); } @@ -109,7 +133,7 @@ Future validateOrFixIosPath( final index = trimmed.indexOf(documentsMarker); if (index >= 0) { final suffix = trimmed.substring(index + documentsMarker.length).trim(); - candidates.add(suffix.isEmpty ? docDir.path : '${docDir.path}/$suffix'); + candidates.add(_joinRecoveredIosPath(docDir.path, suffix)); } } @@ -181,6 +205,14 @@ IosPathValidationResult validateIosPath(String path) { ); } + if (_iosNestedLegacyDocumentsPattern.hasMatch(path)) { + return const IosPathValidationResult( + isValid: false, + errorReason: + 'Invalid iOS app folder path. Please choose App Documents or another local folder.', + ); + } + // Check for container root without subdirectory final containerPattern = RegExp( r'/var/mobile/Containers/Data/Application/[A-F0-9\-]+', From 967523bfc6f4ce94d600747a09eabee0eba13d9a Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:18:58 +0700 Subject: [PATCH 096/145] feat: queue FLAC redownloads for local library tracks Add LocalTrackRedownloadService with confidence-scored metadata matching (ISRC, title, artist, album, duration, track/disc number, year) to find reliable online matches for locally-stored tracks. Wire up 'Queue FLAC' selection action in both local_album_screen and queue_tab (library tab). Shows progress snackbar during resolution, skips ambiguous or low-confidence matches, and reports results. Add Indonesian (id) translations for all queueFlac l10n keys. --- lib/l10n/arb/app_id.arb | 43 ++- lib/screens/local_album_screen.dart | 141 +++++++- lib/screens/queue_tab.dart | 156 +++++++- .../local_track_redownload_service.dart | 338 ++++++++++++++++++ 4 files changed, 667 insertions(+), 11 deletions(-) create mode 100644 lib/services/local_track_redownload_service.dart diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index de9d90d0..bab66bf2 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -2755,6 +2755,47 @@ "@trackReEnrichFfmpegFailed": { "description": "Snackbar when FFmpeg embed fails for MP3/Opus" }, + "queueFlacAction": "Antrekan FLAC", + "@queueFlacAction": { + "description": "Action/button label for queueing FLAC redownloads for local tracks" + }, + "queueFlacConfirmMessage": "Cari kecocokan online untuk track yang dipilih lalu antrekan download FLAC.\n\nFile yang sudah ada tidak akan diubah atau dihapus.\n\nHanya kecocokan dengan keyakinan tinggi yang akan diantrikan otomatis.\n\n{count} dipilih", + "@queueFlacConfirmMessage": { + "description": "Confirmation dialog body before queueing FLAC redownloads for local tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueFlacFindingProgress": "Mencari kecocokan FLAC... ({current}/{total})", + "@queueFlacFindingProgress": { + "description": "Snackbar while resolving remote matches for local FLAC redownloads", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "queueFlacNoReliableMatches": "Tidak ada kecocokan online yang cukup meyakinkan untuk pilihan ini", + "@queueFlacNoReliableMatches": { + "description": "Snackbar when no safe FLAC redownload matches were found" + }, + "queueFlacQueuedWithSkipped": "Menambahkan {addedCount} track ke antrean, melewati {skippedCount}", + "@queueFlacQueuedWithSkipped": { + "description": "Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped", + "placeholders": { + "addedCount": { + "type": "int" + }, + "skippedCount": { + "type": "int" + } + } + }, "trackSaveFailed": "Failed: {error}", "@trackSaveFailed": { "description": "Snackbar when save operation fails", @@ -3114,4 +3155,4 @@ "@downloadUseAlbumArtistForFoldersTrackSubtitle": { "description": "Subtitle when Track Artist is used for folder naming" } -} \ No newline at end of file +} diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index e5584b34..122a4bab 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -4,11 +4,15 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/models/track.dart'; +import 'package:spotiflac_android/providers/download_queue_provider.dart'; +import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart'; import 'package:spotiflac_android/services/library_database.dart'; import 'package:spotiflac_android/services/ffmpeg_service.dart'; +import 'package:spotiflac_android/services/local_track_redownload_service.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/playback_provider.dart'; @@ -41,11 +45,10 @@ class _LocalAlbumScreenState extends ConsumerState { void _showCueVirtualTrackSnackBar() { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text(cueVirtualTrackRequiresSplitMessage), - ), + const SnackBar(content: Text(cueVirtualTrackRequiresSplitMessage)), ); } + late List _sortedDiscNumbersCache; late bool _hasMultipleDiscsCache; String? _commonQualityCache; @@ -897,6 +900,127 @@ class _LocalAlbumScreenState extends ConsumerState { return false; } + Future _queueSelectedAsFlac(List allTracks) async { + final tracksById = {for (final t in allTracks) t.id: t}; + final selected = []; + + for (final id in _selectedIds) { + final item = tracksById[id]; + if (item != null) { + selected.add(item); + } + } + + if (selected.isEmpty) { + return; + } + + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(context.l10n.queueFlacAction), + content: Text(context.l10n.queueFlacConfirmMessage(selected.length)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text(context.l10n.dialogCancel), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: Text(context.l10n.queueFlacAction), + ), + ], + ), + ); + + if (confirmed != true || !mounted) { + return; + } + + final settings = ref.read(settingsProvider); + final extensionState = ref.read(extensionProvider); + final includeExtensions = + settings.useExtensionProviders && + extensionState.extensions.any( + (ext) => ext.enabled && ext.hasMetadataProvider, + ); + final targetService = LocalTrackRedownloadService.preferredFlacService( + settings, + ); + final targetQuality = + LocalTrackRedownloadService.preferredFlacQualityForService( + targetService, + ); + + final matchedTracks = []; + var skippedCount = 0; + final total = selected.length; + + for (var i = 0; i < total; i++) { + if (!mounted) break; + + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.queueFlacFindingProgress(i + 1, total), + ), + duration: const Duration(seconds: 30), + ), + ); + + try { + final resolution = await LocalTrackRedownloadService.resolveBestMatch( + selected[i], + includeExtensions: includeExtensions, + ); + if (resolution.canQueue && resolution.match != null) { + matchedTracks.add(resolution.match!); + } else { + skippedCount++; + } + } catch (_) { + skippedCount++; + } + } + + if (!mounted) { + return; + } + + ScaffoldMessenger.of(context).clearSnackBars(); + + if (matchedTracks.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.queueFlacNoReliableMatches)), + ); + return; + } + + ref + .read(downloadQueueProvider.notifier) + .addMultipleToQueue( + matchedTracks, + targetService, + qualityOverride: targetQuality, + ); + + final summary = skippedCount == 0 + ? context.l10n.snackbarAddedTracksToQueue(matchedTracks.length) + : context.l10n.queueFlacQueuedWithSkipped( + matchedTracks.length, + skippedCount, + ); + + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(summary))); + setState(() { + _selectedIds.clear(); + _isSelectionMode = false; + }); + } + Future _reEnrichSelected(List allTracks) async { final tracksById = {for (final t in allTracks) t.id: t}; final selected = []; @@ -1525,6 +1649,17 @@ class _LocalAlbumScreenState extends ConsumerState { Row( children: [ + Expanded( + child: _LocalAlbumSelectionActionButton( + icon: Icons.download_for_offline_outlined, + label: '${context.l10n.queueFlacAction} ($selectedCount)', + onPressed: selectedCount > 0 + ? () => _queueSelectedAsFlac(tracks) + : null, + colorScheme: colorScheme, + ), + ), + const SizedBox(width: 8), Expanded( child: _LocalAlbumSelectionActionButton( icon: Icons.auto_fix_high_outlined, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 5b14ff21..52ca3309 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -17,11 +17,13 @@ import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; +import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/playback_provider.dart'; import 'package:spotiflac_android/services/library_database.dart'; +import 'package:spotiflac_android/services/local_track_redownload_service.dart'; import 'package:spotiflac_android/services/history_database.dart'; import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; @@ -1321,8 +1323,10 @@ class _QueueTabState extends ConsumerState { .where((p) => _selectedPlaylistIds.contains(p.id)) .toList(); - final totalTracks = - selectedPlaylists.fold(0, (sum, p) => sum + p.tracks.length); + final totalTracks = selectedPlaylists.fold( + 0, + (sum, p) => sum + p.tracks.length, + ); if (totalTracks == 0) { ScaffoldMessenger.of(context).showSnackBar( @@ -1336,7 +1340,10 @@ class _QueueTabState extends ConsumerState { builder: (ctx) => AlertDialog( title: Text(ctx.l10n.dialogDownloadAllTitle), content: Text( - ctx.l10n.dialogDownloadPlaylistsMessage(totalTracks, selectedPlaylists.length), + ctx.l10n.dialogDownloadPlaylistsMessage( + totalTracks, + selectedPlaylists.length, + ), ), actions: [ TextButton( @@ -1392,9 +1399,7 @@ class _QueueTabState extends ConsumerState { _exitPlaylistSelectionMode(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.snackbarAddedTracksToQueue(totalTracks), - ), + content: Text(context.l10n.snackbarAddedTracksToQueue(totalTracks)), ), ); } @@ -1547,7 +1552,9 @@ class _QueueTabState extends ConsumerState { icon: const Icon(Icons.download_rounded), label: Text( selectedCount > 0 - ? context.l10n.bulkDownloadPlaylistsButton(selectedCount) + ? context.l10n.bulkDownloadPlaylistsButton( + selectedCount, + ) : context.l10n.bulkDownloadSelectPlaylists, ), style: FilledButton.styleFrom( @@ -4477,6 +4484,127 @@ class _QueueTabState extends ConsumerState { return false; } + Future _queueSelectedLocalAsFlac( + List allItems, + ) async { + final selectedItems = _selectedItemsFromAll(allItems); + final selectedLocalItems = selectedItems + .map((item) => item.localItem) + .whereType() + .toList(growable: false); + + if (selectedLocalItems.isEmpty) { + return; + } + + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(context.l10n.queueFlacAction), + content: Text( + context.l10n.queueFlacConfirmMessage(selectedLocalItems.length), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text(context.l10n.dialogCancel), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: Text(context.l10n.queueFlacAction), + ), + ], + ), + ); + + if (confirmed != true || !mounted) { + return; + } + + final settings = ref.read(settingsProvider); + final extensionState = ref.read(extensionProvider); + final includeExtensions = + settings.useExtensionProviders && + extensionState.extensions.any( + (ext) => ext.enabled && ext.hasMetadataProvider, + ); + final targetService = LocalTrackRedownloadService.preferredFlacService( + settings, + ); + final targetQuality = + LocalTrackRedownloadService.preferredFlacQualityForService( + targetService, + ); + + final matchedTracks = []; + var skippedCount = 0; + final total = selectedLocalItems.length; + + for (var i = 0; i < total; i++) { + if (!mounted) break; + + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.queueFlacFindingProgress(i + 1, total), + ), + duration: const Duration(seconds: 30), + ), + ); + + try { + final resolution = await LocalTrackRedownloadService.resolveBestMatch( + selectedLocalItems[i], + includeExtensions: includeExtensions, + ); + if (resolution.canQueue && resolution.match != null) { + matchedTracks.add(resolution.match!); + } else { + skippedCount++; + } + } catch (_) { + skippedCount++; + } + } + + if (!mounted) { + return; + } + + ScaffoldMessenger.of(context).clearSnackBars(); + + if (matchedTracks.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.queueFlacNoReliableMatches)), + ); + return; + } + + ref + .read(downloadQueueProvider.notifier) + .addMultipleToQueue( + matchedTracks, + targetService, + qualityOverride: targetQuality, + ); + + final summary = skippedCount == 0 + ? context.l10n.snackbarAddedTracksToQueue(matchedTracks.length) + : context.l10n.queueFlacQueuedWithSkipped( + matchedTracks.length, + skippedCount, + ); + + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(summary))); + setState(() { + _selectedIds.clear(); + _isSelectionMode = false; + }); + } + Future _reEnrichSelectedLocalFromQueue( List allItems, ) async { @@ -5244,6 +5372,20 @@ class _QueueTabState extends ConsumerState { // Action buttons row: Share/Re-enrich, Convert, Delete Row( children: [ + if (localOnlySelection) ...[ + Expanded( + child: _SelectionActionButton( + icon: Icons.download_for_offline_outlined, + label: + '${context.l10n.queueFlacAction} ($selectedCount)', + onPressed: selectedCount > 0 + ? () => _queueSelectedLocalAsFlac(unifiedItems) + : null, + colorScheme: colorScheme, + ), + ), + const SizedBox(width: 8), + ], Expanded( child: _SelectionActionButton( icon: localOnlySelection diff --git a/lib/services/local_track_redownload_service.dart b/lib/services/local_track_redownload_service.dart new file mode 100644 index 00000000..4b8500c7 --- /dev/null +++ b/lib/services/local_track_redownload_service.dart @@ -0,0 +1,338 @@ +import 'package:spotiflac_android/models/settings.dart'; +import 'package:spotiflac_android/models/track.dart'; +import 'package:spotiflac_android/services/library_database.dart'; +import 'package:spotiflac_android/services/platform_bridge.dart'; + +class LocalTrackRedownloadResolution { + final LocalLibraryItem localItem; + final Track? match; + final int score; + final String reason; + + const LocalTrackRedownloadResolution({ + required this.localItem, + required this.match, + required this.score, + required this.reason, + }); + + bool get canQueue => match != null; +} + +class LocalTrackRedownloadService { + static const int _minimumConfidenceScore = 85; + static const int _ambiguousScoreGap = 8; + + static Future resolveBestMatch( + LocalLibraryItem item, { + required bool includeExtensions, + }) async { + final query = _buildSearchQuery(item); + final rawResults = await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 10, + includeExtensions: includeExtensions, + ); + + if (rawResults.isEmpty) { + return LocalTrackRedownloadResolution( + localItem: item, + match: null, + score: 0, + reason: 'No candidates found', + ); + } + + final scored = + rawResults + .map( + (raw) => ( + track: _parseSearchTrack(raw), + score: _scoreMatch(item, raw), + ), + ) + .where((entry) => entry.track.name.trim().isNotEmpty) + .toList(growable: false) + ..sort((a, b) => b.score.compareTo(a.score)); + + if (scored.isEmpty) { + return LocalTrackRedownloadResolution( + localItem: item, + match: null, + score: 0, + reason: 'No usable candidates found', + ); + } + + final best = scored.first; + final runnerUp = scored.length > 1 ? scored[1] : null; + final exactIsrc = + _normalizedIsrc(item.isrc) != null && + _normalizedIsrc(item.isrc) == _normalizedIsrc(best.track.isrc); + final isAmbiguous = + !exactIsrc && + runnerUp != null && + best.score < (_minimumConfidenceScore + 10) && + (best.score - runnerUp.score) <= _ambiguousScoreGap; + + if (!exactIsrc && (best.score < _minimumConfidenceScore || isAmbiguous)) { + return LocalTrackRedownloadResolution( + localItem: item, + match: null, + score: best.score, + reason: isAmbiguous ? 'Ambiguous match' : 'Low-confidence match', + ); + } + + return LocalTrackRedownloadResolution( + localItem: item, + match: best.track, + score: best.score, + reason: exactIsrc ? 'Exact ISRC match' : 'High-confidence metadata match', + ); + } + + static String preferredFlacService(AppSettings settings) { + switch (settings.defaultService.toLowerCase()) { + case 'tidal': + case 'qobuz': + case 'deezer': + return settings.defaultService.toLowerCase(); + default: + return 'tidal'; + } + } + + static String preferredFlacQualityForService(String service) { + return service.toLowerCase() == 'deezer' ? 'FLAC' : 'LOSSLESS'; + } + + static String _buildSearchQuery(LocalLibraryItem item) { + final artist = _primaryArtist(item.artistName); + final album = item.albumName.trim(); + if (album.isNotEmpty && album.toLowerCase() != 'unknown album') { + return '${item.trackName} $artist $album'.trim(); + } + return '${item.trackName} $artist'.trim(); + } + + static Track _parseSearchTrack(Map data) { + final durationMs = _extractDurationMs(data); + final itemType = data['item_type']?.toString(); + + return Track( + id: (data['spotify_id'] ?? data['id'] ?? '').toString(), + name: (data['name'] ?? '').toString(), + artistName: (data['artists'] ?? data['artist'] ?? '').toString(), + albumName: (data['album_name'] ?? data['album'] ?? '').toString(), + albumArtist: data['album_artist']?.toString(), + artistId: (data['artist_id'] ?? data['artistId'])?.toString(), + albumId: data['album_id']?.toString(), + coverUrl: (data['cover_url'] ?? data['images'])?.toString(), + isrc: data['isrc']?.toString(), + duration: (durationMs / 1000).round(), + trackNumber: data['track_number'] as int?, + discNumber: data['disc_number'] as int?, + releaseDate: data['release_date']?.toString(), + totalTracks: data['total_tracks'] as int?, + source: data['source']?.toString() ?? data['provider_id']?.toString(), + albumType: data['album_type']?.toString(), + itemType: itemType, + ); + } + + static int _extractDurationMs(Map data) { + final durationMsRaw = data['duration_ms']; + if (durationMsRaw is num && durationMsRaw > 0) { + return durationMsRaw.toInt(); + } + if (durationMsRaw is String) { + final parsed = num.tryParse(durationMsRaw.trim()); + if (parsed != null && parsed > 0) { + return parsed.toInt(); + } + } + + final durationSecRaw = data['duration']; + if (durationSecRaw is num && durationSecRaw > 0) { + return (durationSecRaw * 1000).toInt(); + } + if (durationSecRaw is String) { + final parsed = num.tryParse(durationSecRaw.trim()); + if (parsed != null && parsed > 0) { + return (parsed * 1000).toInt(); + } + } + + return 0; + } + + static int _scoreMatch(LocalLibraryItem item, Map raw) { + final track = _parseSearchTrack(raw); + var score = 0; + + final localIsrc = _normalizedIsrc(item.isrc); + final candidateIsrc = _normalizedIsrc(track.isrc); + if (localIsrc != null && candidateIsrc != null) { + score += localIsrc == candidateIsrc ? 140 : -120; + } + + final localTitle = _normalizedTitle(item.trackName); + final candidateTitle = _normalizedTitle(track.name); + if (localTitle == candidateTitle) { + score += 45; + } else if (_tokenOverlap(localTitle, candidateTitle) >= 0.75) { + score += 24; + } else { + score -= 25; + } + + final localArtist = _normalizedArtistGroup(item.artistName); + final candidateArtist = _normalizedArtistGroup(track.artistName); + final artistOverlap = _tokenOverlap(localArtist, candidateArtist); + if (localArtist == candidateArtist) { + score += 30; + } else if (artistOverlap >= 0.6) { + score += 16; + } else { + score -= 20; + } + + final localAlbum = _normalizedText(item.albumName); + final candidateAlbum = _normalizedText(track.albumName); + if (localAlbum.isNotEmpty && candidateAlbum.isNotEmpty) { + if (localAlbum == candidateAlbum) { + score += 12; + } else if (_tokenOverlap(localAlbum, candidateAlbum) >= 0.7) { + score += 6; + } + } + + final localDuration = item.duration ?? 0; + final candidateDuration = track.duration; + if (localDuration > 0 && candidateDuration > 0) { + final diff = (localDuration - candidateDuration).abs(); + if (diff <= 2) { + score += 20; + } else if (diff <= 5) { + score += 12; + } else if (diff <= 10) { + score += 5; + } else if (diff > 20) { + score -= 30; + } + } + + if (item.trackNumber != null && + track.trackNumber != null && + item.trackNumber == track.trackNumber) { + score += 6; + } + if (item.discNumber != null && + track.discNumber != null && + item.discNumber == track.discNumber) { + score += 4; + } + + final localYear = _extractYear(item.releaseDate); + final candidateYear = _extractYear(track.releaseDate); + if (localYear != null && + candidateYear != null && + localYear == candidateYear) { + score += 4; + } + + score += _versionPenalty(item.trackName, track.name); + return score; + } + + static String? _normalizedIsrc(String? value) { + final normalized = value?.trim().toUpperCase(); + if (normalized == null || normalized.isEmpty) { + return null; + } + return normalized; + } + + static String _normalizedTitle(String value) { + final cleaned = _normalizedText(value) + .replaceAll(RegExp(r'\b(feat|ft|featuring)\b.*$'), ' ') + .replaceAll(RegExp(r'\b(remaster(?:ed)?|deluxe|bonus)\b'), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + return cleaned; + } + + static String _normalizedArtistGroup(String value) { + return _normalizedText( + value + .replaceAll(RegExp(r'\b(feat|ft|featuring|with|x)\b'), ',') + .replaceAll('&', ','), + ); + } + + static String _primaryArtist(String value) { + final parts = _normalizedArtistGroup( + value, + ).split(',').map((part) => part.trim()).where((part) => part.isNotEmpty); + return parts.isEmpty ? value.trim() : parts.first; + } + + static String _normalizedText(String value) { + return value + .toLowerCase() + .replaceAll(RegExp(r'[\(\)\[\]\{\}]'), ' ') + .replaceAll(RegExp(r'[^a-z0-9, ]+'), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + } + + static double _tokenOverlap(String left, String right) { + final leftTokens = left + .split(RegExp(r'[\s,]+')) + .where((token) => token.isNotEmpty) + .toSet(); + final rightTokens = right + .split(RegExp(r'[\s,]+')) + .where((token) => token.isNotEmpty) + .toSet(); + if (leftTokens.isEmpty || rightTokens.isEmpty) { + return 0; + } + final intersection = leftTokens.intersection(rightTokens).length; + final denominator = leftTokens.length > rightTokens.length + ? leftTokens.length + : rightTokens.length; + return intersection / denominator; + } + + static int _versionPenalty(String localTitle, String candidateTitle) { + const riskyMarkers = [ + 'live', + 'karaoke', + 'instrumental', + 'acoustic', + 'radio edit', + 'sped up', + 'slowed', + ]; + final local = _normalizedText(localTitle); + final candidate = _normalizedText(candidateTitle); + var penalty = 0; + for (final marker in riskyMarkers) { + final localHas = local.contains(marker); + final candidateHas = candidate.contains(marker); + if (!localHas && candidateHas) { + penalty -= 18; + } + } + return penalty; + } + + static int? _extractYear(String? date) { + if (date == null || date.length < 4) { + return null; + } + return int.tryParse(date.substring(0, 4)); + } +} From b7f34ec47ceb8f31cc386b8c3ef888c126d6e203 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:35:42 +0700 Subject: [PATCH 097/145] feat: selective auto-fill from online in Edit Metadata sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 'Auto-fill from online' expandable section to the metadata editor that lets users choose exactly which fields to populate from online metadata search. Users can select individual fields via filter chips, use 'All' or 'Empty only' quick-select buttons, then tap 'Fetch & Fill' to search metadata providers and fill only the selected controllers. The search uses existing searchTracksWithMetadataProviders API with ISRC-preferring best-match selection. Extended metadata (genre, label, copyright) is fetched via Deezer extended metadata API when available. Cover art is downloaded from the match's cover_url. All results are previewed in the editor before saving — nothing is written to the file until the user taps Save. Add 21 new l10n keys (editMetadata* namespace) for all UI strings. --- lib/l10n/app_localizations.dart | 126 +++++++ lib/l10n/app_localizations_de.dart | 74 ++++ lib/l10n/app_localizations_en.dart | 74 ++++ lib/l10n/app_localizations_es.dart | 74 ++++ lib/l10n/app_localizations_fr.dart | 74 ++++ lib/l10n/app_localizations_hi.dart | 74 ++++ lib/l10n/app_localizations_id.dart | 74 ++++ lib/l10n/app_localizations_ja.dart | 74 ++++ lib/l10n/app_localizations_ko.dart | 74 ++++ lib/l10n/app_localizations_nl.dart | 74 ++++ lib/l10n/app_localizations_pt.dart | 74 ++++ lib/l10n/app_localizations_ru.dart | 74 ++++ lib/l10n/app_localizations_tr.dart | 74 ++++ lib/l10n/app_localizations_zh.dart | 74 ++++ lib/l10n/arb/app_en.arb | 89 +++++ lib/screens/track_metadata_screen.dart | 479 +++++++++++++++++++++++++ 16 files changed, 1656 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ae9a4cc0..01af801f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4817,6 +4817,132 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'{count, plural, =1{1 playlist} other{{count} playlists}}'** String playlistsCount(int count); + + /// Section title for selective online metadata auto-fill in the edit metadata sheet + /// + /// In en, this message translates to: + /// **'Auto-fill from online'** + String get editMetadataAutoFill; + + /// Description for the auto-fill section + /// + /// In en, this message translates to: + /// **'Select fields to fill automatically from online metadata'** + String get editMetadataAutoFillDesc; + + /// Button label to fetch online metadata and fill selected fields + /// + /// In en, this message translates to: + /// **'Fetch & Fill'** + String get editMetadataAutoFillFetch; + + /// Snackbar shown while searching for online metadata + /// + /// In en, this message translates to: + /// **'Searching online...'** + String get editMetadataAutoFillSearching; + + /// Snackbar when online metadata search returns no results + /// + /// In en, this message translates to: + /// **'No matching metadata found online'** + String get editMetadataAutoFillNoResults; + + /// Snackbar confirming how many fields were auto-filled + /// + /// In en, this message translates to: + /// **'Filled {count} {count, plural, =1{field} other{fields}} from online metadata'** + String editMetadataAutoFillDone(int count); + + /// Snackbar when user taps Fetch without selecting any fields + /// + /// In en, this message translates to: + /// **'Select at least one field to auto-fill'** + String get editMetadataAutoFillNoneSelected; + + /// Chip label for title field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Title'** + String get editMetadataFieldTitle; + + /// Chip label for artist field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Artist'** + String get editMetadataFieldArtist; + + /// Chip label for album field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Album'** + String get editMetadataFieldAlbum; + + /// Chip label for album artist field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Album Artist'** + String get editMetadataFieldAlbumArtist; + + /// Chip label for date field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Date'** + String get editMetadataFieldDate; + + /// Chip label for track number field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Track #'** + String get editMetadataFieldTrackNum; + + /// Chip label for disc number field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Disc #'** + String get editMetadataFieldDiscNum; + + /// Chip label for genre field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Genre'** + String get editMetadataFieldGenre; + + /// Chip label for ISRC field in auto-fill selector + /// + /// In en, this message translates to: + /// **'ISRC'** + String get editMetadataFieldIsrc; + + /// Chip label for label field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Label'** + String get editMetadataFieldLabel; + + /// Chip label for copyright field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Copyright'** + String get editMetadataFieldCopyright; + + /// Chip label for cover art field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Cover Art'** + String get editMetadataFieldCover; + + /// Button to select all fields for auto-fill + /// + /// In en, this message translates to: + /// **'All'** + String get editMetadataSelectAll; + + /// Button to select only fields that are currently empty + /// + /// In en, this message translates to: + /// **'Empty only'** + String get editMetadataSelectEmpty; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ce489e3d..c3ee5083 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2826,4 +2826,78 @@ class AppLocalizationsDe extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index bd280e94..180810f3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2798,4 +2798,78 @@ class AppLocalizationsEn extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 81ce15ff..cea3915b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2798,6 +2798,80 @@ class AppLocalizationsEs extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } /// The translations for Spanish Castilian, as used in Spain (`es_ES`). diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index b35b22d0..8270c7c2 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2800,4 +2800,78 @@ class AppLocalizationsFr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index e36316e9..1767c438 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2798,4 +2798,78 @@ class AppLocalizationsHi extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 68bffbe5..67a9c2fc 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2805,4 +2805,78 @@ class AppLocalizationsId extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 8145c794..e406ba35 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2785,4 +2785,78 @@ class AppLocalizationsJa extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 66d21aec..b8085a70 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2778,4 +2778,78 @@ class AppLocalizationsKo extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e87906ac..c54e044a 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2798,4 +2798,78 @@ class AppLocalizationsNl extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 466572ce..ac0f577d 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2798,6 +2798,80 @@ class AppLocalizationsPt extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } /// The translations for Portuguese, as used in Portugal (`pt_PT`). diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index dd460698..76814d38 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2857,4 +2857,78 @@ class AppLocalizationsRu extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 6e1a37f1..f24d9f88 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2810,4 +2810,78 @@ class AppLocalizationsTr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 9248d997..d9440cff 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2798,6 +2798,80 @@ class AppLocalizationsZh extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 3de4ec31..6da406d0 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -3706,5 +3706,94 @@ "type": "int" } } + }, + "editMetadataAutoFill": "Auto-fill from online", + "@editMetadataAutoFill": { + "description": "Section title for selective online metadata auto-fill in the edit metadata sheet" + }, + "editMetadataAutoFillDesc": "Select fields to fill automatically from online metadata", + "@editMetadataAutoFillDesc": { + "description": "Description for the auto-fill section" + }, + "editMetadataAutoFillFetch": "Fetch & Fill", + "@editMetadataAutoFillFetch": { + "description": "Button label to fetch online metadata and fill selected fields" + }, + "editMetadataAutoFillSearching": "Searching online...", + "@editMetadataAutoFillSearching": { + "description": "Snackbar shown while searching for online metadata" + }, + "editMetadataAutoFillNoResults": "No matching metadata found online", + "@editMetadataAutoFillNoResults": { + "description": "Snackbar when online metadata search returns no results" + }, + "editMetadataAutoFillDone": "Filled {count} {count, plural, =1{field} other{fields}} from online metadata", + "@editMetadataAutoFillDone": { + "description": "Snackbar confirming how many fields were auto-filled", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "editMetadataAutoFillNoneSelected": "Select at least one field to auto-fill", + "@editMetadataAutoFillNoneSelected": { + "description": "Snackbar when user taps Fetch without selecting any fields" + }, + "editMetadataFieldTitle": "Title", + "@editMetadataFieldTitle": { + "description": "Chip label for title field in auto-fill selector" + }, + "editMetadataFieldArtist": "Artist", + "@editMetadataFieldArtist": { + "description": "Chip label for artist field in auto-fill selector" + }, + "editMetadataFieldAlbum": "Album", + "@editMetadataFieldAlbum": { + "description": "Chip label for album field in auto-fill selector" + }, + "editMetadataFieldAlbumArtist": "Album Artist", + "@editMetadataFieldAlbumArtist": { + "description": "Chip label for album artist field in auto-fill selector" + }, + "editMetadataFieldDate": "Date", + "@editMetadataFieldDate": { + "description": "Chip label for date field in auto-fill selector" + }, + "editMetadataFieldTrackNum": "Track #", + "@editMetadataFieldTrackNum": { + "description": "Chip label for track number field in auto-fill selector" + }, + "editMetadataFieldDiscNum": "Disc #", + "@editMetadataFieldDiscNum": { + "description": "Chip label for disc number field in auto-fill selector" + }, + "editMetadataFieldGenre": "Genre", + "@editMetadataFieldGenre": { + "description": "Chip label for genre field in auto-fill selector" + }, + "editMetadataFieldIsrc": "ISRC", + "@editMetadataFieldIsrc": { + "description": "Chip label for ISRC field in auto-fill selector" + }, + "editMetadataFieldLabel": "Label", + "@editMetadataFieldLabel": { + "description": "Chip label for label field in auto-fill selector" + }, + "editMetadataFieldCopyright": "Copyright", + "@editMetadataFieldCopyright": { + "description": "Chip label for copyright field in auto-fill selector" + }, + "editMetadataFieldCover": "Cover Art", + "@editMetadataFieldCover": { + "description": "Chip label for cover art field in auto-fill selector" + }, + "editMetadataSelectAll": "All", + "@editMetadataSelectAll": { + "description": "Button to select all fields for auto-fill" + }, + "editMetadataSelectEmpty": "Empty only", + "@editMetadataSelectEmpty": { + "description": "Button to select only fields that are currently empty" } } diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index cd37ee6a..989b6a47 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -3932,6 +3932,8 @@ class _EditMetadataSheet extends StatefulWidget { class _EditMetadataSheetState extends State<_EditMetadataSheet> { bool _saving = false; bool _showAdvanced = false; + bool _showAutoFill = false; + bool _fetching = false; String? _selectedCoverPath; String? _selectedCoverTempDir; String? _selectedCoverName; @@ -3939,6 +3941,25 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { String? _currentCoverTempDir; bool _loadingCurrentCover = false; + // Auto-fill field selection — which fields the user wants to fetch + final Set _autoFillFields = {}; + + // All auto-fillable fields and their mapping + static const _fieldDefs = { + 'title': 'title', + 'artist': 'artist', + 'album': 'album', + 'album_artist': 'album_artist', + 'date': 'date', + 'track_number': 'track_number', + 'disc_number': 'disc_number', + 'genre': 'genre', + 'isrc': 'isrc', + 'label': 'label', + 'copyright': 'copyright', + 'cover': 'cover', + }; + late final TextEditingController _titleCtrl; late final TextEditingController _artistCtrl; late final TextEditingController _albumCtrl; @@ -4132,6 +4153,286 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } } + String _fieldLabel(String key) { + final l10n = context.l10n; + switch (key) { + case 'title': + return l10n.editMetadataFieldTitle; + case 'artist': + return l10n.editMetadataFieldArtist; + case 'album': + return l10n.editMetadataFieldAlbum; + case 'album_artist': + return l10n.editMetadataFieldAlbumArtist; + case 'date': + return l10n.editMetadataFieldDate; + case 'track_number': + return l10n.editMetadataFieldTrackNum; + case 'disc_number': + return l10n.editMetadataFieldDiscNum; + case 'genre': + return l10n.editMetadataFieldGenre; + case 'isrc': + return l10n.editMetadataFieldIsrc; + case 'label': + return l10n.editMetadataFieldLabel; + case 'copyright': + return l10n.editMetadataFieldCopyright; + case 'cover': + return l10n.editMetadataFieldCover; + default: + return key; + } + } + + TextEditingController? _controllerForKey(String key) { + switch (key) { + case 'title': + return _titleCtrl; + case 'artist': + return _artistCtrl; + case 'album': + return _albumCtrl; + case 'album_artist': + return _albumArtistCtrl; + case 'date': + return _dateCtrl; + case 'track_number': + return _trackNumCtrl; + case 'disc_number': + return _discNumCtrl; + case 'genre': + return _genreCtrl; + case 'isrc': + return _isrcCtrl; + case 'label': + return _labelCtrl; + case 'copyright': + return _copyrightCtrl; + default: + return null; + } + } + + void _selectAllFields() { + setState(() { + _autoFillFields.addAll(_fieldDefs.keys); + }); + } + + void _selectEmptyFields() { + setState(() { + _autoFillFields.clear(); + for (final key in _fieldDefs.keys) { + if (key == 'cover') { + if (!_hasValue(_currentCoverPath) && !_hasValue(_selectedCoverPath)) { + _autoFillFields.add(key); + } + continue; + } + final ctrl = _controllerForKey(key); + if (ctrl != null && ctrl.text.trim().isEmpty) { + _autoFillFields.add(key); + } + } + }); + } + + Future _fetchAndFill() async { + if (_autoFillFields.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.editMetadataAutoFillNoneSelected)), + ); + return; + } + + setState(() => _fetching = true); + + try { + // Build search query from current field values + final title = _titleCtrl.text.trim(); + final artist = _artistCtrl.text.trim(); + final album = _albumCtrl.text.trim(); + final queryParts = []; + if (title.isNotEmpty) queryParts.add(title); + if (artist.isNotEmpty) queryParts.add(artist); + if (album.isNotEmpty) queryParts.add(album); + + if (queryParts.isEmpty) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.editMetadataAutoFillNoResults), + ), + ); + } + return; + } + + final query = queryParts.join(' '); + final results = await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 5, + ); + + if (!mounted) return; + + if (results.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.editMetadataAutoFillNoResults)), + ); + return; + } + + // Pick best match: prefer ISRC match, then first result + final currentIsrc = _isrcCtrl.text.trim().toUpperCase(); + Map? best; + if (currentIsrc.isNotEmpty) { + for (final r in results) { + final candidateIsrc = + (r['isrc']?.toString() ?? '').trim().toUpperCase(); + if (candidateIsrc == currentIsrc) { + best = r; + break; + } + } + } + best ??= results.first; + + // Extract metadata from best match + final enriched = { + 'title': (best['name'] ?? '').toString(), + 'artist': (best['artists'] ?? best['artist'] ?? '').toString(), + 'album': (best['album_name'] ?? best['album'] ?? '').toString(), + 'album_artist': (best['album_artist'] ?? '').toString(), + 'date': (best['release_date'] ?? '').toString(), + 'track_number': (best['track_number'] ?? '').toString(), + 'disc_number': (best['disc_number'] ?? '').toString(), + 'isrc': (best['isrc'] ?? '').toString(), + }; + + // Try to get extended metadata (genre, label, copyright) from Deezer + final trackId = + (best['spotify_id'] ?? best['id'] ?? '').toString(); + final source = (best['source'] ?? best['provider_id'] ?? '').toString(); + + if ((_autoFillFields.contains('genre') || + _autoFillFields.contains('label') || + _autoFillFields.contains('copyright')) && + trackId.isNotEmpty) { + try { + // If source is Deezer, fetch extended metadata directly + Map? extended; + if (source.toLowerCase().contains('deezer')) { + extended = await PlatformBridge.getDeezerExtendedMetadata(trackId); + } else { + // Try ISRC lookup via Deezer for genre/label/copyright + final isrcForLookup = enriched['isrc'] ?? ''; + if (isrcForLookup.isNotEmpty) { + try { + final deezerResult = await PlatformBridge.searchDeezerByISRC( + isrcForLookup, + ); + final deezerTrackId = + (deezerResult['id'] ?? deezerResult['track_id'] ?? '') + .toString(); + if (deezerTrackId.isNotEmpty) { + extended = await PlatformBridge.getDeezerExtendedMetadata( + deezerTrackId, + ); + } + } catch (_) {} + } + } + if (extended != null) { + enriched['genre'] = extended['genre'] ?? ''; + enriched['label'] = extended['label'] ?? ''; + enriched['copyright'] = extended['copyright'] ?? ''; + } + } catch (_) { + // Extended metadata is best-effort + } + } + + if (!mounted) return; + + // Apply selected fields to controllers + var filledCount = 0; + for (final key in _autoFillFields) { + if (key == 'cover') continue; // Handle cover separately below + final value = enriched[key]; + if (value != null && value.isNotEmpty && value != '0' && value != 'null') { + final ctrl = _controllerForKey(key); + if (ctrl != null) { + ctrl.text = value; + filledCount++; + } + } + } + + // Handle cover art download + if (_autoFillFields.contains('cover')) { + final coverUrl = + (best['cover_url'] ?? best['images'] ?? '').toString(); + if (coverUrl.isNotEmpty) { + try { + final tempDir = await Directory.systemTemp.createTemp( + 'autofill_cover_', + ); + final coverOutput = + '${tempDir.path}${Platform.pathSeparator}cover.jpg'; + final response = await HttpClient() + .getUrl(Uri.parse(coverUrl)) + .then((req) => req.close()); + final file = File(coverOutput); + final sink = file.openWrite(); + await response.pipe(sink); + if (await file.exists() && await file.length() > 0) { + await _cleanupSelectedCoverTemp(); + if (mounted) { + setState(() { + _selectedCoverPath = coverOutput; + _selectedCoverTempDir = tempDir.path; + _selectedCoverName = 'Online cover'; + }); + filledCount++; + } + } else { + try { + await tempDir.delete(recursive: true); + } catch (_) {} + } + } catch (_) { + // Cover download is best-effort + } + } + } + + if (mounted) { + setState(() {}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + filledCount > 0 + ? context.l10n.editMetadataAutoFillDone(filledCount) + : context.l10n.editMetadataAutoFillNoResults, + ), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.snackbarError(e.toString())), + ), + ); + } + } finally { + if (mounted) setState(() => _fetching = false); + } + } + @override void initState() { super.initState(); @@ -4416,6 +4717,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { children: [ const SizedBox(height: 6), _buildCoverEditor(cs), + _buildAutoFillSection(cs), _field('Title', _titleCtrl), _field('Artist', _artistCtrl), _field('Album', _albumCtrl), @@ -4487,6 +4789,183 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ); } + Widget _buildAutoFillSection(ColorScheme cs) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + decoration: BoxDecoration( + color: cs.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.5)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () => setState(() => _showAutoFill = !_showAutoFill), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + child: Row( + children: [ + Icon( + Icons.travel_explore, + size: 20, + color: cs.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + context.l10n.editMetadataAutoFill, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: cs.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ), + Icon( + _showAutoFill ? Icons.expand_less : Icons.expand_more, + size: 20, + color: cs.onSurfaceVariant, + ), + ], + ), + ), + ), + if (_showAutoFill) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + context.l10n.editMetadataAutoFillDesc, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: cs.onSurfaceVariant, + ), + ), + ), + const SizedBox(height: 8), + // Quick select buttons + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + _quickSelectButton( + label: context.l10n.editMetadataSelectAll, + onTap: _selectAllFields, + cs: cs, + ), + const SizedBox(width: 8), + _quickSelectButton( + label: context.l10n.editMetadataSelectEmpty, + onTap: _selectEmptyFields, + cs: cs, + ), + ], + ), + ), + const SizedBox(height: 8), + // Field chips + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Wrap( + spacing: 6, + runSpacing: 4, + children: _fieldDefs.keys.map((key) { + final selected = _autoFillFields.contains(key); + return FilterChip( + label: Text(_fieldLabel(key)), + selected: selected, + onSelected: _fetching + ? null + : (val) { + setState(() { + if (val) { + _autoFillFields.add(key); + } else { + _autoFillFields.remove(key); + } + }); + }, + selectedColor: cs.primaryContainer, + checkmarkColor: cs.onPrimaryContainer, + labelStyle: Theme.of(context).textTheme.labelSmall + ?.copyWith( + color: selected + ? cs.onPrimaryContainer + : cs.onSurfaceVariant, + ), + visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + }).toList(), + ), + ), + const SizedBox(height: 10), + // Fetch button + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + bottom: 12, + ), + child: SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: + (_fetching || _saving || _autoFillFields.isEmpty) + ? null + : _fetchAndFill, + icon: _fetching + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.auto_fix_high), + label: Text( + _fetching + ? context.l10n.editMetadataAutoFillSearching + : context.l10n.editMetadataAutoFillFetch, + ), + ), + ), + ), + ], + ], + ), + ), + ); + } + + Widget _quickSelectButton({ + required String label, + required VoidCallback onTap, + required ColorScheme cs, + }) { + return InkWell( + onTap: _fetching ? null : onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: cs.outline.withValues(alpha: 0.5)), + ), + child: Text( + label, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: cs.primary, + ), + ), + ), + ); + } + Widget _buildCoverEditor(ColorScheme cs) { final hasSelectedCover = _hasValue(_selectedCoverPath); final hasCurrentCover = _hasValue(_currentCoverPath); From 89a38af538a0499b6e1adda9f0eceaf1087d3abe Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:42:22 +0700 Subject: [PATCH 098/145] fix: resolve all flutter analyze warnings and improve auto-fill enrichment chain - Fix use_build_context_synchronously in _embedLyrics by capturing l10n strings before async gaps (snackbarFailedToWriteStorage, snackbarFailedToEmbedLyrics, snackbarUnsupportedAudioFormat) - Improve auto-fill metadata enrichment to use proper API chain: search providers -> convertSpotifyToDeezer (SongLink) for Deezer ID -> getDeezerMetadata for ISRC -> getDeezerExtendedMetadata for genre/label/copyright. Falls back to ISRC-based Deezer lookup when SongLink conversion unavailable. - flutter analyze now reports 0 issues --- lib/screens/track_metadata_screen.dart | 135 +++++++++++++++++++------ 1 file changed, 103 insertions(+), 32 deletions(-) diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 989b6a47..5dde4282 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -1757,6 +1757,11 @@ class _TrackMetadataScreenState extends ConsumerState { setState(() => _isEmbedding = true); + // Capture l10n strings before async gaps to avoid use_build_context_synchronously + final l10nFailedToWriteStorage = context.l10n.snackbarFailedToWriteStorage; + final l10nFailedToEmbedLyrics = context.l10n.snackbarFailedToEmbedLyrics; + final l10nUnsupportedFormat = context.l10n.snackbarUnsupportedAudioFormat; + String? safTempPath; String? coverPath; @@ -1793,13 +1798,13 @@ class _TrackMetadataScreenState extends ConsumerState { ); success = ok; if (!ok) { - error = context.l10n.snackbarFailedToWriteStorage; + error = l10nFailedToWriteStorage; } } else { success = true; } } else { - error = result['error']?.toString() ?? context.l10n.snackbarFailedToEmbedLyrics; + error = result['error']?.toString() ?? l10nFailedToEmbedLyrics; } } else if (isMp3 || isOpus) { final metadata = _buildFallbackMetadata(); @@ -1845,7 +1850,7 @@ class _TrackMetadataScreenState extends ConsumerState { } if (ffmpegResult == null) { - error = context.l10n.snackbarFailedToEmbedLyrics; + error = l10nFailedToEmbedLyrics; } else if (_isSafFile) { final ok = await PlatformBridge.writeTempToSaf( ffmpegResult, @@ -1853,13 +1858,13 @@ class _TrackMetadataScreenState extends ConsumerState { ); success = ok; if (!ok) { - error = context.l10n.snackbarFailedToWriteStorage; + error = l10nFailedToWriteStorage; } } else { success = true; } } else { - error = context.l10n.snackbarUnsupportedAudioFormat; + error = l10nUnsupportedFormat; } if (mounted) { @@ -4299,7 +4304,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } best ??= results.first; - // Extract metadata from best match + // Extract basic metadata from search result final enriched = { 'title': (best['name'] ?? '').toString(), 'artist': (best['artists'] ?? best['artist'] ?? '').toString(), @@ -4311,39 +4316,82 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { 'isrc': (best['isrc'] ?? '').toString(), }; - // Try to get extended metadata (genre, label, copyright) from Deezer + final needsIsrc = _autoFillFields.contains('isrc') && + enriched['isrc']!.isEmpty; + final needsExtended = _autoFillFields.contains('genre') || + _autoFillFields.contains('label') || + _autoFillFields.contains('copyright'); + final trackId = (best['spotify_id'] ?? best['id'] ?? '').toString(); - final source = (best['source'] ?? best['provider_id'] ?? '').toString(); + final source = + (best['source'] ?? best['provider_id'] ?? '').toString(); + final isDeezerSource = source.toLowerCase().contains('deezer'); - if ((_autoFillFields.contains('genre') || - _autoFillFields.contains('label') || - _autoFillFields.contains('copyright')) && - trackId.isNotEmpty) { + // Resolve Deezer track ID for extended metadata + ISRC + String? deezerId; + + if ((needsIsrc || needsExtended) && trackId.isNotEmpty) { try { - // If source is Deezer, fetch extended metadata directly - Map? extended; - if (source.toLowerCase().contains('deezer')) { - extended = await PlatformBridge.getDeezerExtendedMetadata(trackId); + if (isDeezerSource) { + // Source is Deezer — trackId is already a Deezer ID + deezerId = trackId; } else { - // Try ISRC lookup via Deezer for genre/label/copyright - final isrcForLookup = enriched['isrc'] ?? ''; - if (isrcForLookup.isNotEmpty) { - try { - final deezerResult = await PlatformBridge.searchDeezerByISRC( - isrcForLookup, - ); - final deezerTrackId = - (deezerResult['id'] ?? deezerResult['track_id'] ?? '') - .toString(); - if (deezerTrackId.isNotEmpty) { - extended = await PlatformBridge.getDeezerExtendedMetadata( - deezerTrackId, - ); - } - } catch (_) {} + // Source is Spotify/extension — convert to Deezer via SongLink + final deezerData = await PlatformBridge.convertSpotifyToDeezer( + 'track', + trackId, + ); + final trackData = deezerData['track']; + if (trackData is Map) { + final rawId = trackData['spotify_id'] as String?; + if (rawId != null && rawId.startsWith('deezer:')) { + deezerId = rawId.split(':')[1]; + } + // Also grab ISRC and release_date from the conversion response + final convIsrc = (trackData['isrc'] ?? '').toString().trim(); + if (convIsrc.isNotEmpty && enriched['isrc']!.isEmpty) { + enriched['isrc'] = convIsrc; + } + final convDate = + (trackData['release_date'] ?? '').toString().trim(); + if (convDate.isNotEmpty && enriched['date']!.isEmpty) { + enriched['date'] = convDate; + } } + // Fallback: legacy ID format + deezerId ??= (deezerData['id'] ?? '').toString(); + if (deezerId.isEmpty) deezerId = null; } + } catch (_) { + // SongLink conversion is best-effort + } + } + + if (!mounted) return; + + // Fetch ISRC from Deezer track metadata if still missing + if (needsIsrc && enriched['isrc']!.isEmpty && deezerId != null) { + try { + final deezerMeta = await PlatformBridge.getDeezerMetadata( + 'track', + deezerId, + ); + final deezerIsrc = (deezerMeta['isrc'] ?? '').toString().trim(); + if (deezerIsrc.isNotEmpty) { + enriched['isrc'] = deezerIsrc; + } + } catch (_) {} + } + + if (!mounted) return; + + // Fetch genre/label/copyright from Deezer extended metadata + if (needsExtended && deezerId != null) { + try { + final extended = await PlatformBridge.getDeezerExtendedMetadata( + deezerId, + ); if (extended != null) { enriched['genre'] = extended['genre'] ?? ''; enriched['label'] = extended['label'] ?? ''; @@ -4354,6 +4402,29 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } } + // Fallback: if still no Deezer ID but we have ISRC, try ISRC lookup + if (needsExtended && + deezerId == null && + enriched['isrc']!.isNotEmpty) { + try { + final deezerResult = await PlatformBridge.searchDeezerByISRC( + enriched['isrc']!, + ); + final fallbackId = + (deezerResult['id'] ?? deezerResult['track_id'] ?? '') + .toString(); + if (fallbackId.isNotEmpty) { + final extended = + await PlatformBridge.getDeezerExtendedMetadata(fallbackId); + if (extended != null) { + enriched['genre'] = extended['genre'] ?? ''; + enriched['label'] = extended['label'] ?? ''; + enriched['copyright'] = extended['copyright'] ?? ''; + } + } + } catch (_) {} + } + if (!mounted) return; // Apply selected fields to controllers From e8327545ad5423f85ea6a950c34af09f0ed37adc Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 21:11:36 +0700 Subject: [PATCH 099/145] feat: improve auto-fill track resolution in Edit Metadata sheet - Identifier-first resolution (ISRC/Deezer/Spotify) before falling back to text search - Score-based match selection via _metadataMatchScore instead of provider order - Pass sourceTrackId from TrackMetadataScreen into _EditMetadataSheet - Refactor buildDeezerExtendedMetadataResult and buildDeezerISRCSearchResult as testable helpers - Add unit tests for buildDeezerExtendedMetadataResult and buildDeezerISRCSearchResult - Propagate copyright through Deezer enrichment chain (exports, extension_providers) --- go_backend/deezer.go | 13 +- go_backend/exports.go | 75 ++- go_backend/exports_deezer_metadata_test.go | 59 ++ go_backend/extension_providers.go | 17 +- lib/screens/track_metadata_screen.dart | 592 +++++++++++++++------ 5 files changed, 566 insertions(+), 190 deletions(-) create mode 100644 go_backend/exports_deezer_metadata_test.go diff --git a/go_backend/deezer.go b/go_backend/deezer.go index 568e61c4..a389f3c8 100644 --- a/go_backend/deezer.go +++ b/go_backend/deezer.go @@ -256,6 +256,7 @@ type deezerAlbumFull struct { NbTracks int `json:"nb_tracks"` RecordType string `json:"record_type"` Label string `json:"label"` + Copyright string `json:"copyright"` Genres struct { Data []deezerGenre `json:"data"` } `json:"genres"` @@ -1084,8 +1085,9 @@ func (c *DeezerClient) getBestAlbumImage(album deezerAlbumFull) string { } type AlbumExtendedMetadata struct { - Genre string - Label string + Genre string + Label string + Copyright string } func (c *DeezerClient) GetAlbumExtendedMetadata(ctx context.Context, albumID string) (*AlbumExtendedMetadata, error) { @@ -1116,8 +1118,9 @@ func (c *DeezerClient) GetAlbumExtendedMetadata(ctx context.Context, albumID str } result := &AlbumExtendedMetadata{ - Genre: strings.Join(genres, ", "), - Label: album.Label, + Genre: strings.Join(genres, ", "), + Label: album.Label, + Copyright: album.Copyright, } c.cacheMu.Lock() @@ -1129,7 +1132,7 @@ func (c *DeezerClient) GetAlbumExtendedMetadata(ctx context.Context, albumID str c.maybeCleanupCachesLocked(now) c.cacheMu.Unlock() - GoLog("[Deezer] Album metadata fetched - Genre: %s, Label: %s\n", result.Genre, result.Label) + GoLog("[Deezer] Album metadata fetched - Genre: %s, Label: %s, Copyright: %s\n", result.Genre, result.Label, result.Copyright) return result, nil } diff --git a/go_backend/exports.go b/go_backend/exports.go index b8178d77..1c5f84ed 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -283,7 +283,7 @@ func enrichRequestExtendedMetadata(req *DownloadRequest) { return } - if req.ISRC == "" || (req.Genre != "" && req.Label != "") { + if req.ISRC == "" || (req.Genre != "" && req.Label != "" && req.Copyright != "") { return } @@ -305,8 +305,11 @@ func enrichRequestExtendedMetadata(req *DownloadRequest) { if req.Label == "" && extMeta.Label != "" { req.Label = extMeta.Label } - if req.Genre != "" || req.Label != "" { - GoLog("[DownloadWithFallback] Extended metadata ready: genre=%s, label=%s\n", req.Genre, req.Label) + if req.Copyright == "" && extMeta.Copyright != "" { + req.Copyright = extMeta.Copyright + } + if req.Genre != "" || req.Label != "" || req.Copyright != "" { + GoLog("[DownloadWithFallback] Extended metadata ready: genre=%s, label=%s, copyright=%s\n", req.Genre, req.Label, req.Copyright) } } @@ -1335,10 +1338,7 @@ func GetDeezerExtendedMetadata(trackID string) (string, error) { return "", err } - result := map[string]string{ - "genre": metadata.Genre, - "label": metadata.Label, - } + result := buildDeezerExtendedMetadataResult(metadata) jsonBytes, err := json.Marshal(result) if err != nil { @@ -1358,7 +1358,8 @@ func SearchDeezerByISRC(isrc string) (string, error) { return "", err } - jsonBytes, err := json.Marshal(track) + result := buildDeezerISRCSearchResult(track) + jsonBytes, err := json.Marshal(result) if err != nil { return "", err } @@ -1366,6 +1367,55 @@ func SearchDeezerByISRC(isrc string) (string, error) { return string(jsonBytes), nil } +func buildDeezerExtendedMetadataResult(metadata *AlbumExtendedMetadata) map[string]string { + if metadata == nil { + return map[string]string{ + "genre": "", + "label": "", + "copyright": "", + } + } + + return map[string]string{ + "genre": metadata.Genre, + "label": metadata.Label, + "copyright": metadata.Copyright, + } +} + +func buildDeezerISRCSearchResult(track *TrackMetadata) map[string]interface{} { + if track == nil { + return map[string]interface{}{} + } + + result := map[string]interface{}{ + "spotify_id": track.SpotifyID, + "artists": track.Artists, + "name": track.Name, + "album_name": track.AlbumName, + "album_artist": track.AlbumArtist, + "duration_ms": track.DurationMS, + "images": track.Images, + "release_date": track.ReleaseDate, + "track_number": track.TrackNumber, + "total_tracks": track.TotalTracks, + "disc_number": track.DiscNumber, + "external_urls": track.ExternalURL, + "isrc": track.ISRC, + "album_id": track.AlbumID, + "artist_id": track.ArtistID, + "album_type": track.AlbumType, + } + + if deezerID := strings.TrimSpace(strings.TrimPrefix(track.SpotifyID, "deezer:")); deezerID != "" { + result["id"] = deezerID + result["track_id"] = deezerID + result["success"] = true + } + + return result +} + func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -1824,8 +1874,8 @@ func ReEnrichFile(requestJSON string) (string, error) { GoLog("[ReEnrich] Metadata provider search failed: %v\n", searchErr) } - // Try to get extended metadata (genre, label) from Deezer if not already set - if found && req.ISRC != "" && (req.Genre == "" || req.Label == "") { + // Try to get extended metadata from Deezer if not already set + if found && req.ISRC != "" && (req.Genre == "" || req.Label == "" || req.Copyright == "") { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) extMeta, err := deezerClient.GetExtendedMetadataByISRC(ctx, req.ISRC) cancel() @@ -1836,7 +1886,10 @@ func ReEnrichFile(requestJSON string) (string, error) { if req.Label == "" && extMeta.Label != "" { req.Label = extMeta.Label } - GoLog("[ReEnrich] Extended metadata: genre=%s, label=%s\n", req.Genre, req.Label) + if req.Copyright == "" && extMeta.Copyright != "" { + req.Copyright = extMeta.Copyright + } + GoLog("[ReEnrich] Extended metadata: genre=%s, label=%s, copyright=%s\n", req.Genre, req.Label, req.Copyright) } } diff --git a/go_backend/exports_deezer_metadata_test.go b/go_backend/exports_deezer_metadata_test.go new file mode 100644 index 00000000..763965fc --- /dev/null +++ b/go_backend/exports_deezer_metadata_test.go @@ -0,0 +1,59 @@ +package gobackend + +import "testing" + +func TestBuildDeezerExtendedMetadataResultHandlesNil(t *testing.T) { + result := buildDeezerExtendedMetadataResult(nil) + + if result["genre"] != "" { + t.Fatalf("expected empty genre, got %q", result["genre"]) + } + if result["label"] != "" { + t.Fatalf("expected empty label, got %q", result["label"]) + } + if result["copyright"] != "" { + t.Fatalf("expected empty copyright, got %q", result["copyright"]) + } +} + +func TestBuildDeezerExtendedMetadataResultIncludesCopyright(t *testing.T) { + result := buildDeezerExtendedMetadataResult(&AlbumExtendedMetadata{ + Genre: "Rock", + Label: "EMI", + Copyright: "(C) Queen", + }) + + if result["genre"] != "Rock" { + t.Fatalf("unexpected genre: %q", result["genre"]) + } + if result["label"] != "EMI" { + t.Fatalf("unexpected label: %q", result["label"]) + } + if result["copyright"] != "(C) Queen" { + t.Fatalf("unexpected copyright: %q", result["copyright"]) + } +} + +func TestBuildDeezerISRCSearchResultAddsCompatibilityIDs(t *testing.T) { + result := buildDeezerISRCSearchResult(&TrackMetadata{ + SpotifyID: "deezer:3135556", + Name: "Love Of My Life", + Artists: "Queen", + AlbumName: "A Night at the Opera", + ISRC: "GBUM71029604", + ReleaseDate: "1975-11-21", + }) + + if result["spotify_id"] != "deezer:3135556" { + t.Fatalf("unexpected spotify_id: %v", result["spotify_id"]) + } + if result["id"] != "3135556" { + t.Fatalf("unexpected id: %v", result["id"]) + } + if result["track_id"] != "3135556" { + t.Fatalf("unexpected track_id: %v", result["track_id"]) + } + if result["success"] != true { + t.Fatalf("expected success=true, got %v", result["success"]) + } +} diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 1866543f..b9f79bc4 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -1065,8 +1065,9 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro GoLog("[DownloadWithExtensionFallback] Metadata provider search failed (non-fatal): %v\n", searchErr) } - // Try Deezer extended metadata for genre/label if we have ISRC - if req.ISRC != "" && (req.Genre == "" || req.Label == "") { + // Try Deezer extended metadata if we have ISRC + if req.ISRC != "" && + (req.Genre == "" || req.Label == "" || req.Copyright == "") { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) extMeta, err := GetDeezerClient().GetExtendedMetadataByISRC(ctx, req.ISRC) cancel() @@ -1077,7 +1078,10 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro if req.Label == "" && extMeta.Label != "" { req.Label = extMeta.Label } - GoLog("[DownloadWithExtensionFallback] Extended metadata from Deezer: genre=%s, label=%s\n", req.Genre, req.Label) + if req.Copyright == "" && extMeta.Copyright != "" { + req.Copyright = extMeta.Copyright + } + GoLog("[DownloadWithExtensionFallback] Extended metadata from Deezer: genre=%s, label=%s, copyright=%s\n", req.Genre, req.Label, req.Copyright) } } } @@ -1249,7 +1253,8 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro GoLog("[DownloadWithExtensionFallback] Trying provider: %s\n", providerID) if isBuiltInProvider(providerIDNormalized) { - if (req.Genre == "" || req.Label == "") && req.ISRC != "" { + if (req.Genre == "" || req.Label == "" || req.Copyright == "") && + req.ISRC != "" { GoLog("[DownloadWithExtensionFallback] Enriching extended metadata from Deezer for ISRC: %s\n", req.ISRC) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) deezerClient := GetDeezerClient() @@ -1264,6 +1269,10 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro req.Label = extMeta.Label GoLog("[DownloadWithExtensionFallback] Label from Deezer: %s\n", req.Label) } + if req.Copyright == "" && extMeta.Copyright != "" { + req.Copyright = extMeta.Copyright + GoLog("[DownloadWithExtensionFallback] Copyright from Deezer: %s\n", req.Copyright) + } } else if err != nil { GoLog("[DownloadWithExtensionFallback] Failed to get extended metadata from Deezer: %v\n", err) } diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 5dde4282..85e803b6 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -307,11 +307,13 @@ class _TrackMetadataScreenState extends ConsumerState { ); // Fill in album name from file tags if stored value is empty - final needsAlbum = resolvedAlbum != null && + final needsAlbum = + resolvedAlbum != null && resolvedAlbum.isNotEmpty && (albumName.isEmpty); // Fill in duration from file if stored value is missing/zero - final needsDuration = resolvedDuration != null && + final needsDuration = + resolvedDuration != null && resolvedDuration > 0 && (duration == null || duration == 0); @@ -585,9 +587,9 @@ class _TrackMetadataScreenState extends ConsumerState { } void _showCueVirtualTrackSnackBar(BuildContext context) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(_cueVirtualTrackGuidance(context))), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(_cueVirtualTrackGuidance(context)))); } void _hideCurrentSnackBar() { @@ -606,17 +608,14 @@ class _TrackMetadataScreenState extends ConsumerState { } void _showSnackBarMessage(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); } void _showLongSnackBarMessage(String message) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - duration: const Duration(seconds: 60), - ), + SnackBar(content: Text(message), duration: const Duration(seconds: 60)), ); } @@ -1144,9 +1143,7 @@ class _TrackMetadataScreenState extends ConsumerState { _copyToClipboard(context, webUrl); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.snackbarUrlCopied(serviceName), - ), + content: Text(context.l10n.snackbarUrlCopied(serviceName)), ), ); } @@ -1879,16 +1876,18 @@ class _TrackMetadataScreenState extends ConsumerState { } else { setState(() => _isEmbedding = false); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(error ?? context.l10n.snackbarFailedToEmbedLyrics)), + SnackBar( + content: Text(error ?? context.l10n.snackbarFailedToEmbedLyrics), + ), ); } } } catch (e) { if (mounted) { setState(() => _isEmbedding = false); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), + ); } } finally { if (coverPath != null) { @@ -2568,8 +2567,11 @@ class _TrackMetadataScreenState extends ConsumerState { onTap: () { _closeOptionsMenuAndRun( sheetContext, - () => - _showEditMetadataSheet(screenContext, ref, colorScheme), + () => _showEditMetadataSheet( + screenContext, + ref, + colorScheme, + ), ); }, ), @@ -3008,31 +3010,29 @@ class _TrackMetadataScreenState extends ConsumerState { const SizedBox(height: 16), Text( sheetContext.l10n.cueSplitTitle, - style: Theme.of(sheetContext).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of(sheetContext).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), Text( sheetContext.l10n.cueSplitAlbum(album), - style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(sheetContext).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: 4), Text( sheetContext.l10n.cueSplitArtist(artist), - style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(sheetContext).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: 4), Text( sheetContext.l10n.cueSplitTrackCount(tracks.length), - style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( - color: colorScheme.primary, - fontWeight: FontWeight.w600, - ), + style: Theme.of(sheetContext).textTheme.bodyMedium + ?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), ), const SizedBox(height: 16), // Track list preview (scrollable, max 200px) @@ -3259,8 +3259,12 @@ class _TrackMetadataScreenState extends ConsumerState { // Determine output directory final String outputDir; - final treeUri = !_isLocalItem ? (_downloadItem?.downloadTreeUri ?? '') : ''; - final relativeDir = !_isLocalItem ? (_downloadItem?.safRelativeDir ?? '') : ''; + final treeUri = !_isLocalItem + ? (_downloadItem?.downloadTreeUri ?? '') + : ''; + final relativeDir = !_isLocalItem + ? (_downloadItem?.safRelativeDir ?? '') + : ''; final writeBackToSaf = isSafSource && treeUri.isNotEmpty; if (writeBackToSaf) { final tempDir = await getTemporaryDirectory(); @@ -3326,9 +3330,7 @@ class _TrackMetadataScreenState extends ConsumerState { // Read existing metadata first final metadata = await PlatformBridge.readFileMetadata(path); if (metadata['error'] == null) { - final fields = { - 'cover_path': coverPath, - }; + final fields = {'cover_path': coverPath}; // Preserve existing fields for (final entry in metadata.entries) { if (entry.key == 'error' || entry.value == null) continue; @@ -3706,6 +3708,7 @@ class _TrackMetadataScreenState extends ConsumerState { colorScheme: colorScheme, initialValues: initialValues, filePath: cleanFilePath, + sourceTrackId: _spotifyId, ), ); @@ -3789,10 +3792,7 @@ class _TrackMetadataScreenState extends ConsumerState { ); } - void _closeOptionsMenuAndRun( - BuildContext sheetContext, - VoidCallback action, - ) { + void _closeOptionsMenuAndRun(BuildContext sheetContext, VoidCallback action) { Navigator.pop(sheetContext); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; @@ -3919,15 +3919,24 @@ class _TrackMetadataScreenState extends ConsumerState { } } +class _ResolvedAutoFillTrack { + final Map track; + final String? deezerId; + + const _ResolvedAutoFillTrack({required this.track, this.deezerId}); +} + class _EditMetadataSheet extends StatefulWidget { final ColorScheme colorScheme; final Map initialValues; final String filePath; + final String? sourceTrackId; const _EditMetadataSheet({ required this.colorScheme, required this.initialValues, required this.filePath, + this.sourceTrackId, }); @override @@ -3935,6 +3944,12 @@ class _EditMetadataSheet extends StatefulWidget { } class _EditMetadataSheetState extends State<_EditMetadataSheet> { + static final RegExp _metadataCollapsePattern = RegExp(r'[^a-z0-9]+'); + static final RegExp _metadataWhitespacePattern = RegExp(r'\s+'); + static final RegExp _spotifyTrackIdPattern = RegExp(r'^[A-Za-z0-9]{22}$'); + static final RegExp _deezerTrackIdPattern = RegExp(r'^\d+$'); + static final RegExp _isrcPattern = RegExp(r'^[A-Z]{2}[A-Z0-9]{3}\d{7}$'); + bool _saving = false; bool _showAdvanced = false; bool _showAutoFill = false; @@ -4152,9 +4167,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { }); } catch (e) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), + ); } } @@ -4243,6 +4258,251 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { }); } + String _normalizeMetadataText(String value) { + final collapsed = value + .toLowerCase() + .replaceAll(_metadataCollapsePattern, ' ') + .trim(); + return collapsed.replaceAll(_metadataWhitespacePattern, ' '); + } + + bool _looksLikeIsrc(String value) { + return _isrcPattern.hasMatch(value.trim().toUpperCase()); + } + + String? _extractRawSpotifyTrackIdFromValue(Object? value) { + final raw = value?.toString().trim() ?? ''; + if (raw.isEmpty) return null; + + if (_spotifyTrackIdPattern.hasMatch(raw)) { + return raw; + } + + if (raw.startsWith('spotify:')) { + final parts = raw.split(':'); + final last = parts.isNotEmpty ? parts.last.trim() : ''; + if (_spotifyTrackIdPattern.hasMatch(last)) { + return last; + } + return null; + } + + final uri = Uri.tryParse(raw); + if (uri != null && + uri.host.contains('spotify.com') && + uri.pathSegments.length >= 2 && + uri.pathSegments.first == 'track') { + final candidate = uri.pathSegments[1].trim(); + if (_spotifyTrackIdPattern.hasMatch(candidate)) { + return candidate; + } + } + + return null; + } + + String? _extractRawDeezerTrackIdFromValue(Object? value) { + final raw = value?.toString().trim() ?? ''; + if (raw.isEmpty) return null; + + if (_deezerTrackIdPattern.hasMatch(raw)) { + return raw; + } + + if (raw.startsWith('deezer:')) { + final parts = raw.split(':'); + final last = parts.isNotEmpty ? parts.last.trim() : ''; + if (_deezerTrackIdPattern.hasMatch(last)) { + return last; + } + } + + final uri = Uri.tryParse(raw); + if (uri != null && uri.host.contains('deezer.com')) { + final trackIndex = uri.pathSegments.indexOf('track'); + if (trackIndex >= 0 && trackIndex + 1 < uri.pathSegments.length) { + final candidate = uri.pathSegments[trackIndex + 1].trim(); + if (_deezerTrackIdPattern.hasMatch(candidate)) { + return candidate; + } + } + } + + return null; + } + + String? _extractRawSpotifyTrackId(Map track) { + for (final candidate in [track['spotify_id'], track['id']]) { + final spotifyId = _extractRawSpotifyTrackIdFromValue(candidate); + if (spotifyId != null) return spotifyId; + } + + final externalLinks = track['external_links']; + if (externalLinks is Map) { + final spotifyId = _extractRawSpotifyTrackIdFromValue( + externalLinks['spotify'], + ); + if (spotifyId != null) return spotifyId; + } + + return null; + } + + String? _extractRawDeezerTrackId(Map track) { + for (final candidate in [ + track['deezer_id'], + track['spotify_id'], + track['id'], + ]) { + final deezerId = _extractRawDeezerTrackIdFromValue(candidate); + if (deezerId != null) return deezerId; + } + + final externalLinks = track['external_links']; + if (externalLinks is Map) { + final deezerId = _extractRawDeezerTrackIdFromValue( + externalLinks['deezer'], + ); + if (deezerId != null) return deezerId; + } + + return null; + } + + Map _unwrapTrackPayload(Map payload) { + final track = payload['track']; + if (track is Map) { + return track; + } + return payload; + } + + void _mergeOnlineTrackData( + Map enriched, + Map track, + ) { + void put(String key, Object? value) { + final text = value?.toString().trim() ?? ''; + if (text.isNotEmpty && text != 'null') { + enriched[key] = text; + } + } + + put('title', track['name'] ?? track['title']); + put('artist', track['artists'] ?? track['artist']); + put('album', track['album_name'] ?? track['album']); + put('album_artist', track['album_artist']); + put('date', track['release_date']); + put('track_number', track['track_number']); + put('disc_number', track['disc_number']); + put('isrc', track['isrc']); + put('genre', track['genre']); + put('label', track['label']); + put('copyright', track['copyright']); + } + + Future<_ResolvedAutoFillTrack?> _resolveAutoFillTrackFromIdentifiers( + String currentIsrc, + ) async { + if (_looksLikeIsrc(currentIsrc)) { + final deezerTrack = await PlatformBridge.searchDeezerByISRC(currentIsrc); + return _ResolvedAutoFillTrack( + track: _unwrapTrackPayload(deezerTrack), + deezerId: _extractRawDeezerTrackId(deezerTrack), + ); + } + + final sourceTrackId = widget.sourceTrackId?.trim() ?? ''; + if (sourceTrackId.isEmpty) { + return null; + } + + final deezerId = _extractRawDeezerTrackIdFromValue(sourceTrackId); + if (deezerId != null) { + final deezerTrack = await PlatformBridge.getDeezerMetadata( + 'track', + deezerId, + ); + return _ResolvedAutoFillTrack( + track: _unwrapTrackPayload(deezerTrack), + deezerId: deezerId, + ); + } + + final spotifyId = _extractRawSpotifyTrackIdFromValue(sourceTrackId); + if (spotifyId != null) { + final deezerTrack = await PlatformBridge.convertSpotifyToDeezer( + 'track', + spotifyId, + ); + final track = _unwrapTrackPayload(deezerTrack); + return _ResolvedAutoFillTrack( + track: track, + deezerId: + _extractRawDeezerTrackId(track) ?? + _extractRawDeezerTrackId(deezerTrack), + ); + } + + return null; + } + + int _metadataMatchScore( + Map track, { + required String currentTitle, + required String currentArtist, + required String currentAlbum, + required String currentIsrc, + }) { + var score = 0; + + final candidateIsrc = (track['isrc']?.toString() ?? '') + .trim() + .toUpperCase(); + if (currentIsrc.isNotEmpty && candidateIsrc == currentIsrc) { + score += 10000; + } + + final candidateTitle = _normalizeMetadataText( + (track['name'] ?? track['title'] ?? '').toString(), + ); + final candidateArtist = _normalizeMetadataText( + (track['artists'] ?? track['artist'] ?? '').toString(), + ); + final candidateAlbum = _normalizeMetadataText( + (track['album_name'] ?? track['album'] ?? '').toString(), + ); + + if (currentTitle.isNotEmpty && candidateTitle.isNotEmpty) { + if (candidateTitle == currentTitle) { + score += 400; + } else if (candidateTitle.contains(currentTitle) || + currentTitle.contains(candidateTitle)) { + score += 180; + } + } + + if (currentArtist.isNotEmpty && candidateArtist.isNotEmpty) { + if (candidateArtist == currentArtist) { + score += 320; + } else if (candidateArtist.contains(currentArtist) || + currentArtist.contains(candidateArtist)) { + score += 140; + } + } + + if (currentAlbum.isNotEmpty && candidateAlbum.isNotEmpty) { + if (candidateAlbum == currentAlbum) { + score += 120; + } else if (candidateAlbum.contains(currentAlbum) || + currentAlbum.contains(candidateAlbum)) { + score += 50; + } + } + + return score; + } + Future _fetchAndFill() async { if (_autoFillFields.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -4254,117 +4514,137 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { setState(() => _fetching = true); try { - // Build search query from current field values final title = _titleCtrl.text.trim(); final artist = _artistCtrl.text.trim(); final album = _albumCtrl.text.trim(); + final currentIsrc = _isrcCtrl.text.trim().toUpperCase(); + Map? best; + String? deezerId; + + try { + final resolved = await _resolveAutoFillTrackFromIdentifiers( + currentIsrc, + ); + if (resolved != null) { + best = resolved.track; + deezerId = resolved.deezerId; + } + } catch (e) { + _log.w('Identifier-first autofill lookup failed: $e'); + } + final queryParts = []; if (title.isNotEmpty) queryParts.add(title); if (artist.isNotEmpty) queryParts.add(artist); - if (album.isNotEmpty) queryParts.add(album); + if (queryParts.isEmpty && album.isNotEmpty) queryParts.add(album); - if (queryParts.isEmpty) { + if (best == null && queryParts.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.editMetadataAutoFillNoResults), - ), + SnackBar(content: Text(context.l10n.editMetadataAutoFillNoResults)), ); } return; } - final query = queryParts.join(' '); - final results = await PlatformBridge.searchTracksWithMetadataProviders( - query, - limit: 5, - ); + final normalizedTitle = _normalizeMetadataText(title); + final normalizedArtist = _normalizeMetadataText(artist); + final normalizedAlbum = _normalizeMetadataText(album); - if (!mounted) return; - - if (results.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.editMetadataAutoFillNoResults)), + if (best == null) { + final query = queryParts.join(' '); + final results = await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 5, ); - return; - } - // Pick best match: prefer ISRC match, then first result - final currentIsrc = _isrcCtrl.text.trim().toUpperCase(); - Map? best; - if (currentIsrc.isNotEmpty) { - for (final r in results) { - final candidateIsrc = - (r['isrc']?.toString() ?? '').trim().toUpperCase(); - if (candidateIsrc == currentIsrc) { - best = r; - break; + if (!mounted) return; + + if (results.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.editMetadataAutoFillNoResults)), + ); + return; + } + + // Pick best match using current metadata, not only provider order. + best = results.first; + var bestScore = -1; + for (final result in results) { + final score = _metadataMatchScore( + result, + currentTitle: normalizedTitle, + currentArtist: normalizedArtist, + currentAlbum: normalizedAlbum, + currentIsrc: currentIsrc, + ); + if (score > bestScore) { + bestScore = score; + best = result; } } } - best ??= results.first; + + final selectedBest = best; + if (selectedBest == null) { + throw StateError('No metadata match resolved for auto-fill'); + } // Extract basic metadata from search result final enriched = { - 'title': (best['name'] ?? '').toString(), - 'artist': (best['artists'] ?? best['artist'] ?? '').toString(), - 'album': (best['album_name'] ?? best['album'] ?? '').toString(), - 'album_artist': (best['album_artist'] ?? '').toString(), - 'date': (best['release_date'] ?? '').toString(), - 'track_number': (best['track_number'] ?? '').toString(), - 'disc_number': (best['disc_number'] ?? '').toString(), - 'isrc': (best['isrc'] ?? '').toString(), + 'title': (selectedBest['name'] ?? '').toString(), + 'artist': (selectedBest['artists'] ?? selectedBest['artist'] ?? '') + .toString(), + 'album': (selectedBest['album_name'] ?? selectedBest['album'] ?? '') + .toString(), + 'album_artist': (selectedBest['album_artist'] ?? '').toString(), + 'date': (selectedBest['release_date'] ?? '').toString(), + 'track_number': (selectedBest['track_number'] ?? '').toString(), + 'disc_number': (selectedBest['disc_number'] ?? '').toString(), + 'isrc': (selectedBest['isrc'] ?? '').toString(), }; + _mergeOnlineTrackData(enriched, selectedBest); - final needsIsrc = _autoFillFields.contains('isrc') && - enriched['isrc']!.isEmpty; - final needsExtended = _autoFillFields.contains('genre') || + final needsIsrc = + _autoFillFields.contains('isrc') && enriched['isrc']!.isEmpty; + final needsExtended = + _autoFillFields.contains('genre') || _autoFillFields.contains('label') || _autoFillFields.contains('copyright'); - final trackId = - (best['spotify_id'] ?? best['id'] ?? '').toString(); - final source = - (best['source'] ?? best['provider_id'] ?? '').toString(); - final isDeezerSource = source.toLowerCase().contains('deezer'); + final rawSpotifyId = _extractRawSpotifyTrackId(selectedBest); - // Resolve Deezer track ID for extended metadata + ISRC - String? deezerId; + deezerId ??= _extractRawDeezerTrackId(selectedBest); + final candidateIsrc = enriched['isrc']!.trim().toUpperCase(); + final deezerLookupIsrc = _looksLikeIsrc(currentIsrc) + ? currentIsrc + : (_looksLikeIsrc(candidateIsrc) ? candidateIsrc : ''); - if ((needsIsrc || needsExtended) && trackId.isNotEmpty) { + if (needsIsrc || needsExtended) { try { - if (isDeezerSource) { - // Source is Deezer — trackId is already a Deezer ID - deezerId = trackId; - } else { - // Source is Spotify/extension — convert to Deezer via SongLink + if (deezerId == null && deezerLookupIsrc.isNotEmpty) { + final deezerResult = await PlatformBridge.searchDeezerByISRC( + deezerLookupIsrc, + ); + deezerId = _extractRawDeezerTrackId(deezerResult); + _mergeOnlineTrackData(enriched, deezerResult); + } + + if (deezerId == null && rawSpotifyId != null) { + // Spotify IDs can be mapped through SongLink to a Deezer track. final deezerData = await PlatformBridge.convertSpotifyToDeezer( 'track', - trackId, + rawSpotifyId, ); final trackData = deezerData['track']; if (trackData is Map) { - final rawId = trackData['spotify_id'] as String?; - if (rawId != null && rawId.startsWith('deezer:')) { - deezerId = rawId.split(':')[1]; - } - // Also grab ISRC and release_date from the conversion response - final convIsrc = (trackData['isrc'] ?? '').toString().trim(); - if (convIsrc.isNotEmpty && enriched['isrc']!.isEmpty) { - enriched['isrc'] = convIsrc; - } - final convDate = - (trackData['release_date'] ?? '').toString().trim(); - if (convDate.isNotEmpty && enriched['date']!.isEmpty) { - enriched['date'] = convDate; - } + deezerId = _extractRawDeezerTrackId(trackData); + _mergeOnlineTrackData(enriched, trackData); } - // Fallback: legacy ID format - deezerId ??= (deezerData['id'] ?? '').toString(); - if (deezerId.isEmpty) deezerId = null; + deezerId ??= _extractRawDeezerTrackId(deezerData); } } catch (_) { - // SongLink conversion is best-effort + // Deezer resolution is best-effort } } @@ -4377,7 +4657,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { 'track', deezerId, ); - final deezerIsrc = (deezerMeta['isrc'] ?? '').toString().trim(); + final trackData = _unwrapTrackPayload(deezerMeta); + _mergeOnlineTrackData(enriched, trackData); + final deezerIsrc = (trackData['isrc'] ?? '').toString().trim(); if (deezerIsrc.isNotEmpty) { enriched['isrc'] = deezerIsrc; } @@ -4402,29 +4684,6 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } } - // Fallback: if still no Deezer ID but we have ISRC, try ISRC lookup - if (needsExtended && - deezerId == null && - enriched['isrc']!.isNotEmpty) { - try { - final deezerResult = await PlatformBridge.searchDeezerByISRC( - enriched['isrc']!, - ); - final fallbackId = - (deezerResult['id'] ?? deezerResult['track_id'] ?? '') - .toString(); - if (fallbackId.isNotEmpty) { - final extended = - await PlatformBridge.getDeezerExtendedMetadata(fallbackId); - if (extended != null) { - enriched['genre'] = extended['genre'] ?? ''; - enriched['label'] = extended['label'] ?? ''; - enriched['copyright'] = extended['copyright'] ?? ''; - } - } - } catch (_) {} - } - if (!mounted) return; // Apply selected fields to controllers @@ -4432,7 +4691,10 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { for (final key in _autoFillFields) { if (key == 'cover') continue; // Handle cover separately below final value = enriched[key]; - if (value != null && value.isNotEmpty && value != '0' && value != 'null') { + if (value != null && + value.isNotEmpty && + value != '0' && + value != 'null') { final ctrl = _controllerForKey(key); if (ctrl != null) { ctrl.text = value; @@ -4444,7 +4706,8 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { // Handle cover art download if (_autoFillFields.contains('cover')) { final coverUrl = - (best['cover_url'] ?? best['images'] ?? '').toString(); + (selectedBest['cover_url'] ?? selectedBest['images'] ?? '') + .toString(); if (coverUrl.isNotEmpty) { try { final tempDir = await Directory.systemTemp.createTemp( @@ -4494,9 +4757,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.snackbarError(e.toString())), - ), + SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), ); } } finally { @@ -4722,9 +4983,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } } catch (e) { if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), + ); } } finally { if (mounted) setState(() => _saving = false); @@ -4882,11 +5143,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ), child: Row( children: [ - Icon( - Icons.travel_explore, - size: 20, - color: cs.primary, - ), + Icon(Icons.travel_explore, size: 20, color: cs.primary), const SizedBox(width: 8), Expanded( child: Text( @@ -4911,9 +5168,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( context.l10n.editMetadataAutoFillDesc, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: cs.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant), ), ), const SizedBox(height: 8), @@ -4976,18 +5233,13 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { const SizedBox(height: 10), // Fetch button Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - bottom: 12, - ), + padding: const EdgeInsets.only(left: 12, right: 12, bottom: 12), child: SizedBox( width: double.infinity, child: FilledButton.icon( - onPressed: - (_fetching || _saving || _autoFillFields.isEmpty) - ? null - : _fetchAndFill, + onPressed: (_fetching || _saving || _autoFillFields.isEmpty) + ? null + : _fetchAndFill, icon: _fetching ? const SizedBox( width: 16, @@ -5029,9 +5281,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ), child: Text( label, - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: cs.primary, - ), + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: cs.primary), ), ), ); From 2b23678c0d98254b097d391662d60d17f4a6e0b3 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:13:45 +0700 Subject: [PATCH 100/145] feat: add FLAC/ALAC bidirectional lossless conversion support - Add _convertToAlac() and _convertToFlac() in ffmpeg_service with single-pass FFmpeg encoding, metadata tags, and cover art embedding - Wire lossless formats (ALAC, FLAC) into single-track convert sheet with dynamic format list based on source format, hidden bitrate for lossless targets, and lossless hint text - Add lossless conversion to batch convert UI in downloaded_album, local_album, and queue_tab screens with lossy-source filtering - Fix M4A quality probe in Go backend: increase audio sample entry buffer from 24 to 32 bytes, read sample rate from correct offset (bytes 28-29) and bit depth from samplesize field (bytes 22-23) - Add l10n keys for lossless confirm dialogs and hints (en, id) --- go_backend/metadata.go | 25 ++- lib/l10n/app_localizations.dart | 23 ++- lib/l10n/app_localizations_de.dart | 23 +++ lib/l10n/app_localizations_en.dart | 26 ++- lib/l10n/app_localizations_es.dart | 26 ++- lib/l10n/app_localizations_fr.dart | 23 +++ lib/l10n/app_localizations_hi.dart | 23 +++ lib/l10n/app_localizations_id.dart | 26 ++- lib/l10n/app_localizations_ja.dart | 23 +++ lib/l10n/app_localizations_ko.dart | 23 +++ lib/l10n/app_localizations_nl.dart | 23 +++ lib/l10n/app_localizations_pt.dart | 26 ++- lib/l10n/app_localizations_ru.dart | 23 +++ lib/l10n/app_localizations_tr.dart | 23 +++ lib/l10n/app_localizations_zh.dart | 26 ++- lib/l10n/arb/app_en.arb | 30 +++- lib/l10n/arb/app_id.arb | 18 +- lib/screens/downloaded_album_screen.dart | 152 ++++++++++------ lib/screens/local_album_screen.dart | 150 ++++++++++------ lib/screens/queue_tab.dart | 154 ++++++++++------ lib/screens/track_metadata_screen.dart | 174 ++++++++++++------ lib/services/ffmpeg_service.dart | 215 ++++++++++++++++++++++- 22 files changed, 1037 insertions(+), 218 deletions(-) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 29ab2d02..391b7e5d 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -743,15 +743,28 @@ func GetM4AQuality(filePath string) (AudioQuality, error) { return AudioQuality{}, err } - buf := make([]byte, 24) + buf := make([]byte, 32) if _, err := f.ReadAt(buf, sampleOffset); err != nil { return AudioQuality{}, fmt.Errorf("failed to read audio sample entry: %w", err) } - sampleRate := int(buf[22])<<8 | int(buf[23]) - bitDepth := 16 - if atomType == "alac" { - bitDepth = 24 + // AudioSampleEntry layout from the box type field: + // [0:4] type ("mp4a"/"alac") + // [4:10] SampleEntry.reserved + // [10:12] data_reference_index + // [12:20] reserved[8] + // [20:22] channelcount + // [22:24] samplesize (bit depth) + // [24:26] pre_defined + // [26:28] reserved + // [28:32] samplerate (16.16 fixed-point) + sampleRate := int(buf[28])<<8 | int(buf[29]) + bitDepth := int(buf[22])<<8 | int(buf[23]) + if bitDepth <= 0 { + bitDepth = 16 + if atomType == "alac" { + bitDepth = 24 + } } return AudioQuality{BitDepth: bitDepth, SampleRate: sampleRate}, nil @@ -874,7 +887,7 @@ func findAudioSampleEntry(f *os.File, start, end, fileSize int64) (int64, string if bestIdx >= 0 { absolute := readPos - int64(len(tail)) + int64(bestIdx) - if absolute+24 > fileSize { + if absolute+32 > fileSize { return 0, "", fmt.Errorf("audio info not found in M4A file") } return absolute, bestType, nil diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 01af801f..15451886 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3883,7 +3883,7 @@ abstract class AppLocalizations { /// Subtitle for convert format menu item /// /// In en, this message translates to: - /// **'Convert to MP3 or Opus'** + /// **'Convert to MP3, Opus, ALAC, or FLAC'** String get trackConvertFormatSubtitle; /// Title of convert bottom sheet @@ -3920,6 +3920,21 @@ abstract class AppLocalizations { String bitrate, ); + /// Confirmation dialog message for lossless-to-lossless conversion + /// + /// In en, this message translates to: + /// **'Convert from {sourceFormat} to {targetFormat}? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'** + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ); + + /// Hint shown when converting between lossless formats + /// + /// In en, this message translates to: + /// **'Lossless conversion — no quality loss'** + String get trackConvertLosslessHint; + /// Snackbar while converting /// /// In en, this message translates to: @@ -4290,6 +4305,12 @@ abstract class AppLocalizations { String bitrate, ); + /// Confirmation dialog message for lossless batch conversion + /// + /// In en, this message translates to: + /// **'Convert {count} {count, plural, =1{track} other{tracks}} to {format}? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'** + String selectionBatchConvertConfirmMessageLossless(int count, String format); + /// Snackbar during batch conversion progress /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c3ee5083..f0137de1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2226,6 +2226,18 @@ class AppLocalizationsDe extends AppLocalizations { return 'Konvertieren von $sourceFormat in $targetFormat bei $bitrate?\n\nDie Originaldatei wird nach der Konvertierung gelöscht.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Konvertiere Audio...'; @@ -2480,6 +2492,17 @@ class AppLocalizationsDe extends AppLocalizations { return 'Konvertiere $count $format $_temp0 zu $bitrate?\n\nOriginaldateien werden nach der Konvertierung gelöscht.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Konvertiere $current von $total...'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 180810f3..041c5bd1 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2176,7 +2176,8 @@ class AppLocalizationsEn extends AppLocalizations { String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => + 'Convert to MP3, Opus, ALAC, or FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -2199,6 +2200,18 @@ class AppLocalizationsEn extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2465,17 @@ class AppLocalizationsEn extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index cea3915b..99849885 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2176,7 +2176,8 @@ class AppLocalizationsEs extends AppLocalizations { String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => + 'Convert to MP3, Opus, ALAC, or FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -2199,6 +2200,18 @@ class AppLocalizationsEs extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2465,17 @@ class AppLocalizationsEs extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 8270c7c2..cec6eae9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2201,6 +2201,18 @@ class AppLocalizationsFr extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2454,6 +2466,17 @@ class AppLocalizationsFr extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 1767c438..9a9ad518 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2199,6 +2199,18 @@ class AppLocalizationsHi extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2464,17 @@ class AppLocalizationsHi extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 67a9c2fc..902d49cd 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2183,7 +2183,8 @@ class AppLocalizationsId extends AppLocalizations { String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => + 'Konversi ke MP3, Opus, ALAC, atau FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -2206,6 +2207,18 @@ class AppLocalizationsId extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Konversi dari $sourceFormat ke $targetFormat? (Lossless — tanpa kehilangan kualitas)\n\nFile asli akan dihapus setelah konversi.'; + } + + @override + String get trackConvertLosslessHint => + 'Konversi lossless — tanpa kehilangan kualitas'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2459,6 +2472,17 @@ class AppLocalizationsId extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index e406ba35..31270670 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2186,6 +2186,18 @@ class AppLocalizationsJa extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'オーディオを変換中...'; @@ -2439,6 +2451,17 @@ class AppLocalizationsJa extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index b8085a70..76f1b737 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2179,6 +2179,18 @@ class AppLocalizationsKo extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2432,6 +2444,17 @@ class AppLocalizationsKo extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index c54e044a..d509e1e7 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2199,6 +2199,18 @@ class AppLocalizationsNl extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2464,17 @@ class AppLocalizationsNl extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ac0f577d..ef87eca7 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2176,7 +2176,8 @@ class AppLocalizationsPt extends AppLocalizations { String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => + 'Convert to MP3, Opus, ALAC, or FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -2199,6 +2200,18 @@ class AppLocalizationsPt extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2465,17 @@ class AppLocalizationsPt extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 76814d38..8bbb8a74 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2252,6 +2252,18 @@ class AppLocalizationsRu extends AppLocalizations { return 'Конвертировать из $sourceFormat в $targetFormat $bitrate?\n\nОригинальный файл будет удален после конвертации.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Конвертация аудио...'; @@ -2511,6 +2523,17 @@ class AppLocalizationsRu extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Конвертация $current из $total...'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index f24d9f88..bbf6ef14 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2211,6 +2211,18 @@ class AppLocalizationsTr extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2464,6 +2476,17 @@ class AppLocalizationsTr extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index d9440cff..465f196c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2176,7 +2176,8 @@ class AppLocalizationsZh extends AppLocalizations { String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => + 'Convert to MP3, Opus, ALAC, or FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -2199,6 +2200,18 @@ class AppLocalizationsZh extends AppLocalizations { return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } + @override + String trackConvertConfirmMessageLossless( + String sourceFormat, + String targetFormat, + ) { + return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertLosslessHint => + 'Lossless conversion — no quality loss'; + @override String get trackConvertConverting => 'Converting audio...'; @@ -2452,6 +2465,17 @@ class AppLocalizationsZh extends AppLocalizations { return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } + @override + String selectionBatchConvertConfirmMessageLossless(int count, String format) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.'; + } + @override String selectionBatchConvertProgress(int current, int total) { return 'Converting $current of $total...'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6da406d0..7f4e1acb 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2873,7 +2873,7 @@ "@trackConvertFormat": { "description": "Menu item - convert audio format" }, - "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "trackConvertFormatSubtitle": "Convert to MP3, Opus, ALAC, or FLAC", "@trackConvertFormatSubtitle": { "description": "Subtitle for convert format menu item" }, @@ -2908,6 +2908,22 @@ } } }, + "trackConvertConfirmMessageLossless": "Convert from {sourceFormat} to {targetFormat}? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless-to-lossless conversion", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + } + } + }, + "trackConvertLosslessHint": "Lossless conversion — no quality loss", + "@trackConvertLosslessHint": { + "description": "Hint shown when converting between lossless formats" + }, "trackConvertConverting": "Converting audio...", "@trackConvertConverting": { "description": "Snackbar while converting" @@ -3259,6 +3275,18 @@ } } }, + "selectionBatchConvertConfirmMessageLossless": "Convert {count} {count, plural, =1{track} other{tracks}} to {format}? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.", + "@selectionBatchConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless batch conversion", + "placeholders": { + "count": { + "type": "int" + }, + "format": { + "type": "String" + } + } + }, "selectionBatchConvertProgress": "Converting {current} of {total}...", "@selectionBatchConvertProgress": { "description": "Snackbar during batch conversion progress", diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index bab66bf2..ff764ec8 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -2809,7 +2809,7 @@ "@trackConvertFormat": { "description": "Menu item - convert audio format" }, - "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "trackConvertFormatSubtitle": "Konversi ke MP3, Opus, ALAC, atau FLAC", "@trackConvertFormatSubtitle": { "description": "Subtitle for convert format menu item" }, @@ -2844,6 +2844,22 @@ } } }, + "trackConvertConfirmMessageLossless": "Konversi dari {sourceFormat} ke {targetFormat}? (Lossless — tanpa kehilangan kualitas)\n\nFile asli akan dihapus setelah konversi.", + "@trackConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless-to-lossless conversion", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + } + } + }, + "trackConvertLosslessHint": "Konversi lossless — tanpa kehilangan kualitas", + "@trackConvertLosslessHint": { + "description": "Hint shown when converting between lossless formats" + }, "trackConvertConverting": "Converting audio...", "@trackConvertConverting": { "description": "Snackbar while converting" diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 55b1b71c..f320aa40 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -912,6 +912,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { ) { String selectedFormat = 'MP3'; String selectedBitrate = '320k'; + bool isLosslessTarget = false; showModalBottomSheet( context: context, @@ -923,7 +924,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['MP3', 'Opus']; + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( @@ -960,51 +961,75 @@ class _DownloadedAlbumScreenState extends ConsumerState { ), ), const SizedBox(height: 8), - Row( - children: formats.map((format) { - final isSelected = format == selectedFormat; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(format), - selected: isSelected, - onSelected: (selected) { - if (selected) { - setSheetState(() { - selectedFormat = format; - selectedBitrate = format == 'Opus' - ? '128k' - : '320k'; - }); - } - }, - ), - ); - }).toList(), - ), - const SizedBox(height: 16), - Text( - context.l10n.trackConvertBitrate, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), Wrap( spacing: 8, - children: bitrates.map((br) { - final isSelected = br == selectedBitrate; + children: formats.map((format) { + final isSelected = format == selectedFormat; return ChoiceChip( - label: Text(br), + label: Text(format), selected: isSelected, onSelected: (selected) { if (selected) { - setSheetState(() => selectedBitrate = br); + setSheetState(() { + selectedFormat = format; + isLosslessTarget = + format == 'ALAC' || format == 'FLAC'; + if (!isLosslessTarget) { + selectedBitrate = + format == 'Opus' ? '128k' : '320k'; + } + }); } }, ); }).toList(), ), + if (!isLosslessTarget) ...[ + const SizedBox(height: 16), + Text( + context.l10n.trackConvertBitrate, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: bitrates.map((br) { + final isSelected = br == selectedBitrate; + return ChoiceChip( + label: Text(br), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setSheetState(() => selectedBitrate = br); + } + }, + ); + }).toList(), + ), + ], + if (isLosslessTarget) ...[ + const SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.verified, + size: 16, + color: colorScheme.primary, + ), + const SizedBox(width: 6), + Text( + context.l10n.trackConvertLosslessHint, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + ), + ), + ], + ), + ], const SizedBox(height: 24), SizedBox( width: double.infinity, @@ -1057,12 +1082,19 @@ class _DownloadedAlbumScreenState extends ConsumerState { : item.filePath.toLowerCase(); final ext = nameToCheck.endsWith('.flac') ? 'FLAC' + : nameToCheck.endsWith('.m4a') + ? 'M4A' : nameToCheck.endsWith('.mp3') ? 'MP3' : (nameToCheck.endsWith('.opus') || nameToCheck.endsWith('.ogg')) ? 'Opus' : null; - if (ext != null && ext != targetFormat) selected.add(item); + if (ext == null || ext == targetFormat) continue; + // Skip lossy sources when target is lossless (pointless re-encoding) + final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; + final isLosslessSource = ext == 'FLAC' || ext == 'M4A'; + if (isLosslessTarget && !isLosslessSource) continue; + selected.add(item); } if (selected.isEmpty) { @@ -1074,16 +1106,22 @@ class _DownloadedAlbumScreenState extends ConsumerState { return; } + final isLossless = targetFormat == 'ALAC' || targetFormat == 'FLAC'; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(context.l10n.selectionBatchConvertConfirmTitle), content: Text( - context.l10n.selectionBatchConvertConfirmMessage( - selected.length, - targetFormat, - bitrate, - ), + isLossless + ? context.l10n.selectionBatchConvertConfirmMessageLossless( + selected.length, + targetFormat, + ) + : context.l10n.selectionBatchConvertConfirmMessage( + selected.length, + targetFormat, + bitrate, + ), ), actions: [ TextButton( @@ -1103,8 +1141,10 @@ class _DownloadedAlbumScreenState extends ConsumerState { int successCount = 0; final total = selected.length; final historyDb = HistoryDatabase.instance; - final newQuality = - '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; + final newQuality = (targetFormat.toUpperCase() == 'ALAC' || + targetFormat.toUpperCase() == 'FLAC') + ? '${targetFormat.toUpperCase()} Lossless' + : '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; final settings = ref.read(settingsProvider); final shouldEmbedLyrics = settings.embedLyrics && settings.lyricsMode != 'external'; @@ -1207,13 +1247,27 @@ class _DownloadedAlbumScreenState extends ConsumerState { final baseName = dotIdx > 0 ? oldFileName.substring(0, dotIdx) : oldFileName; - final newExt = targetFormat.toLowerCase() == 'opus' - ? '.opus' - : '.mp3'; + String newExt; + String mimeType; + switch (targetFormat.toLowerCase()) { + case 'opus': + newExt = '.opus'; + mimeType = 'audio/opus'; + break; + case 'alac': + newExt = '.m4a'; + mimeType = 'audio/mp4'; + break; + case 'flac': + newExt = '.flac'; + mimeType = 'audio/flac'; + break; + default: + newExt = '.mp3'; + mimeType = 'audio/mpeg'; + break; + } final newFileName = '$baseName$newExt'; - final mimeType = targetFormat.toLowerCase() == 'opus' - ? 'audio/opus' - : 'audio/mpeg'; final safUri = await PlatformBridge.createSafFileFromPath( treeUri: treeUri, diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 122a4bab..1b662324 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -1131,6 +1131,7 @@ class _LocalAlbumScreenState extends ConsumerState { ) { String selectedFormat = 'MP3'; String selectedBitrate = '320k'; + bool isLosslessTarget = false; showModalBottomSheet( context: context, @@ -1142,7 +1143,7 @@ class _LocalAlbumScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['MP3', 'Opus']; + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( @@ -1179,51 +1180,75 @@ class _LocalAlbumScreenState extends ConsumerState { ), ), const SizedBox(height: 8), - Row( - children: formats.map((format) { - final isSelected = format == selectedFormat; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(format), - selected: isSelected, - onSelected: (selected) { - if (selected) { - setSheetState(() { - selectedFormat = format; - selectedBitrate = format == 'Opus' - ? '128k' - : '320k'; - }); - } - }, - ), - ); - }).toList(), - ), - const SizedBox(height: 16), - Text( - context.l10n.trackConvertBitrate, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), Wrap( spacing: 8, - children: bitrates.map((br) { - final isSelected = br == selectedBitrate; + children: formats.map((format) { + final isSelected = format == selectedFormat; return ChoiceChip( - label: Text(br), + label: Text(format), selected: isSelected, onSelected: (selected) { if (selected) { - setSheetState(() => selectedBitrate = br); + setSheetState(() { + selectedFormat = format; + isLosslessTarget = + format == 'ALAC' || format == 'FLAC'; + if (!isLosslessTarget) { + selectedBitrate = + format == 'Opus' ? '128k' : '320k'; + } + }); } }, ); }).toList(), ), + if (!isLosslessTarget) ...[ + const SizedBox(height: 16), + Text( + context.l10n.trackConvertBitrate, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: bitrates.map((br) { + final isSelected = br == selectedBitrate; + return ChoiceChip( + label: Text(br), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setSheetState(() => selectedBitrate = br); + } + }, + ); + }).toList(), + ), + ], + if (isLosslessTarget) ...[ + const SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.verified, + size: 16, + color: colorScheme.primary, + ), + const SizedBox(width: 6), + Text( + context.l10n.trackConvertLosslessHint, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + ), + ), + ], + ), + ], const SizedBox(height: 24), SizedBox( width: double.infinity, @@ -1276,6 +1301,8 @@ class _LocalAlbumScreenState extends ConsumerState { final fmt = item.format!.toLowerCase(); if (fmt == 'flac') { currentFormat = 'FLAC'; + } else if (fmt == 'm4a') { + currentFormat = 'M4A'; } else if (fmt == 'mp3') { currentFormat = 'MP3'; } else if (fmt == 'opus' || fmt == 'ogg') { @@ -1287,15 +1314,20 @@ class _LocalAlbumScreenState extends ConsumerState { final lower = item.filePath.toLowerCase(); if (lower.endsWith('.flac')) { currentFormat = 'FLAC'; + } else if (lower.endsWith('.m4a')) { + currentFormat = 'M4A'; } else if (lower.endsWith('.mp3')) { currentFormat = 'MP3'; } else if (lower.endsWith('.opus') || lower.endsWith('.ogg')) { currentFormat = 'Opus'; } } - if (currentFormat != null && currentFormat != targetFormat) { - selected.add(item); - } + if (currentFormat == null || currentFormat == targetFormat) continue; + // Skip lossy sources when target is lossless (pointless re-encoding) + final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; + final isLosslessSource = currentFormat == 'FLAC' || currentFormat == 'M4A'; + if (isLosslessTarget && !isLosslessSource) continue; + selected.add(item); } if (selected.isEmpty) { @@ -1307,16 +1339,22 @@ class _LocalAlbumScreenState extends ConsumerState { return; } + final isLossless = targetFormat == 'ALAC' || targetFormat == 'FLAC'; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(context.l10n.selectionBatchConvertConfirmTitle), content: Text( - context.l10n.selectionBatchConvertConfirmMessage( - selected.length, - targetFormat, - bitrate, - ), + isLossless + ? context.l10n.selectionBatchConvertConfirmMessageLossless( + selected.length, + targetFormat, + ) + : context.l10n.selectionBatchConvertConfirmMessage( + selected.length, + targetFormat, + bitrate, + ), ), actions: [ TextButton( @@ -1481,13 +1519,27 @@ class _LocalAlbumScreenState extends ConsumerState { final baseName = dotIdx > 0 ? oldFileName.substring(0, dotIdx) : oldFileName; - final newExt = targetFormat.toLowerCase() == 'opus' - ? '.opus' - : '.mp3'; + String newExt; + String mimeType; + switch (targetFormat.toLowerCase()) { + case 'opus': + newExt = '.opus'; + mimeType = 'audio/opus'; + break; + case 'alac': + newExt = '.m4a'; + mimeType = 'audio/mp4'; + break; + case 'flac': + newExt = '.flac'; + mimeType = 'audio/flac'; + break; + default: + newExt = '.mp3'; + mimeType = 'audio/mpeg'; + break; + } final newFileName = '$baseName$newExt'; - final mimeType = targetFormat.toLowerCase() == 'opus' - ? 'audio/opus' - : 'audio/mpeg'; final safUri = await PlatformBridge.createSafFileFromPath( treeUri: treeUri, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 52ca3309..64f9c0e3 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4757,6 +4757,7 @@ class _QueueTabState extends ConsumerState { ) async { String selectedFormat = 'MP3'; String selectedBitrate = '320k'; + bool isLosslessTarget = false; var didStartConversion = false; _hideSelectionOverlay(); @@ -4772,7 +4773,7 @@ class _QueueTabState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['MP3', 'Opus']; + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( @@ -4809,51 +4810,75 @@ class _QueueTabState extends ConsumerState { ), ), const SizedBox(height: 8), - Row( - children: formats.map((format) { - final isSelected = format == selectedFormat; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(format), - selected: isSelected, - onSelected: (selected) { - if (selected) { - setSheetState(() { - selectedFormat = format; - selectedBitrate = format == 'Opus' - ? '128k' - : '320k'; - }); - } - }, - ), - ); - }).toList(), - ), - const SizedBox(height: 16), - Text( - context.l10n.trackConvertBitrate, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), Wrap( spacing: 8, - children: bitrates.map((br) { - final isSelected = br == selectedBitrate; + children: formats.map((format) { + final isSelected = format == selectedFormat; return ChoiceChip( - label: Text(br), + label: Text(format), selected: isSelected, onSelected: (selected) { if (selected) { - setSheetState(() => selectedBitrate = br); + setSheetState(() { + selectedFormat = format; + isLosslessTarget = + format == 'ALAC' || format == 'FLAC'; + if (!isLosslessTarget) { + selectedBitrate = + format == 'Opus' ? '128k' : '320k'; + } + }); } }, ); }).toList(), ), + if (!isLosslessTarget) ...[ + const SizedBox(height: 16), + Text( + context.l10n.trackConvertBitrate, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: bitrates.map((br) { + final isSelected = br == selectedBitrate; + return ChoiceChip( + label: Text(br), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setSheetState(() => selectedBitrate = br); + } + }, + ); + }).toList(), + ), + ], + if (isLosslessTarget) ...[ + const SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.verified, + size: 16, + color: colorScheme.primary, + ), + const SizedBox(width: 6), + Text( + context.l10n.trackConvertLosslessHint, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + ), + ), + ], + ), + ], const SizedBox(height: 24), SizedBox( width: double.infinity, @@ -4929,14 +4954,19 @@ class _QueueTabState extends ConsumerState { } final ext = nameToCheck.endsWith('.flac') ? 'FLAC' + : nameToCheck.endsWith('.m4a') + ? 'M4A' : nameToCheck.endsWith('.mp3') ? 'MP3' : (nameToCheck.endsWith('.opus') || nameToCheck.endsWith('.ogg')) ? 'Opus' : null; - if (ext != null && ext != targetFormat) { - selectedItems.add(item); - } + if (ext == null || ext == targetFormat) continue; + // Skip lossy sources when target is lossless (pointless re-encoding) + final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; + final isLosslessSource = ext == 'FLAC' || ext == 'M4A'; + if (isLosslessTarget && !isLosslessSource) continue; + selectedItems.add(item); } if (selectedItems.isEmpty) { @@ -4949,16 +4979,22 @@ class _QueueTabState extends ConsumerState { } // Confirm + final isLossless = targetFormat == 'ALAC' || targetFormat == 'FLAC'; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(context.l10n.selectionBatchConvertConfirmTitle), content: Text( - context.l10n.selectionBatchConvertConfirmMessage( - selectedItems.length, - targetFormat, - bitrate, - ), + isLossless + ? context.l10n.selectionBatchConvertConfirmMessageLossless( + selectedItems.length, + targetFormat, + ) + : context.l10n.selectionBatchConvertConfirmMessage( + selectedItems.length, + targetFormat, + bitrate, + ), ), actions: [ TextButton( @@ -4978,8 +5014,10 @@ class _QueueTabState extends ConsumerState { int successCount = 0; final total = selectedItems.length; final historyDb = HistoryDatabase.instance; - final newQuality = - '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; + final newQuality = (targetFormat.toUpperCase() == 'ALAC' || + targetFormat.toUpperCase() == 'FLAC') + ? '${targetFormat.toUpperCase()} Lossless' + : '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; final settings = ref.read(settingsProvider); final shouldEmbedLyrics = settings.embedLyrics && settings.lyricsMode != 'external'; @@ -5093,13 +5131,27 @@ class _QueueTabState extends ConsumerState { final baseName = dotIdx > 0 ? oldFileName.substring(0, dotIdx) : oldFileName; - final newExt = targetFormat.toLowerCase() == 'opus' - ? '.opus' - : '.mp3'; + String newExt; + String mimeType; + switch (targetFormat.toLowerCase()) { + case 'opus': + newExt = '.opus'; + mimeType = 'audio/opus'; + break; + case 'alac': + newExt = '.m4a'; + mimeType = 'audio/mp4'; + break; + case 'flac': + newExt = '.flac'; + mimeType = 'audio/flac'; + break; + default: + newExt = '.mp3'; + mimeType = 'audio/mpeg'; + break; + } final newFileName = '$baseName$newExt'; - final mimeType = targetFormat.toLowerCase() == 'opus' - ? 'audio/opus' - : 'audio/mpeg'; final safUri = await PlatformBridge.createSafFileFromPath( treeUri: treeUri, diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 85e803b6..744acf54 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -2662,6 +2662,7 @@ class _TrackMetadataScreenState extends ConsumerState { bool get _isConvertibleFormat { final lower = cleanFilePath.toLowerCase(); return lower.endsWith('.flac') || + lower.endsWith('.m4a') || lower.endsWith('.mp3') || lower.endsWith('.opus') || lower.endsWith('.ogg'); @@ -2692,6 +2693,7 @@ class _TrackMetadataScreenState extends ConsumerState { } final lower = cleanFilePath.toLowerCase(); if (lower.endsWith('.flac')) return 'FLAC'; + if (lower.endsWith('.m4a')) return 'M4A'; if (lower.endsWith('.mp3')) return 'MP3'; if (lower.endsWith('.opus') || lower.endsWith('.ogg')) return 'Opus'; if (lower.endsWith('.cue')) return 'CUE'; @@ -2749,8 +2751,12 @@ class _TrackMetadataScreenState extends ConsumerState { } String _buildConvertedQualityLabel(String targetFormat, String bitrate) { + final upper = targetFormat.toUpperCase(); + if (upper == 'ALAC' || upper == 'FLAC') { + return '$upper Lossless'; + } final normalizedBitrate = bitrate.trim().toLowerCase(); - return '${targetFormat.toUpperCase()} $normalizedBitrate'; + return '$upper $normalizedBitrate'; } String? _extractLossyBitrateLabel(String? quality) { @@ -2790,17 +2796,27 @@ class _TrackMetadataScreenState extends ConsumerState { void _showConvertSheet(BuildContext context) { final currentFormat = _currentFileFormat; - // Available target formats (exclude current) - final formats = [ - 'MP3', - 'Opus', - ].where((f) => f != currentFormat).toList(); + final isLosslessSource = + currentFormat == 'FLAC' || currentFormat == 'M4A'; + + // Build available target formats based on source + final formats = []; if (currentFormat == 'FLAC') { - // FLAC can convert to both + formats.addAll(['ALAC', 'MP3', 'Opus']); + } else if (currentFormat == 'M4A') { + formats.addAll(['FLAC', 'MP3', 'Opus']); + } else if (currentFormat == 'MP3') { + formats.add('Opus'); + } else if (currentFormat == 'Opus') { + formats.add('MP3'); + } else { + formats.addAll(['MP3', 'Opus']); } String selectedFormat = formats.first; String selectedBitrate = selectedFormat == 'Opus' ? '128k' : '320k'; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; showModalBottomSheet( context: context, @@ -2849,53 +2865,79 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), const SizedBox(height: 8), - Row( - children: formats.map((format) { - final isSelected = format == selectedFormat; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(format), - selected: isSelected, - onSelected: (selected) { - if (selected) { - setSheetState(() { - selectedFormat = format; - // Reset bitrate to default for format - selectedBitrate = format == 'Opus' - ? '128k' - : '320k'; - }); - } - }, - ), - ); - }).toList(), - ), - const SizedBox(height: 16), - - Text( - context.l10n.trackConvertBitrate, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), Wrap( spacing: 8, - children: bitrates.map((br) { - final isSelected = br == selectedBitrate; + children: formats.map((format) { + final isSelected = format == selectedFormat; return ChoiceChip( - label: Text(br), + label: Text(format), selected: isSelected, onSelected: (selected) { if (selected) { - setSheetState(() => selectedBitrate = br); + setSheetState(() { + selectedFormat = format; + isLosslessTarget = + format == 'ALAC' || format == 'FLAC'; + if (!isLosslessTarget) { + selectedBitrate = + format == 'Opus' ? '128k' : '320k'; + } + }); } }, ); }).toList(), ), + + // Only show bitrate for lossy targets + if (!isLosslessTarget) ...[ + const SizedBox(height: 16), + Text( + context.l10n.trackConvertBitrate, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: bitrates.map((br) { + final isSelected = br == selectedBitrate; + return ChoiceChip( + label: Text(br), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setSheetState(() => selectedBitrate = br); + } + }, + ); + }).toList(), + ), + ], + + // Show lossless indicator + if (isLosslessTarget && isLosslessSource) ...[ + const SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.verified, + size: 16, + color: colorScheme.primary, + ), + const SizedBox(width: 6), + Text( + context.l10n.trackConvertLosslessHint, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + ), + ), + ], + ), + ], const SizedBox(height: 24), SizedBox( @@ -2917,7 +2959,9 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), child: Text( - '$currentFormat -> $selectedFormat @ $selectedBitrate', + isLosslessTarget + ? '$currentFormat -> $selectedFormat (Lossless)' + : '$currentFormat -> $selectedFormat @ $selectedBitrate', ), ), ), @@ -3402,17 +3446,25 @@ class _TrackMetadataScreenState extends ConsumerState { required String targetFormat, required String bitrate, }) { + final isLossless = + targetFormat.toUpperCase() == 'ALAC' || + targetFormat.toUpperCase() == 'FLAC'; showDialog( context: context, builder: (dialogContext) { return AlertDialog( title: Text(dialogContext.l10n.trackConvertConfirmTitle), content: Text( - dialogContext.l10n.trackConvertConfirmMessage( - sourceFormat, - targetFormat, - bitrate, - ), + isLossless + ? dialogContext.l10n.trackConvertConfirmMessageLossless( + sourceFormat, + targetFormat, + ) + : dialogContext.l10n.trackConvertConfirmMessage( + sourceFormat, + targetFormat, + bitrate, + ), ), actions: [ TextButton( @@ -3561,11 +3613,27 @@ class _TrackMetadataScreenState extends ConsumerState { final baseName = dotIdx > 0 ? oldFileName.substring(0, dotIdx) : oldFileName; - final newExt = targetFormat.toLowerCase() == 'opus' ? '.opus' : '.mp3'; + String newExt; + String mimeType; + switch (targetFormat.toLowerCase()) { + case 'opus': + newExt = '.opus'; + mimeType = 'audio/opus'; + break; + case 'alac': + newExt = '.m4a'; + mimeType = 'audio/mp4'; + break; + case 'flac': + newExt = '.flac'; + mimeType = 'audio/flac'; + break; + default: // mp3 + newExt = '.mp3'; + mimeType = 'audio/mpeg'; + break; + } final newFileName = '$baseName$newExt'; - final mimeType = targetFormat.toLowerCase() == 'opus' - ? 'audio/opus' - : 'audio/mpeg'; final safUri = await PlatformBridge.createSafFileFromPath( treeUri: treeUri, diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index b9aa6603..cb533cb0 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1209,7 +1209,8 @@ class FFmpegService { } /// Unified audio format conversion with full metadata + cover preservation. - /// Supports: FLAC/MP3/Opus -> MP3/Opus (any direction except same format). + /// Supports: FLAC/M4A/MP3/Opus -> MP3/Opus/ALAC/FLAC. + /// ALAC and FLAC targets are lossless (bitrate parameter is ignored). /// Returns the new file path on success, null on failure. static Future convertAudioFormat({ required String inputPath, @@ -1220,11 +1221,30 @@ class FFmpegService { bool deleteOriginal = true, }) async { final format = targetFormat.toLowerCase(); - if (format != 'mp3' && format != 'opus') { + if (!const {'mp3', 'opus', 'alac', 'flac'}.contains(format)) { _log.e('Unsupported target format: $targetFormat'); return null; } + // Lossless targets: dedicated single-pass methods + if (format == 'alac') { + return _convertToAlac( + inputPath: inputPath, + metadata: metadata, + coverPath: coverPath, + deleteOriginal: deleteOriginal, + ); + } + if (format == 'flac') { + return _convertToFlac( + inputPath: inputPath, + metadata: metadata, + coverPath: coverPath, + deleteOriginal: deleteOriginal, + ); + } + + // Lossy targets: MP3 / Opus final extension = format == 'opus' ? '.opus' : '.mp3'; final outputPath = _buildOutputPath(inputPath, extension); @@ -1296,6 +1316,197 @@ class FFmpegService { return outputPath; } + /// Convert any audio format to ALAC (Apple Lossless) in an M4A container. + /// Metadata and cover art are embedded in a single FFmpeg pass. + static Future _convertToAlac({ + required String inputPath, + required Map metadata, + String? coverPath, + bool deleteOriginal = true, + }) async { + final outputPath = _buildOutputPath(inputPath, '.m4a'); + + final cmdBuffer = StringBuffer(); + cmdBuffer.write('-i "$inputPath" '); + + // Cover art as second input for M4A attached picture + final hasCover = coverPath != null && + coverPath.trim().isNotEmpty && + await File(coverPath).exists(); + if (hasCover) { + cmdBuffer.write('-i "$coverPath" '); + } + + cmdBuffer.write('-map 0:a '); + if (hasCover) { + cmdBuffer.write('-map 1:v -c:v copy -disposition:v:0 attached_pic '); + } + cmdBuffer.write('-c:a alac '); + cmdBuffer.write('-map_metadata -1 '); + + // Embed M4A metadata tags + final m4aTags = _convertToM4aTags(metadata); + for (final entry in m4aTags.entries) { + final sanitized = entry.value.replaceAll('"', '\\"'); + cmdBuffer.write('-metadata ${entry.key}="$sanitized" '); + } + + cmdBuffer.write('"$outputPath" -y'); + + _log.i( + 'Converting ${inputPath.split(Platform.pathSeparator).last} to ALAC', + ); + final result = await _execute(cmdBuffer.toString()); + + if (!result.success) { + _log.e('ALAC conversion failed: ${result.output}'); + return null; + } + + if (deleteOriginal) { + try { + await File(inputPath).delete(); + _log.i( + 'Deleted original: ${inputPath.split(Platform.pathSeparator).last}', + ); + } catch (e) { + _log.w('Failed to delete original: $e'); + } + } + + return outputPath; + } + + /// Convert any audio format to FLAC. + /// Metadata (Vorbis comments) and cover art (METADATA_BLOCK_PICTURE) are + /// embedded in a single FFmpeg pass. + static Future _convertToFlac({ + required String inputPath, + required Map metadata, + String? coverPath, + bool deleteOriginal = true, + }) async { + final outputPath = _buildOutputPath(inputPath, '.flac'); + + final cmdBuffer = StringBuffer(); + cmdBuffer.write('-i "$inputPath" '); + cmdBuffer.write('-map 0:a '); + cmdBuffer.write('-c:a flac -compression_level 8 '); + cmdBuffer.write('-map_metadata -1 '); + + // Embed Vorbis comments + for (final entry in metadata.entries) { + if (entry.value.trim().isEmpty) continue; + final sanitized = entry.value.replaceAll('"', '\\"'); + cmdBuffer.write('-metadata ${entry.key}="$sanitized" '); + } + + // Embed cover art via METADATA_BLOCK_PICTURE (same approach as Opus) + if (coverPath != null && coverPath.trim().isNotEmpty) { + try { + if (await File(coverPath).exists()) { + final pictureBlock = await _createMetadataBlockPicture(coverPath); + if (pictureBlock != null) { + final escapedBlock = pictureBlock.replaceAll('"', '\\"'); + cmdBuffer.write( + '-metadata METADATA_BLOCK_PICTURE="$escapedBlock" ', + ); + _log.d( + 'Created METADATA_BLOCK_PICTURE for FLAC (${pictureBlock.length} chars)', + ); + } + } + } catch (e) { + _log.e('Error creating METADATA_BLOCK_PICTURE for FLAC: $e'); + } + } + + cmdBuffer.write('"$outputPath" -y'); + + _log.i( + 'Converting ${inputPath.split(Platform.pathSeparator).last} to FLAC', + ); + final result = await _execute(cmdBuffer.toString()); + + if (!result.success) { + _log.e('FLAC conversion failed: ${result.output}'); + return null; + } + + if (deleteOriginal) { + try { + await File(inputPath).delete(); + _log.i( + 'Deleted original: ${inputPath.split(Platform.pathSeparator).last}', + ); + } catch (e) { + _log.w('Failed to delete original: $e'); + } + } + + return outputPath; + } + + /// Map Vorbis comment keys to M4A/MP4 metadata tag names for FFmpeg. + static Map _convertToM4aTags( + Map metadata, + ) { + final m4aMap = {}; + + for (final entry in metadata.entries) { + final key = entry.key.toUpperCase().replaceAll(RegExp(r'[^A-Z0-9]'), ''); + final value = entry.value; + if (value.trim().isEmpty) continue; + + switch (key) { + case 'TITLE': + m4aMap['title'] = value; + break; + case 'ARTIST': + m4aMap['artist'] = value; + break; + case 'ALBUM': + m4aMap['album'] = value; + break; + case 'ALBUMARTIST': + m4aMap['album_artist'] = value; + break; + case 'TRACKNUMBER': + case 'TRACK': + case 'TRCK': + m4aMap['track'] = value; + break; + case 'DISCNUMBER': + case 'DISC': + case 'TPOS': + m4aMap['disc'] = value; + break; + case 'DATE': + case 'YEAR': + m4aMap['date'] = value; + break; + case 'GENRE': + m4aMap['genre'] = value; + break; + case 'COMPOSER': + m4aMap['composer'] = value; + break; + case 'COMMENT': + m4aMap['comment'] = value; + break; + case 'COPYRIGHT': + m4aMap['copyright'] = value; + break; + case 'LYRICS': + case 'UNSYNCEDLYRICS': + m4aMap['lyrics'] = value; + break; + } + } + + return m4aMap; + } + static Map _convertToId3Tags( Map vorbisMetadata, ) { From 47bd24c1bd91494063f966b99a49c4d847e6841a Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:26:53 +0700 Subject: [PATCH 101/145] fix: preserve metadata and cover art in ALAC/M4A to FLAC conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use -map_metadata 0 instead of -map_metadata -1 so FFmpeg copies and auto-remaps source tags (M4A/ID3 → Vorbis comments) as a base - Add _normalizeToVorbisComments() to filter technical fields (BIT_DEPTH, SAMPLE_RATE, DURATION) and normalize key variations to standard Vorbis comment names before applying overrides - Switch cover art embedding from METADATA_BLOCK_PICTURE base64 (unreliable on Android due to command-line length limits) to -i cover -map 1:v -disposition attached_pic (same proven approach as embedMetadata and _convertToAlac) - Drop zero-value track/disc numbers from override map to prevent clobbering source metadata with '0' from Go readFileMetadata --- lib/services/ffmpeg_service.dart | 129 ++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 28 deletions(-) diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index cb533cb0..dfb88847 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1378,8 +1378,11 @@ class FFmpegService { } /// Convert any audio format to FLAC. - /// Metadata (Vorbis comments) and cover art (METADATA_BLOCK_PICTURE) are - /// embedded in a single FFmpeg pass. + /// Source metadata is preserved via -map_metadata 0 (FFmpeg auto-remaps + /// tag names between container formats), then explicit Vorbis comment + /// overrides are applied from the [metadata] map. + /// Cover art is embedded via a second input stream (same approach as + /// [embedMetadata] and [_convertToAlac]). static Future _convertToFlac({ required String inputPath, required Map metadata, @@ -1390,35 +1393,32 @@ class FFmpegService { final cmdBuffer = StringBuffer(); cmdBuffer.write('-i "$inputPath" '); - cmdBuffer.write('-map 0:a '); - cmdBuffer.write('-c:a flac -compression_level 8 '); - cmdBuffer.write('-map_metadata -1 '); - // Embed Vorbis comments - for (final entry in metadata.entries) { - if (entry.value.trim().isEmpty) continue; - final sanitized = entry.value.replaceAll('"', '\\"'); - cmdBuffer.write('-metadata ${entry.key}="$sanitized" '); + // Cover art as second input for attached picture + final hasCover = coverPath != null && + coverPath.trim().isNotEmpty && + await File(coverPath).exists(); + if (hasCover) { + cmdBuffer.write('-i "$coverPath" '); } - // Embed cover art via METADATA_BLOCK_PICTURE (same approach as Opus) - if (coverPath != null && coverPath.trim().isNotEmpty) { - try { - if (await File(coverPath).exists()) { - final pictureBlock = await _createMetadataBlockPicture(coverPath); - if (pictureBlock != null) { - final escapedBlock = pictureBlock.replaceAll('"', '\\"'); - cmdBuffer.write( - '-metadata METADATA_BLOCK_PICTURE="$escapedBlock" ', - ); - _log.d( - 'Created METADATA_BLOCK_PICTURE for FLAC (${pictureBlock.length} chars)', - ); - } - } - } catch (e) { - _log.e('Error creating METADATA_BLOCK_PICTURE for FLAC: $e'); - } + cmdBuffer.write('-map 0:a '); + if (hasCover) { + cmdBuffer.write('-map 1:v -c:v copy -disposition:v:0 attached_pic '); + cmdBuffer.write('-metadata:s:v title="Album cover" '); + cmdBuffer.write('-metadata:s:v comment="Cover (front)" '); + } + cmdBuffer.write('-c:a flac -compression_level 8 '); + + // Copy source metadata as base (FFmpeg auto-remaps M4A/ID3 tags to + // Vorbis comment names), then override with our explicit values. + cmdBuffer.write('-map_metadata 0 '); + + // Apply normalized Vorbis comment overrides + final vorbisComments = _normalizeToVorbisComments(metadata); + for (final entry in vorbisComments.entries) { + final sanitized = entry.value.replaceAll('"', '\\"'); + cmdBuffer.write('-metadata ${entry.key}="$sanitized" '); } cmdBuffer.write('"$outputPath" -y'); @@ -1447,6 +1447,79 @@ class FFmpegService { return outputPath; } + /// Normalize metadata keys to standard Vorbis comment names and filter out + /// technical/non-tag fields (bit_depth, sample_rate, duration, etc.). + static Map _normalizeToVorbisComments( + Map metadata, + ) { + final vorbis = {}; + + for (final entry in metadata.entries) { + final key = entry.key.toUpperCase().replaceAll(RegExp(r'[^A-Z0-9]'), ''); + final value = entry.value; + if (value.trim().isEmpty) continue; + + switch (key) { + case 'TITLE': + vorbis['TITLE'] = value; + break; + case 'ARTIST': + vorbis['ARTIST'] = value; + break; + case 'ALBUM': + vorbis['ALBUM'] = value; + break; + case 'ALBUMARTIST': + vorbis['ALBUMARTIST'] = value; + break; + case 'TRACKNUMBER': + case 'TRACKNBR': + case 'TRACK': + case 'TRCK': + if (value != '0') vorbis['TRACKNUMBER'] = value; + break; + case 'DISCNUMBER': + case 'DISC': + case 'TPOS': + if (value != '0') vorbis['DISCNUMBER'] = value; + break; + case 'DATE': + case 'YEAR': + vorbis['DATE'] = value; + break; + case 'GENRE': + vorbis['GENRE'] = value; + break; + case 'ISRC': + vorbis['ISRC'] = value; + break; + case 'LABEL': + case 'ORGANIZATION': + vorbis['ORGANIZATION'] = value; + break; + case 'COPYRIGHT': + vorbis['COPYRIGHT'] = value; + break; + case 'COMPOSER': + vorbis['COMPOSER'] = value; + break; + case 'COMMENT': + vorbis['COMMENT'] = value; + break; + case 'LYRICS': + case 'UNSYNCEDLYRICS': + // Write both keys for compatibility with different FLAC readers + vorbis['LYRICS'] = value; + vorbis['UNSYNCEDLYRICS'] = value; + break; + // Technical fields (BIT_DEPTH, SAMPLE_RATE, DURATION, etc.) are + // intentionally dropped — they are not Vorbis comment tags. + } + } + + return vorbis; + } + /// Map Vorbis comment keys to M4A/MP4 metadata tag names for FFmpeg. static Map _convertToM4aTags( Map metadata, From 6ebe0c51ce78b88c6031f02e027b02adb4a385f5 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:39:11 +0700 Subject: [PATCH 102/145] fix: filter batch convert target formats based on source formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exclude same-format and lossy-to-lossless targets from the batch convert sheet so users cannot pick pointless conversions like FLAC→FLAC. Also clean up redundant inline comments. --- lib/screens/downloaded_album_screen.dart | 42 ++++++++++++++++-- lib/screens/local_album_screen.dart | 54 ++++++++++++++++++++++-- lib/screens/queue_tab.dart | 48 +++++++++++++++++++-- lib/services/ffmpeg_service.dart | 19 ++------- 4 files changed, 135 insertions(+), 28 deletions(-) diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index f320aa40..ecbdbbe7 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -910,9 +910,44 @@ class _DownloadedAlbumScreenState extends ConsumerState { BuildContext context, List allTracks, ) { - String selectedFormat = 'MP3'; - String selectedBitrate = '320k'; - bool isLosslessTarget = false; + final tracksById = {for (final t in allTracks) t.id: t}; + final sourceFormats = {}; + for (final id in _selectedIds) { + final item = tracksById[id]; + if (item == null) continue; + final nameToCheck = + (item.safFileName != null && item.safFileName!.isNotEmpty) + ? item.safFileName!.toLowerCase() + : item.filePath.toLowerCase(); + final ext = nameToCheck.endsWith('.flac') + ? 'FLAC' + : nameToCheck.endsWith('.m4a') + ? 'M4A' + : nameToCheck.endsWith('.mp3') + ? 'MP3' + : (nameToCheck.endsWith('.opus') || nameToCheck.endsWith('.ogg')) + ? 'Opus' + : null; + if (ext != null) sourceFormats.add(ext); + } + + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus'].where((target) { + return sourceFormats.any((src) { + if (src == target) return false; + final isLosslessTarget = target == 'ALAC' || target == 'FLAC'; + final isLosslessSource = src == 'FLAC' || src == 'M4A'; + if (isLosslessTarget && !isLosslessSource) return false; + return true; + }); + }).toList(); + + if (formats.isEmpty) return; + + String selectedFormat = formats.first; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; + String selectedBitrate = + isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); showModalBottomSheet( context: context, @@ -924,7 +959,6 @@ class _DownloadedAlbumScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 1b662324..42818a12 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -1129,9 +1129,56 @@ class _LocalAlbumScreenState extends ConsumerState { BuildContext context, List allTracks, ) { - String selectedFormat = 'MP3'; - String selectedBitrate = '320k'; - bool isLosslessTarget = false; + final tracksById = {for (final t in allTracks) t.id: t}; + final sourceFormats = {}; + for (final id in _selectedIds) { + final item = tracksById[id]; + if (item == null) continue; + String? ext; + if (item.format != null && item.format!.isNotEmpty) { + final fmt = item.format!.toLowerCase(); + if (fmt == 'flac') { + ext = 'FLAC'; + } else if (fmt == 'm4a') { + ext = 'M4A'; + } else if (fmt == 'mp3') { + ext = 'MP3'; + } else if (fmt == 'opus' || fmt == 'ogg') { + ext = 'Opus'; + } + } + if (ext == null) { + final lower = item.filePath.toLowerCase(); + if (lower.endsWith('.flac')) { + ext = 'FLAC'; + } else if (lower.endsWith('.m4a')) { + ext = 'M4A'; + } else if (lower.endsWith('.mp3')) { + ext = 'MP3'; + } else if (lower.endsWith('.opus') || lower.endsWith('.ogg')) { + ext = 'Opus'; + } + } + if (ext != null) sourceFormats.add(ext); + } + + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus'].where((target) { + return sourceFormats.any((src) { + if (src == target) return false; + final isLosslessTarget = target == 'ALAC' || target == 'FLAC'; + final isLosslessSource = src == 'FLAC' || src == 'M4A'; + if (isLosslessTarget && !isLosslessSource) return false; + return true; + }); + }).toList(); + + if (formats.isEmpty) return; + + String selectedFormat = formats.first; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; + String selectedBitrate = + isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); showModalBottomSheet( context: context, @@ -1143,7 +1190,6 @@ class _LocalAlbumScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 64f9c0e3..69b1aa44 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4755,9 +4755,50 @@ class _QueueTabState extends ConsumerState { BuildContext context, List allItems, ) async { - String selectedFormat = 'MP3'; - String selectedBitrate = '320k'; - bool isLosslessTarget = false; + final itemsById = {for (final item in allItems) item.id: item}; + final sourceFormats = {}; + for (final id in _selectedIds) { + final item = itemsById[id]; + if (item == null) continue; + String nameToCheck; + if (item.historyItem?.safFileName != null && + item.historyItem!.safFileName!.isNotEmpty) { + nameToCheck = item.historyItem!.safFileName!.toLowerCase(); + } else if (item.localItem?.format != null && + item.localItem!.format!.isNotEmpty) { + nameToCheck = '.${item.localItem!.format!.toLowerCase()}'; + } else { + nameToCheck = item.filePath.toLowerCase(); + } + final ext = nameToCheck.endsWith('.flac') + ? 'FLAC' + : nameToCheck.endsWith('.m4a') + ? 'M4A' + : nameToCheck.endsWith('.mp3') + ? 'MP3' + : (nameToCheck.endsWith('.opus') || nameToCheck.endsWith('.ogg')) + ? 'Opus' + : null; + if (ext != null) sourceFormats.add(ext); + } + + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus'].where((target) { + return sourceFormats.any((src) { + if (src == target) return false; + final isLosslessTarget = target == 'ALAC' || target == 'FLAC'; + final isLosslessSource = src == 'FLAC' || src == 'M4A'; + if (isLosslessTarget && !isLosslessSource) return false; + return true; + }); + }).toList(); + + if (formats.isEmpty) return; + + String selectedFormat = formats.first; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; + String selectedBitrate = + isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); var didStartConversion = false; _hideSelectionOverlay(); @@ -4773,7 +4814,6 @@ class _QueueTabState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index dfb88847..f5608c9f 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1377,12 +1377,7 @@ class FFmpegService { return outputPath; } - /// Convert any audio format to FLAC. - /// Source metadata is preserved via -map_metadata 0 (FFmpeg auto-remaps - /// tag names between container formats), then explicit Vorbis comment - /// overrides are applied from the [metadata] map. - /// Cover art is embedded via a second input stream (same approach as - /// [embedMetadata] and [_convertToAlac]). + /// Convert any audio format to FLAC with metadata and cover art preservation. static Future _convertToFlac({ required String inputPath, required Map metadata, @@ -1394,7 +1389,6 @@ class FFmpegService { final cmdBuffer = StringBuffer(); cmdBuffer.write('-i "$inputPath" '); - // Cover art as second input for attached picture final hasCover = coverPath != null && coverPath.trim().isNotEmpty && await File(coverPath).exists(); @@ -1409,12 +1403,8 @@ class FFmpegService { cmdBuffer.write('-metadata:s:v comment="Cover (front)" '); } cmdBuffer.write('-c:a flac -compression_level 8 '); - - // Copy source metadata as base (FFmpeg auto-remaps M4A/ID3 tags to - // Vorbis comment names), then override with our explicit values. cmdBuffer.write('-map_metadata 0 '); - // Apply normalized Vorbis comment overrides final vorbisComments = _normalizeToVorbisComments(metadata); for (final entry in vorbisComments.entries) { final sanitized = entry.value.replaceAll('"', '\\"'); @@ -1447,8 +1437,8 @@ class FFmpegService { return outputPath; } - /// Normalize metadata keys to standard Vorbis comment names and filter out - /// technical/non-tag fields (bit_depth, sample_rate, duration, etc.). + /// Normalize metadata keys to standard Vorbis comment names, filtering out + /// technical fields (bit_depth, sample_rate, duration, etc.). static Map _normalizeToVorbisComments( Map metadata, ) { @@ -1508,12 +1498,9 @@ class FFmpegService { break; case 'LYRICS': case 'UNSYNCEDLYRICS': - // Write both keys for compatibility with different FLAC readers vorbis['LYRICS'] = value; vorbis['UNSYNCEDLYRICS'] = value; break; - // Technical fields (BIT_DEPTH, SAMPLE_RATE, DURATION, etc.) are - // intentionally dropped — they are not Vorbis comment tags. } } From 280b921755964f1525aa27bf2ab48e39a06b9971 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:43:13 +0700 Subject: [PATCH 103/145] fix: detect embedded lyrics in M4A/ALAC files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add extractLyricsFromM4A() that walks the MP4 box tree (moov/udta/meta/ilst/©lyr) to read lyrics. Wire it into ExtractLyrics so the Embed Lyrics button is hidden when lyrics already exist in the file. --- go_backend/metadata.go | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 391b7e5d..19783cc2 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -552,6 +552,14 @@ func ExtractLyrics(filePath string) (string, error) { return extractLyricsFromSidecarLRC(filePath) } + if strings.HasSuffix(lower, ".m4a") || strings.HasSuffix(lower, ".aac") { + lyrics, err := extractLyricsFromM4A(filePath) + if err == nil && strings.TrimSpace(lyrics) != "" { + return lyrics, nil + } + return extractLyricsFromSidecarLRC(filePath) + } + if strings.HasSuffix(lower, ".mp3") { meta, err := ReadID3Tags(filePath) if err == nil && meta != nil { @@ -581,6 +589,80 @@ func ExtractLyrics(filePath string) (string, error) { return extractLyricsFromSidecarLRC(filePath) } +func extractLyricsFromM4A(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return "", err + } + fileSize := fi.Size() + + moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize) + if err != nil || !found { + return "", fmt.Errorf("moov not found") + } + + bodyStart := moov.offset + moov.headerSize + bodySize := moov.size - moov.headerSize + + udta, found, err := findAtomInRange(f, bodyStart, bodySize, "udta", fileSize) + if err != nil || !found { + return "", fmt.Errorf("udta not found") + } + + bodyStart = udta.offset + udta.headerSize + bodySize = udta.size - udta.headerSize + + meta, found, err := findAtomInRange(f, bodyStart, bodySize, "meta", fileSize) + if err != nil || !found { + return "", fmt.Errorf("meta not found") + } + + // meta atom has 4-byte version/flags after the header + bodyStart = meta.offset + meta.headerSize + 4 + bodySize = meta.size - meta.headerSize - 4 + + ilst, found, err := findAtomInRange(f, bodyStart, bodySize, "ilst", fileSize) + if err != nil || !found { + return "", fmt.Errorf("ilst not found") + } + + bodyStart = ilst.offset + ilst.headerSize + bodySize = ilst.size - ilst.headerSize + + lyr, found, err := findAtomInRange(f, bodyStart, bodySize, "\xa9lyr", fileSize) + if err != nil || !found { + return "", fmt.Errorf("lyrics atom not found") + } + + dataStart := lyr.offset + lyr.headerSize + dataSize := lyr.size - lyr.headerSize + + dataAtom, found, err := findAtomInRange(f, dataStart, dataSize, "data", fileSize) + if err != nil || !found { + return "", fmt.Errorf("data atom not found in lyrics") + } + + // data atom: 8 bytes header + 4 bytes type indicator + 4 bytes locale = skip 8 + textStart := dataAtom.offset + dataAtom.headerSize + 8 + textLen := dataAtom.size - dataAtom.headerSize - 8 + if textLen <= 0 { + return "", fmt.Errorf("empty lyrics") + } + + buf := make([]byte, textLen) + if _, err := f.ReadAt(buf, textStart); err != nil { + return "", err + } + + return string(buf), nil +} + func extractLyricsFromSidecarLRC(filePath string) (string, error) { ext := filepath.Ext(filePath) base := strings.TrimSuffix(filePath, ext) From af6fa6ea53cb01cf885f0c7ee4893c8bf09799d2 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:49:48 +0700 Subject: [PATCH 104/145] fix: extract cover art from M4A/ALAC files for conversion Add extractCoverFromM4A() that reads the covr atom from the MP4 box tree (moov/udta/meta/ilst/covr/data). Wire it into ExtractCoverToFile so ALAC-to-FLAC conversion preserves cover art. --- go_backend/exports.go | 2 ++ go_backend/metadata.go | 73 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/go_backend/exports.go b/go_backend/exports.go index 1c5f84ed..e65b0197 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1694,6 +1694,8 @@ func ExtractCoverToFile(audioPath string, outputPath string) error { if strings.HasSuffix(lower, ".flac") { coverData, err = ExtractCoverArt(audioPath) + } else if strings.HasSuffix(lower, ".m4a") || strings.HasSuffix(lower, ".aac") { + coverData, err = extractCoverFromM4A(audioPath) } else if strings.HasSuffix(lower, ".mp3") { coverData, _, err = extractMP3CoverArt(audioPath) } else if strings.HasSuffix(lower, ".opus") || strings.HasSuffix(lower, ".ogg") { diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 19783cc2..d4d28e37 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -663,6 +663,79 @@ func extractLyricsFromM4A(filePath string) (string, error) { return string(buf), nil } +func extractCoverFromM4A(filePath string) ([]byte, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return nil, err + } + fileSize := fi.Size() + + moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("moov not found") + } + + bodyStart := moov.offset + moov.headerSize + bodySize := moov.size - moov.headerSize + + udta, found, err := findAtomInRange(f, bodyStart, bodySize, "udta", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("udta not found") + } + + bodyStart = udta.offset + udta.headerSize + bodySize = udta.size - udta.headerSize + + meta, found, err := findAtomInRange(f, bodyStart, bodySize, "meta", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("meta not found") + } + + bodyStart = meta.offset + meta.headerSize + 4 + bodySize = meta.size - meta.headerSize - 4 + + ilst, found, err := findAtomInRange(f, bodyStart, bodySize, "ilst", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("ilst not found") + } + + bodyStart = ilst.offset + ilst.headerSize + bodySize = ilst.size - ilst.headerSize + + covr, found, err := findAtomInRange(f, bodyStart, bodySize, "covr", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("cover atom not found") + } + + dataStart := covr.offset + covr.headerSize + dataSize := covr.size - covr.headerSize + + dataAtom, found, err := findAtomInRange(f, dataStart, dataSize, "data", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("data atom not found in cover") + } + + // data atom: header + 4 bytes type indicator + 4 bytes locale + imgStart := dataAtom.offset + dataAtom.headerSize + 8 + imgLen := dataAtom.size - dataAtom.headerSize - 8 + if imgLen <= 0 { + return nil, fmt.Errorf("empty cover data") + } + + buf := make([]byte, imgLen) + if _, err := f.ReadAt(buf, imgStart); err != nil { + return nil, err + } + + return buf, nil +} + func extractLyricsFromSidecarLRC(filePath string) (string, error) { ext := filepath.Ext(filePath) base := strings.TrimSuffix(filePath, ext) From 09eb6cf20613dc0aaa1421ef54de86ced12f78a2 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 03:28:27 +0700 Subject: [PATCH 105/145] fix: use album-level artist for Various Artists albums instead of first track's artist - Extension: fix extractSchemaOrg to find album-level schema (with numTracks) instead of per-track schema - Extension: add secondaryText2 fallback in parseDescriptiveRows for VA album track artists - Extension: use headerPrimaryText as primary album artist source, overriding schema.org - App: album_screen now uses widget.artistName (album-level) instead of tracks.first.artistName - App: home_tab _parseTrack now populates albumArtist from track data or album-level artist - Bump Amazon extension to v2.0.1 --- lib/screens/album_screen.dart | 7 +++++-- lib/screens/home_tab.dart | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index f498d834..139a5720 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -95,7 +95,7 @@ class _AlbumScreenState extends ConsumerState { .recordAlbumAccess( id: widget.albumId, name: widget.albumName, - artistName: widget.tracks?.firstOrNull?.artistName, + artistName: widget.artistName ?? widget.tracks?.firstOrNull?.albumArtist ?? widget.tracks?.firstOrNull?.artistName, imageUrl: widget.coverUrl, providerId: providerId, ); @@ -283,7 +283,10 @@ class _AlbumScreenState extends ConsumerState { ) { final expandedHeight = _calculateExpandedHeight(context); final tracks = _tracks ?? []; - final artistName = tracks.isNotEmpty ? tracks.first.artistName : null; + final artistName = widget.artistName ?? + (tracks.isNotEmpty + ? (tracks.first.albumArtist ?? tracks.first.artistName) + : null); final releaseDate = tracks.isNotEmpty ? tracks.first.releaseDate : null; return SliverAppBar( diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 142c59de..1cffcf2d 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -3909,6 +3909,7 @@ class _ExtensionAlbumScreenState extends ConsumerState { name: (data['name'] ?? '').toString(), artistName: (data['artists'] ?? data['artist'] ?? '').toString(), albumName: (data['album_name'] ?? widget.albumName).toString(), + albumArtist: (data['album_artist'] ?? _artistName)?.toString(), artistId: (data['artist_id'] ?? data['artistId'])?.toString() ?? _artistId, albumId: data['album_id']?.toString() ?? widget.albumId, From 529a920b24746d67168fcbd8665b60eebe155028 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 03:29:45 +0700 Subject: [PATCH 106/145] bump version to 3.8.5+111 --- lib/constants/app_info.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 03766dff..88a98c6e 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -3,8 +3,8 @@ import 'package:flutter/foundation.dart'; /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.8.0'; - static const String buildNumber = '106'; + static const String version = '3.8.5'; + static const String buildNumber = '111'; static const String fullVersion = '$version+$buildNumber'; /// Shows "Internal" in debug builds, actual version in release. diff --git a/pubspec.yaml b/pubspec.yaml index 1aa0fcf1..c54be740 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Deezer publish_to: "none" -version: 3.8.0+106 +version: 3.8.5+111 environment: sdk: ^3.10.0 From 5b5f043624a7139ff085f2774aece2455c128538 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 03:36:42 +0700 Subject: [PATCH 107/145] docs: add extension store URL setup guide to README --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 21feb955..f6d99306 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,15 @@ Extensions allow the community to add new music sources and features without wai ### Installing Extensions 1. Go to **Store** tab in the app -2. Browse and install extensions with one tap -3. Or download a `.spotiflac-ext` file and install manually via **Settings > Extensions** -4. Configure extension settings if needed -5. Set provider priority in **Settings > Extensions > Provider Priority** +2. When opening the Store for the first time, you will be asked to enter an **Extension Repository URL**. You can use this community repository: + ``` + https://github.com/zarzet/SpotiFLAC-Extension + ``` + > **Why is this needed?** SpotiFLAC uses a decentralized extension system — extensions are hosted on GitHub repositories rather than a built-in server. This allows anyone to create and host their own extension repository. You only need to enter this URL once; the app will remember it. +3. Browse and install extensions with one tap +4. Or download a `.spotiflac-ext` file and install manually via **Settings > Extensions** +5. Configure extension settings if needed +6. Set provider priority in **Settings > Extensions > Provider Priority** ### Developing Extensions Want to create your own extension? Check out the [Extension Development Guide](https://zarzet.github.io/SpotiFLAC-Mobile/docs) for complete documentation. @@ -55,6 +60,11 @@ Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music for Window ## FAQ +**Q: Why does the Store tab ask me to enter a URL?** +A: Starting from version 3.8.0, SpotiFLAC uses a decentralized extension system. Extensions (like Amazon Music, YouTube Music, etc.) are hosted on GitHub repositories instead of a built-in server. On first launch, enter an extension repository URL in the Store tab, for example: +`https://github.com/zarzet/SpotiFLAC-Extension` +You only need to do this once — the app will save it and load extensions automatically on future launches. + **Q: Why is my download failing with "Song not found"?** A: The track may not be available on the streaming services. Try enabling more download services in Settings > Download > Provider Priority, or install additional extensions like Amazon Music from the Store. From 15f977d98d7046e63970a0df4fcd5b3526872b87 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 03:48:40 +0700 Subject: [PATCH 108/145] fix: skip already-downloaded tracks in Download All for albums and playlists Album and playlist Download All buttons now check download history and local library before enqueuing, matching the existing behavior in artist discography and CSV import. Tracks already in library are skipped with a summary snackbar. --- lib/screens/album_screen.dart | 67 +++++++++++++++++++++++++------- lib/screens/playlist_screen.dart | 67 +++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 30 deletions(-) diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index 139a5720..9b8da6a4 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -574,37 +574,74 @@ class _AlbumScreenState extends ConsumerState { void _downloadAll(BuildContext context) { final tracks = _tracks; if (tracks == null || tracks.isEmpty) return; + + // Filter out tracks already in download history or local library + final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); + final localLibState = (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) + ? ref.read(localLibraryProvider) + : null; + final tracksToQueue = []; + int skippedCount = 0; + + for (final track in tracks) { + final isInHistory = historyState.isDownloaded(track.id) || + (track.isrc != null && historyState.getByIsrc(track.isrc!) != null) || + historyState.findByTrackAndArtist(track.name, track.artistName) != null; + final isInLocal = localLibState?.existsInLibrary( + isrc: track.isrc, + trackName: track.name, + artistName: track.artistName, + ) ?? + false; + + if (isInHistory || isInLocal) { + skippedCount++; + } else { + tracksToQueue.add(track); + } + } + + if (tracksToQueue.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.discographySkippedDownloaded(0, skippedCount), + ), + ), + ); + return; + } + if (settings.askQualityBeforeDownload) { DownloadServicePicker.show( context, - trackName: '${tracks.length} tracks', + trackName: '${tracksToQueue.length} tracks', artistName: widget.albumName, onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarAddedTracksToQueue(tracks.length), - ), - ), - ); + .addMultipleToQueue(tracksToQueue, service, qualityOverride: quality); + _showQueuedSnackbar(context, tracksToQueue.length, skippedCount); }, ); } else { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), - ), - ); + .addMultipleToQueue(tracksToQueue, settings.defaultService); + _showQueuedSnackbar(context, tracksToQueue.length, skippedCount); } } + void _showQueuedSnackbar(BuildContext context, int added, int skipped) { + final message = skipped > 0 + ? context.l10n.discographySkippedDownloaded(added, skipped) + : context.l10n.snackbarAddedTracksToQueue(added); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + Widget _buildLoveAllButton() { final collectionsState = ref.watch(libraryCollectionsProvider); final tracks = _tracks; diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 8a035def..67d377b9 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -608,45 +608,82 @@ class _PlaylistScreenState extends ConsumerState { void _downloadTracks(BuildContext context, List tracks) { if (tracks.isEmpty) return; + + // Filter out tracks already in download history or local library + final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); + final localLibState = (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) + ? ref.read(localLibraryProvider) + : null; + final tracksToQueue = []; + int skippedCount = 0; + + for (final track in tracks) { + final isInHistory = historyState.isDownloaded(track.id) || + (track.isrc != null && historyState.getByIsrc(track.isrc!) != null) || + historyState.findByTrackAndArtist(track.name, track.artistName) != null; + final isInLocal = localLibState?.existsInLibrary( + isrc: track.isrc, + trackName: track.name, + artistName: track.artistName, + ) ?? + false; + + if (isInHistory || isInLocal) { + skippedCount++; + } else { + tracksToQueue.add(track); + } + } + + if (tracksToQueue.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.discographySkippedDownloaded(0, skippedCount), + ), + ), + ); + return; + } + if (settings.askQualityBeforeDownload) { DownloadServicePicker.show( context, - trackName: '${tracks.length} tracks', + trackName: '${tracksToQueue.length} tracks', artistName: _playlistName, onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) .addMultipleToQueue( - tracks, + tracksToQueue, service, qualityOverride: quality, playlistName: _playlistName, ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarAddedTracksToQueue(tracks.length), - ), - ), - ); + _showQueuedSnackbar(context, tracksToQueue.length, skippedCount); }, ); } else { ref .read(downloadQueueProvider.notifier) .addMultipleToQueue( - tracks, + tracksToQueue, settings.defaultService, playlistName: _playlistName, ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), - ), - ); + _showQueuedSnackbar(context, tracksToQueue.length, skippedCount); } } + + void _showQueuedSnackbar(BuildContext context, int added, int skipped) { + final message = skipped > 0 + ? context.l10n.discographySkippedDownloaded(added, skipped) + : context.l10n.snackbarAddedTracksToQueue(added); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } } /// Separate Consumer widget for each track - only rebuilds when this specific track's status changes From 9627ef66cf82d270535413f5b0f4445373879257 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 04:12:14 +0700 Subject: [PATCH 109/145] fix: verify resolved Tidal/Deezer tracks match the download request before downloading SongLink can return incorrect track IDs (e.g. a different track from the same album). Qobuz already had verification via qobuzTrackMatchesRequest. This adds equivalent verification for Tidal and Deezer using a shared trackMatchesRequest() helper in title_match_utils.go that checks artist, title, and duration. Mismatched SongLink/ISRC results are now rejected so the wrong audio is never embedded with Spotify metadata. --- go_backend/deezer_download.go | 53 +++++++++++++++++++++++++++----- go_backend/tidal.go | 26 ++++++++++++++++ go_backend/title_match_utils.go | 42 +++++++++++++++++++++++++ lib/screens/album_screen.dart | 8 ++--- lib/screens/playlist_screen.dart | 3 +- 5 files changed, 117 insertions(+), 15 deletions(-) diff --git a/go_backend/deezer_download.go b/go_backend/deezer_download.go index bef0e2ff..974ffcf6 100644 --- a/go_backend/deezer_download.go +++ b/go_backend/deezer_download.go @@ -203,29 +203,48 @@ func resolveDeezerTrackURL(req DownloadRequest) (string, error) { } } if deezerID != "" { - return fmt.Sprintf("https://www.deezer.com/track/%s", deezerID), nil + trackURL := fmt.Sprintf("https://www.deezer.com/track/%s", deezerID) + if err := verifyDeezerTrack(req, deezerID); err != nil { + GoLog("[Deezer] Direct ID %s verification failed: %v\n", deezerID, err) + // Don't reject direct IDs from request payload — they're presumably correct. + } + return trackURL, nil } - // Try resolving Deezer ID from Spotify ID via SongLink + // Try SongLink spotifyID := strings.TrimSpace(req.SpotifyID) if spotifyID != "" && isLikelySpotifyTrackID(spotifyID) { songlink := NewSongLinkClient() availability, err := songlink.CheckTrackAvailability(spotifyID, "") if err == nil && availability.Deezer && availability.DeezerURL != "" { - return availability.DeezerURL, nil + resolvedID := extractDeezerIDFromURL(availability.DeezerURL) + if resolvedID != "" { + if verifyErr := verifyDeezerTrack(req, resolvedID); verifyErr != nil { + GoLog("[Deezer] SongLink ID %s rejected: %v\n", resolvedID, verifyErr) + // Fall through to ISRC search instead of using wrong track. + } else { + return availability.DeezerURL, nil + } + } else { + return availability.DeezerURL, nil + } } } - // Try resolving from ISRC + // Try ISRC isrc := strings.TrimSpace(req.ISRC) if isrc != "" { ctx, cancel := context.WithTimeout(context.Background(), SongLinkTimeout) defer cancel() track, err := GetDeezerClient().SearchByISRC(ctx, isrc) if err == nil && track != nil { - deezerID = songLinkExtractDeezerTrackID(track) - if deezerID != "" { - return fmt.Sprintf("https://www.deezer.com/track/%s", deezerID), nil + resolvedID := songLinkExtractDeezerTrackID(track) + if resolvedID != "" { + if verifyErr := verifyDeezerTrack(req, resolvedID); verifyErr != nil { + GoLog("[Deezer] ISRC-resolved ID %s rejected: %v\n", resolvedID, verifyErr) + return "", fmt.Errorf("deezer track resolved via ISRC does not match: %w", verifyErr) + } + return fmt.Sprintf("https://www.deezer.com/track/%s", resolvedID), nil } } } @@ -233,6 +252,26 @@ func resolveDeezerTrackURL(req DownloadRequest) (string, error) { return "", fmt.Errorf("could not resolve Deezer track URL") } +func verifyDeezerTrack(req DownloadRequest, deezerID string) error { + ctx, cancel := context.WithTimeout(context.Background(), SongLinkTimeout) + defer cancel() + trackResp, err := GetDeezerClient().GetTrack(ctx, deezerID) + if err != nil { + return nil // Can't verify — don't block the download. + } + resolved := resolvedTrackInfo{ + Title: trackResp.Track.Name, + ArtistName: trackResp.Track.Artists, + Duration: trackResp.Track.DurationMS / 1000, + } + if !trackMatchesRequest(req, resolved, "Deezer") { + return fmt.Errorf("expected '%s - %s', got '%s - %s'", + req.ArtistName, req.TrackName, resolved.ArtistName, resolved.Title) + } + GoLog("[Deezer] Track %s verified: '%s - %s' ✓\n", deezerID, resolved.ArtistName, resolved.Title) + return nil +} + type deezerMusicDLRequest struct { Platform string `json:"platform"` URL string `json:"url"` diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 582e4ff6..15f17b1c 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1911,6 +1911,32 @@ func resolveTidalTrackForRequest(req DownloadRequest, downloader *TidalDownloade return nil, fmt.Errorf("failed to find tidal track id from request/cache/songlink") } + // Verify the resolved track matches the request. + actualTrack, fetchErr := downloader.getPublicTrack(strconv.FormatInt(trackID, 10)) + if fetchErr != nil { + GoLog("[%s] Warning: could not fetch Tidal track %d for verification: %v\n", logPrefix, trackID, fetchErr) + // Continue without verification — better than failing entirely. + } else { + providerArtist := actualTrack.Artist.Name + if providerArtist == "" && len(actualTrack.Artists) > 0 { + providerArtist = actualTrack.Artists[0].Name + } + resolved := resolvedTrackInfo{ + Title: actualTrack.Title, + ArtistName: providerArtist, + Duration: actualTrack.Duration, + } + if !trackMatchesRequest(req, resolved, logPrefix) { + // Invalidate the cached ID so future requests don't reuse it. + if req.ISRC != "" { + GetTrackIDCache().SetTidal(req.ISRC, 0) + } + return nil, fmt.Errorf("tidal track %d does not match request: expected '%s - %s', got '%s - %s'", + trackID, req.ArtistName, req.TrackName, resolved.ArtistName, resolved.Title) + } + GoLog("[%s] Track %d verified: '%s - %s' ✓\n", logPrefix, trackID, resolved.ArtistName, resolved.Title) + } + track := &TidalTrack{ ID: trackID, Title: strings.TrimSpace(req.TrackName), diff --git a/go_backend/title_match_utils.go b/go_backend/title_match_utils.go index 039ff434..22b0eebd 100644 --- a/go_backend/title_match_utils.go +++ b/go_backend/title_match_utils.go @@ -68,3 +68,45 @@ func normalizeSymbolOnlyTitle(title string) string { return b.String() } + +// ==================== Shared Track Verification ==================== + +// resolvedTrackInfo holds the metadata fetched from a provider for verification. +type resolvedTrackInfo struct { + Title string + ArtistName string + Duration int // seconds +} + +// trackMatchesRequest checks whether a resolved track from a provider matches +// the original download request. Returns true if the track is a plausible match. +func trackMatchesRequest(req DownloadRequest, resolved resolvedTrackInfo, logPrefix string) bool { + if req.ArtistName != "" && resolved.ArtistName != "" && + !artistsMatch(req.ArtistName, resolved.ArtistName) { + GoLog("[%s] Verification failed: artist mismatch — expected '%s', got '%s'\n", + logPrefix, req.ArtistName, resolved.ArtistName) + return false + } + + if req.TrackName != "" && resolved.Title != "" && + !titlesMatch(req.TrackName, resolved.Title) { + GoLog("[%s] Verification failed: title mismatch — expected '%s', got '%s'\n", + logPrefix, req.TrackName, resolved.Title) + return false + } + + expectedDurationSec := req.DurationMS / 1000 + if expectedDurationSec > 0 && resolved.Duration > 0 { + diff := expectedDurationSec - resolved.Duration + if diff < 0 { + diff = -diff + } + if diff > 10 { + GoLog("[%s] Verification failed: duration mismatch — expected %ds, got %ds\n", + logPrefix, expectedDurationSec, resolved.Duration) + return false + } + } + + return true +} diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index 9b8da6a4..b54fb92a 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -81,7 +81,6 @@ class _AlbumScreenState extends ConsumerState { _scrollController.addListener(_onScroll); WidgetsBinding.instance.addPostFrameCallback((_) { - // Use extensionId if available, otherwise detect from albumId prefix final providerId = widget.extensionId ?? (() { @@ -134,9 +133,7 @@ class _AlbumScreenState extends ConsumerState { return (mediaSize.height * 0.55).clamp(360.0, 520.0); } - /// Upgrade cover URL to a reasonable resolution for full-screen display. - /// Spotify CDN only has 300, 640, ~2000 — we stay at 640 (no intermediate). - /// Deezer CDN: upgrade to 1000x1000 (available: 56, 250, 500, 1000, 1400, 1800). + /// Upgrade cover URL to a higher resolution for full-screen display. String? _highResCoverUrl(String? url) { if (url == null) return null; // Spotify CDN: upgrade 300 → 640 only (no intermediate between 640 and 2000) @@ -519,7 +516,6 @@ class _AlbumScreenState extends ConsumerState { } Widget _buildInfoCard(BuildContext context, ColorScheme colorScheme) { - // Info is now displayed in the full-screen cover overlay return const SliverToBoxAdapter(child: SizedBox.shrink()); } @@ -575,7 +571,7 @@ class _AlbumScreenState extends ConsumerState { final tracks = _tracks; if (tracks == null || tracks.isEmpty) return; - // Filter out tracks already in download history or local library + // Skip already-downloaded tracks final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); final localLibState = (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 67d377b9..c2d9bff3 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -350,7 +350,6 @@ class _PlaylistScreenState extends ConsumerState { } Widget _buildInfoCard(BuildContext context, ColorScheme colorScheme) { - // Info is now displayed in the full-screen cover overlay return const SliverToBoxAdapter(child: SizedBox.shrink()); } @@ -609,7 +608,7 @@ class _PlaylistScreenState extends ConsumerState { void _downloadTracks(BuildContext context, List tracks) { if (tracks.isEmpty) return; - // Filter out tracks already in download history or local library + // Skip already-downloaded tracks final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); final localLibState = (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) From c7194064256e7380230b469f6a89a5f4b8f41040 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 04:24:18 +0700 Subject: [PATCH 110/145] docs: update readme --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f6d99306..bfffd8ea 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,7 @@ Extensions allow the community to add new music sources and features without wai ### Installing Extensions 1. Go to **Store** tab in the app -2. When opening the Store for the first time, you will be asked to enter an **Extension Repository URL**. You can use this community repository: - ``` - https://github.com/zarzet/SpotiFLAC-Extension - ``` - > **Why is this needed?** SpotiFLAC uses a decentralized extension system — extensions are hosted on GitHub repositories rather than a built-in server. This allows anyone to create and host their own extension repository. You only need to enter this URL once; the app will remember it. +2. When opening the Store for the first time, you will be asked to enter an **Extension Repository URL** 3. Browse and install extensions with one tap 4. Or download a `.spotiflac-ext` file and install manually via **Settings > Extensions** 5. Configure extension settings if needed @@ -61,9 +57,7 @@ Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music for Window ## FAQ **Q: Why does the Store tab ask me to enter a URL?** -A: Starting from version 3.8.0, SpotiFLAC uses a decentralized extension system. Extensions (like Amazon Music, YouTube Music, etc.) are hosted on GitHub repositories instead of a built-in server. On first launch, enter an extension repository URL in the Store tab, for example: -`https://github.com/zarzet/SpotiFLAC-Extension` -You only need to do this once — the app will save it and load extensions automatically on future launches. +A: Starting from version 3.8.0, SpotiFLAC uses a decentralized extension repository system — extensions are hosted on GitHub repositories rather than a built-in server, so anyone can create and host their own. Enter a repository URL in the Store tab to browse and install extensions. **Q: Why is my download failing with "Song not found"?** A: The track may not be available on the streaming services. Try enabling more download services in Settings > Download > Provider Priority, or install additional extensions like Amazon Music from the Store. From 95e755e54ea3bb96487ecc0e885ff8ca9c6f6a76 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:17:00 +0700 Subject: [PATCH 111/145] fix: delay iOS folder picker after sheet dismiss and update Afkar hosts --- CHANGELOG.md | 4 ++-- go_backend/qobuz.go | 2 +- go_backend/spotfetch_api.go | 2 +- .../settings/download_settings_page.dart | 21 ++++++++++++++++++- lib/screens/setup_screen.dart | 21 ++++++++++++++++++- 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8b7254..9916536f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -334,7 +334,7 @@ Thank you for your understanding and continued support. This decision was made t - Routing priority: YouTube service -> extension fallback -> built-in fallback -> direct service - New Android method channel handler: `"downloadByStrategy"` -> `Gobackend.downloadByStrategy(...)` - SpotFetch metadata fallback integration for Spotify-blocked regions - - New backend client for `spotify.afkarxyz.fun/api` + - New backend client for `sp.afkarxyz.qzz.io/api` - Automatic fallback in Spotify metadata fetch path when primary source fails - Lyrics extraction now supports MP3 (ID3v2) and Opus/OGG (Vorbis comments) in addition to FLAC - Includes heuristic detection of lyrics stored in Comment fields @@ -349,7 +349,7 @@ Thank you for your understanding and continued support. This decision was made t - Legacy Dart bridge methods (`downloadTrack`, `downloadWithFallback`, `downloadWithExtensions`, `downloadFromYouTube`) are now thin wrappers and marked `@Deprecated` - Qobuz downloader updated to latest Jumo API contract (`/get` endpoint, required headers) - Amazon download flow now returns `decryption_key` from Go and performs decryption in Flutter (local file + SAF paths) -- Amazon now uses the new `amazon.afkarxyz.fun` API flow (ASIN-based track endpoint + legacy fallback) with encrypted stream support +- Amazon now uses the new `amzn.afkarxyz.qzz.io` API flow (ASIN-based track endpoint + legacy fallback) with encrypted stream support - Amazon ASIN extraction rewritten with robust URL/query-param parsing and regex fallback - Amazon provider re-enabled in download service picker and download settings (alongside Tidal, Qobuz, and YouTube picker flow) - Track Metadata cover UI now refreshes from the embedded file after Edit Metadata/Re-enrich, so the displayed art matches actual file tags diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index ec0489ff..e354f479 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -54,7 +54,7 @@ const ( qobuzDownloadAPIURL = "https://www.musicdl.me/api/qobuz/download" qobuzDabMusicAPIURL = "https://dabmusic.xyz/api/stream?trackId=" qobuzDeebAPIURL = "https://dab.yeet.su/api/stream?trackId=" - qobuzAfkarAPIURL = "https://qbz.afkarxyz.fun/api/track/" + qobuzAfkarAPIURL = "https://qbz.afkarxyz.qzz.io/api/track/" qobuzSquidAPIURL = "https://qobuz.squid.wtf/api/download-music?country=US&track_id=" qobuzDebugKeyXORMask = byte(0x5A) ) diff --git a/go_backend/spotfetch_api.go b/go_backend/spotfetch_api.go index d6bfab19..e12804a4 100644 --- a/go_backend/spotfetch_api.go +++ b/go_backend/spotfetch_api.go @@ -10,7 +10,7 @@ import ( "time" ) -const DefaultSpotFetchAPIBaseURL = "https://spotify.afkarxyz.fun/api" +const DefaultSpotFetchAPIBaseURL = "https://sp.afkarxyz.qzz.io/api" // GetSpotifyDataWithAPI fetches Spotify metadata through SpotFetch-compatible API. // This is used as a fallback when direct Spotify API access is blocked/limited. diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index b4a6d1a7..fb583df7 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -1310,8 +1310,27 @@ class _DownloadSettingsPageState extends ConsumerState { subtitle: Text(context.l10n.setupChooseFromFilesSubtitle), onTap: () async { Navigator.pop(ctx); + if (Platform.isIOS) { + await Future.delayed(const Duration(milliseconds: 250)); + } + // Note: iOS requires folder to have at least one file to be selectable - final result = await FilePicker.platform.getDirectoryPath(); + String? result; + try { + result = await FilePicker.platform.getDirectoryPath(); + } catch (e) { + if (ctx.mounted) { + ScaffoldMessenger.of(ctx).showSnackBar( + SnackBar( + content: Text('Failed to open folder picker: $e'), + backgroundColor: Theme.of(ctx).colorScheme.error, + duration: const Duration(seconds: 4), + ), + ); + } + return; + } + if (result != null) { // iOS: Validate the selected path is writable (not iCloud or container root) if (Platform.isIOS) { diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index 7c74d92e..6d12825d 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -321,7 +321,26 @@ class _SetupScreenState extends ConsumerState { title: Text(context.l10n.setupChooseFromFiles), onTap: () async { Navigator.pop(ctx); - final result = await FilePicker.platform.getDirectoryPath(); + if (Platform.isIOS) { + await Future.delayed(const Duration(milliseconds: 250)); + } + + String? result; + try { + result = await FilePicker.platform.getDirectoryPath(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to open folder picker: $e'), + backgroundColor: Theme.of(context).colorScheme.error, + duration: const Duration(seconds: 4), + ), + ); + } + return; + } + if (result != null) { // iOS: Validate the selected path is writable if (Platform.isIOS) { From e63e3662289ed61d9518031ad5538757ae4910bb Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:28:25 +0700 Subject: [PATCH 112/145] feat: add mc nuggets jimmy, CJBGR and michahRicie as supporters Add new supporters to the donate page. michahRicie is highlighted as a gold supporter. --- lib/screens/settings/donate_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index f822f5d3..e1f76a6d 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -164,7 +164,7 @@ class _RecentDonorsCard extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - const donorNames = ['a fan']; + const donorNames = ['a fan', 'mc nuggets jimmy', 'CJBGR', 'michahRicie']; // Match SettingsGroup color logic final cardColor = isDark @@ -479,8 +479,8 @@ int _cr(String v) { return r; } -// Highlighted supporters (hashes of names): none for now. -const _cv = {}; +// Highlighted supporters (hashes of names). +const _cv = {1365043105}; class _SupporterChip extends StatelessWidget { final String name; From f170ead7b9aeb99c5c2678721eb401db52010134 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:28:31 +0700 Subject: [PATCH 113/145] docs: add contributors section to README Add auto-generated contributor avatars via contrib.rocks with a link to the GitHub contributors page. Include acknowledgement for translators and bug reporters. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index bfffd8ea..4ad91b55 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,18 @@ _If this software is useful and brings you value, consider supporting the projec [![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/zarzet) +## Contributors + +Thanks to all the amazing people who have contributed to SpotiFLAC Mobile! + + + + + +We also appreciate everyone who has helped with [translations on Crowdin](https://crowdin.com/project/spotiflac-mobile), reported bugs, suggested features, and spread the word about SpotiFLAC Mobile. + +Interested in contributing? Check out our [Contributing Guide](CONTRIBUTING.md) to get started! + ## API Credits [hifi-api](https://github.com/binimum/hifi-api) · [music.binimum.org](https://music.binimum.org) · [qqdl.site](https://qqdl.site) · [squid.wtf](https://squid.wtf) · [spotisaver.net](https://spotisaver.net) · [dabmusic.xyz](https://dabmusic.xyz) · [AfkarXYZ](https://github.com/afkarxyz) · [LRCLib](https://lrclib.net) · [Paxsenix](https://lyrics.paxsenix.org) · [Cobalt](https://cobalt.tools) · [qwkuns.me](https://qwkuns.me) · [SpotubeDL](https://spotubedl.com) · [Song.link](https://song.link) · [IDHS](https://github.com/sjdonado/idonthavespotify) From 929c5f324969ef45f9077b8c42e9bfb8a77be041 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:28:37 +0700 Subject: [PATCH 114/145] fix: remove double horizontal padding in store tab extension list The extension list was wrapped in an extra Padding(horizontal: 16) on top of SettingsGroup's default 16px margin, resulting in 32px total inset. Remove the outer wrapper to match settings tab width. --- lib/screens/store_tab.dart | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index 72c91444..63256832 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -269,22 +269,19 @@ class _StoreTabState extends ConsumerState { ), SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SettingsGroup( - children: filteredExtensions.asMap().entries.map((entry) { - final index = entry.key; - final ext = entry.value; - return _ExtensionItem( - extension: ext, - showDivider: index < filteredExtensions.length - 1, - isDownloading: downloadingId == ext.id, - onInstall: () => _installExtension(ext), - onUpdate: () => _updateExtension(ext), - onTap: () => _showExtensionDetails(ext), - ); - }).toList(), - ), + child: SettingsGroup( + children: filteredExtensions.asMap().entries.map((entry) { + final index = entry.key; + final ext = entry.value; + return _ExtensionItem( + extension: ext, + showDivider: index < filteredExtensions.length - 1, + isDownloading: downloadingId == ext.id, + onInstall: () => _installExtension(ext), + onUpdate: () => _updateExtension(ext), + onTap: () => _showExtensionDetails(ext), + ); + }).toList(), ), ), From 6710f90e1e7736b4530f6cc510269d7fcea14cfa Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:28:45 +0700 Subject: [PATCH 115/145] feat: add auto-scan option for local library Add a new 'Auto Scan' setting under Local Library with four modes: off, every app open (10min cooldown), daily, and weekly. The app uses WidgetsBindingObserver to trigger incremental scans on launch and when resuming from background, respecting the configured cooldown based on the last scan timestamp. --- lib/l10n/app_localizations.dart | 36 +++++ lib/l10n/app_localizations_de.dart | 19 +++ lib/l10n/app_localizations_en.dart | 19 +++ lib/l10n/app_localizations_es.dart | 19 +++ lib/l10n/app_localizations_fr.dart | 19 +++ lib/l10n/app_localizations_hi.dart | 19 +++ lib/l10n/app_localizations_id.dart | 19 +++ lib/l10n/app_localizations_ja.dart | 19 +++ lib/l10n/app_localizations_ko.dart | 19 +++ lib/l10n/app_localizations_nl.dart | 19 +++ lib/l10n/app_localizations_pt.dart | 19 +++ lib/l10n/app_localizations_ru.dart | 19 +++ lib/l10n/app_localizations_tr.dart | 19 +++ lib/l10n/app_localizations_zh.dart | 19 +++ lib/l10n/arb/app_en.arb | 24 ++++ lib/main.dart | 75 +++++++++- lib/models/settings.dart | 6 + lib/models/settings.g.dart | 2 + lib/providers/settings_provider.dart | 5 + .../settings/library_settings_page.dart | 134 +++++++++++++++++- 20 files changed, 526 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 15451886..0ea69f2b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3106,6 +3106,42 @@ abstract class AppLocalizations { /// **'Show when searching for existing tracks'** String get libraryShowDuplicateIndicatorSubtitle; + /// Setting for automatic library scanning + /// + /// In en, this message translates to: + /// **'Auto Scan'** + String get libraryAutoScan; + + /// Subtitle for auto scan setting + /// + /// In en, this message translates to: + /// **'Automatically scan your library for new files'** + String get libraryAutoScanSubtitle; + + /// Auto scan disabled + /// + /// In en, this message translates to: + /// **'Off'** + String get libraryAutoScanOff; + + /// Auto scan when app opens + /// + /// In en, this message translates to: + /// **'Every app open'** + String get libraryAutoScanOnOpen; + + /// Auto scan once per day + /// + /// In en, this message translates to: + /// **'Daily'** + String get libraryAutoScanDaily; + + /// Auto scan once per week + /// + /// In en, this message translates to: + /// **'Weekly'** + String get libraryAutoScanWeekly; + /// Section header for library actions /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f0137de1..7ad45e21 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1719,6 +1719,25 @@ class AppLocalizationsDe extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Bei der Suche nach vorhandenen Titeln anzeigen'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Aktionen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 041c5bd1..13208d5b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsEn extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 99849885..113c08f1 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsEs extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index cec6eae9..6521f755 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1697,6 +1697,25 @@ class AppLocalizationsFr extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 9a9ad518..04227142 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsHi extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 902d49cd..b23768e3 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1702,6 +1702,25 @@ class AppLocalizationsId extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 31270670..caf7d16e 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1682,6 +1682,25 @@ class AppLocalizationsJa extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'アクション'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 76f1b737..1cac05e7 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1675,6 +1675,25 @@ class AppLocalizationsKo extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index d509e1e7..ac2c8b57 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsNl extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ef87eca7..3c5c8896 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsPt extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 8bbb8a74..31a1a1dc 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1731,6 +1731,25 @@ class AppLocalizationsRu extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Показать при поиске существующих треков'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Действия'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index bbf6ef14..518bcf77 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1707,6 +1707,25 @@ class AppLocalizationsTr extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 465f196c..d36cf154 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1695,6 +1695,25 @@ class AppLocalizationsZh extends AppLocalizations { String get libraryShowDuplicateIndicatorSubtitle => 'Show when searching for existing tracks'; + @override + String get libraryAutoScan => 'Auto Scan'; + + @override + String get libraryAutoScanSubtitle => + 'Automatically scan your library for new files'; + + @override + String get libraryAutoScanOff => 'Off'; + + @override + String get libraryAutoScanOnOpen => 'Every app open'; + + @override + String get libraryAutoScanDaily => 'Daily'; + + @override + String get libraryAutoScanWeekly => 'Weekly'; + @override String get libraryActions => 'Actions'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7f4e1acb..7055840d 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2242,6 +2242,30 @@ "@libraryShowDuplicateIndicatorSubtitle": { "description": "Subtitle for duplicate indicator toggle" }, + "libraryAutoScan": "Auto Scan", + "@libraryAutoScan": { + "description": "Setting for automatic library scanning" + }, + "libraryAutoScanSubtitle": "Automatically scan your library for new files", + "@libraryAutoScanSubtitle": { + "description": "Subtitle for auto scan setting" + }, + "libraryAutoScanOff": "Off", + "@libraryAutoScanOff": { + "description": "Auto scan disabled" + }, + "libraryAutoScanOnOpen": "Every app open", + "@libraryAutoScanOnOpen": { + "description": "Auto scan when app opens" + }, + "libraryAutoScanDaily": "Daily", + "@libraryAutoScanDaily": { + "description": "Auto scan once per day" + }, + "libraryAutoScanWeekly": "Weekly", + "@libraryAutoScanWeekly": { + "description": "Auto scan once per week" + }, "libraryActions": "Actions", "@libraryActions": { "description": "Section header for library actions" diff --git a/lib/main.dart b/lib/main.dart index 63d74d3a..7ead4a81 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotiflac_android/app.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; @@ -90,16 +91,21 @@ class _EagerInitialization extends ConsumerStatefulWidget { _EagerInitializationState(); } -class _EagerInitializationState extends ConsumerState<_EagerInitialization> { +class _EagerInitializationState extends ConsumerState<_EagerInitialization> + with WidgetsBindingObserver { ProviderSubscription? _localLibraryEnabledSub; Timer? _downloadHistoryWarmupTimer; Timer? _libraryCollectionsWarmupTimer; Timer? _localLibraryWarmupTimer; bool _localLibraryWarmupScheduled = false; + bool _autoScanTriggeredOnLaunch = false; + + static const _lastScannedAtKey = 'local_library_last_scanned_at'; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _initializeAppServices(); @@ -110,6 +116,7 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _localLibraryEnabledSub?.close(); _downloadHistoryWarmupTimer?.cancel(); _libraryCollectionsWarmupTimer?.cancel(); @@ -117,6 +124,13 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _maybeAutoScanLocalLibrary(); + } + } + void _initializeDeferredProviders() { _downloadHistoryWarmupTimer = _scheduleProviderWarmup( const Duration(milliseconds: 400), @@ -155,7 +169,64 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { _localLibraryWarmupScheduled = true; _localLibraryWarmupTimer = _scheduleProviderWarmup( const Duration(milliseconds: 1600), - () => ref.read(localLibraryProvider), + () { + ref.read(localLibraryProvider); + // Trigger auto-scan after initial warmup on first app launch. + if (!_autoScanTriggeredOnLaunch) { + _autoScanTriggeredOnLaunch = true; + // Give the provider a moment to load existing data before scanning. + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) _maybeAutoScanLocalLibrary(); + }); + } + }, + ); + } + + /// Checks whether an automatic incremental scan should be triggered based on + /// the user's auto-scan preference and the time since the last scan. + Future _maybeAutoScanLocalLibrary() async { + if (!mounted) return; + + final settings = ref.read(settingsProvider); + if (!settings.localLibraryEnabled) return; + if (settings.localLibraryPath.isEmpty) return; + if (settings.localLibraryAutoScan == 'off') return; + + // Don't start a scan if one is already running. + final libraryState = ref.read(localLibraryProvider); + if (libraryState.isScanning) return; + + // Determine cooldown based on auto-scan mode. + final now = DateTime.now(); + final prefs = await SharedPreferences.getInstance(); + final lastScannedMs = prefs.getInt(_lastScannedAtKey); + + if (lastScannedMs != null) { + final lastScanned = DateTime.fromMillisecondsSinceEpoch(lastScannedMs); + final elapsed = now.difference(lastScanned); + + switch (settings.localLibraryAutoScan) { + case 'on_open': + // Cooldown of 10 minutes to prevent rapid re-scans. + if (elapsed.inMinutes < 10) return; + break; + case 'daily': + if (elapsed.inHours < 24) return; + break; + case 'weekly': + if (elapsed.inDays < 7) return; + break; + default: + return; + } + } + + // All checks passed -- start an incremental scan. + final iosBookmark = settings.localLibraryBookmark; + ref.read(localLibraryProvider.notifier).startScan( + settings.localLibraryPath, + iosBookmark: iosBookmark.isNotEmpty ? iosBookmark : null, ); } diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 9bf2ee10..87c31e1b 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -59,6 +59,8 @@ class AppSettings { localLibraryBookmark; // Base64-encoded iOS security-scoped bookmark final bool localLibraryShowDuplicates; // Show indicator when searching for existing tracks + final String + localLibraryAutoScan; // Auto-scan mode: 'off', 'on_open', 'daily', 'weekly' final bool hasCompletedTutorial; // Track if user has completed the app tutorial @@ -123,6 +125,7 @@ class AppSettings { this.localLibraryPath = '', this.localLibraryBookmark = '', this.localLibraryShowDuplicates = true, + this.localLibraryAutoScan = 'off', this.hasCompletedTutorial = false, this.lyricsProviders = const [ 'lrclib', @@ -186,6 +189,7 @@ class AppSettings { String? localLibraryPath, String? localLibraryBookmark, bool? localLibraryShowDuplicates, + String? localLibraryAutoScan, bool? hasCompletedTutorial, List? lyricsProviders, bool? lyricsIncludeTranslationNetease, @@ -251,6 +255,8 @@ class AppSettings { localLibraryBookmark: localLibraryBookmark ?? this.localLibraryBookmark, localLibraryShowDuplicates: localLibraryShowDuplicates ?? this.localLibraryShowDuplicates, + localLibraryAutoScan: + localLibraryAutoScan ?? this.localLibraryAutoScan, hasCompletedTutorial: hasCompletedTutorial ?? this.hasCompletedTutorial, lyricsProviders: lyricsProviders ?? this.lyricsProviders, lyricsIncludeTranslationNetease: diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 99a5114d..2b6a35cf 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -57,6 +57,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( localLibraryBookmark: json['localLibraryBookmark'] as String? ?? '', localLibraryShowDuplicates: json['localLibraryShowDuplicates'] as bool? ?? true, + localLibraryAutoScan: json['localLibraryAutoScan'] as String? ?? 'off', hasCompletedTutorial: json['hasCompletedTutorial'] as bool? ?? false, lyricsProviders: (json['lyricsProviders'] as List?) @@ -129,6 +130,7 @@ Map _$AppSettingsToJson( 'localLibraryPath': instance.localLibraryPath, 'localLibraryBookmark': instance.localLibraryBookmark, 'localLibraryShowDuplicates': instance.localLibraryShowDuplicates, + 'localLibraryAutoScan': instance.localLibraryAutoScan, 'hasCompletedTutorial': instance.hasCompletedTutorial, 'lyricsProviders': instance.lyricsProviders, 'lyricsIncludeTranslationNetease': instance.lyricsIncludeTranslationNetease, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index e98a70e0..33706c57 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -518,6 +518,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setLocalLibraryAutoScan(String mode) { + state = state.copyWith(localLibraryAutoScan: mode); + _saveSettings(); + } + void setTutorialComplete() { state = state.copyWith(hasCompletedTutorial: true); _saveSettings(); diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index e71c1046..608eb4bf 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -241,6 +241,99 @@ class _LibrarySettingsPageState extends ConsumerState { } } + String _getAutoScanLabel(BuildContext context, String mode) { + switch (mode) { + case 'on_open': + return context.l10n.libraryAutoScanOnOpen; + case 'daily': + return context.l10n.libraryAutoScanDaily; + case 'weekly': + return context.l10n.libraryAutoScanWeekly; + default: + return context.l10n.libraryAutoScanOff; + } + } + + void _showAutoScanPicker(BuildContext context, String current) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + context.l10n.libraryAutoScan, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.libraryAutoScanSubtitle, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + _AutoScanOption( + icon: Icons.block, + title: context.l10n.libraryAutoScanOff, + selected: current == 'off', + colorScheme: colorScheme, + onTap: () { + ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('off'); + Navigator.pop(context); + }, + ), + _AutoScanOption( + icon: Icons.open_in_new, + title: context.l10n.libraryAutoScanOnOpen, + selected: current == 'on_open', + colorScheme: colorScheme, + onTap: () { + ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('on_open'); + Navigator.pop(context); + }, + ), + _AutoScanOption( + icon: Icons.today, + title: context.l10n.libraryAutoScanDaily, + selected: current == 'daily', + colorScheme: colorScheme, + onTap: () { + ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('daily'); + Navigator.pop(context); + }, + ), + _AutoScanOption( + icon: Icons.date_range, + title: context.l10n.libraryAutoScanWeekly, + selected: current == 'weekly', + colorScheme: colorScheme, + onTap: () { + ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('weekly'); + Navigator.pop(context); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { final settings = ref.watch(settingsProvider); @@ -344,7 +437,18 @@ class _LibrarySettingsPageState extends ConsumerState { onChanged: (value) => ref .read(settingsProvider.notifier) .setLocalLibraryShowDuplicates(value), - showDivider: false, + ), + Opacity( + opacity: settings.localLibraryEnabled ? 1.0 : 0.5, + child: SettingsItem( + icon: Icons.autorenew_rounded, + title: context.l10n.libraryAutoScan, + subtitle: _getAutoScanLabel(context, settings.localLibraryAutoScan), + onTap: settings.localLibraryEnabled + ? () => _showAutoScanPicker(context, settings.localLibraryAutoScan) + : null, + showDivider: false, + ), ), ], ), @@ -825,3 +929,31 @@ class _ScanProgressTile extends StatelessWidget { ); } } + +class _AutoScanOption extends StatelessWidget { + final IconData icon; + final String title; + final bool selected; + final ColorScheme colorScheme; + final VoidCallback onTap; + + const _AutoScanOption({ + required this.icon, + required this.title, + required this.selected, + required this.colorScheme, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon), + title: Text(title), + trailing: selected + ? Icon(Icons.check, color: colorScheme.primary) + : null, + onTap: onTap, + ); + } +} From a8a39732250692994e11cfb446b14b4ab5a8b39d Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 20:28:53 +0700 Subject: [PATCH 116/145] fix: prevent re-download of tracks converted to a different format When a file is converted externally (e.g. FLAC to OPUS), the orphan cleanup would delete the history entry because the original path no longer exists. Now it checks for sibling files with other audio extensions and updates the stored path instead of deleting. Also add extension-stripped keys to path_match_keys so that paths differing only by audio extension still match during local library scan exclusion and queue deduplication. --- lib/providers/download_queue_provider.dart | 47 +++++++++++++++++++++- lib/screens/settings/donate_page.dart | 4 +- lib/utils/path_match_keys.dart | 39 ++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index a01b8e88..6f983bf4 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -770,6 +770,37 @@ class DownloadHistoryNotifier extends Notifier { /// Remove history entries where the file no longer exists on disk /// Returns the number of orphaned entries removed + /// Audio file extensions that the app commonly produces or converts between. + static const _audioExtensions = [ + '.flac', + '.m4a', + '.mp3', + '.opus', + '.ogg', + '.wav', + '.aac', + ]; + + /// When the original file is missing, check whether a sibling with a + /// different audio extension exists (e.g. the user converted .flac → .opus). + /// Returns the path of the first match found, or `null` if none exist. + Future _findConvertedSibling(String originalPath) async { + // Strip the current extension to get the base path. + final dotIndex = originalPath.lastIndexOf('.'); + if (dotIndex < 0) return null; + final basePath = originalPath.substring(0, dotIndex); + final originalExt = originalPath.substring(dotIndex).toLowerCase(); + + for (final ext in _audioExtensions) { + if (ext == originalExt) continue; + final candidatePath = '$basePath$ext'; + try { + if (await fileExists(candidatePath)) return candidatePath; + } catch (_) {} + } + return null; + } + Future cleanupOrphanedDownloads() async { _historyLog.i('Starting orphaned downloads cleanup...'); @@ -791,7 +822,21 @@ class DownloadHistoryNotifier extends Notifier { if (filePath == null || filePath.isEmpty) return null; pathById[id] = filePath; try { - return MapEntry(id, await fileExists(filePath)); + if (await fileExists(filePath)) return MapEntry(id, true); + + // Original file missing -- check for a converted sibling. + final sibling = await _findConvertedSibling(filePath); + if (sibling != null) { + _historyLog.i( + 'Found converted sibling for $id: $filePath → $sibling', + ); + // Update the stored path so future checks succeed immediately. + await _db.updateFilePath(id, sibling); + pathById[id] = sibling; + return MapEntry(id, true); + } + + return MapEntry(id, false); } catch (e) { _historyLog.w('Error checking file existence for $id: $e'); return MapEntry(id, false); diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index e1f76a6d..b810f003 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -164,7 +164,7 @@ class _RecentDonorsCard extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - const donorNames = ['a fan', 'mc nuggets jimmy', 'CJBGR', 'michahRicie']; + const donorNames = ['micahRichie', 'a fan', 'mc nuggets jimmy', 'CJBGR']; // Match SettingsGroup color logic final cardColor = isDark @@ -480,7 +480,7 @@ int _cr(String v) { } // Highlighted supporters (hashes of names). -const _cv = {1365043105}; +const _cv = {1211573191}; class _SupporterChip extends StatelessWidget { final String name; diff --git a/lib/utils/path_match_keys.dart b/lib/utils/path_match_keys.dart index ace140dd..0df1c023 100644 --- a/lib/utils/path_match_keys.dart +++ b/lib/utils/path_match_keys.dart @@ -8,6 +8,33 @@ const _androidStoragePathAliases = [ '/mnt/sdcard', ]; +/// Audio file extensions that the app commonly produces or converts between. +/// Used to generate extension-stripped match keys so that a file converted from +/// one format to another (e.g. .flac → .opus) is still recognised as the same +/// track. +const _audioExtensions = [ + '.flac', + '.m4a', + '.mp3', + '.opus', + '.ogg', + '.wav', + '.aac', +]; + +/// Strips a trailing audio extension from [path] if present. +/// Returns the path without extension, or `null` if no known audio extension +/// was found. +String? _stripAudioExtension(String path) { + final lower = path.toLowerCase(); + for (final ext in _audioExtensions) { + if (lower.endsWith(ext)) { + return path.substring(0, path.length - ext.length); + } + } + return null; +} + Set buildPathMatchKeys(String? filePath) { final raw = filePath?.trim() ?? ''; if (raw.isEmpty) return const {}; @@ -79,6 +106,18 @@ Set buildPathMatchKeys(String? filePath) { } addNormalized(cleaned); + + // Add extension-stripped variants so that a file converted from one audio + // format to another (e.g. Song.flac → Song.opus) still matches. + final extensionStrippedKeys = {}; + for (final key in keys) { + final stripped = _stripAudioExtension(key); + if (stripped != null && stripped.isNotEmpty) { + extensionStrippedKeys.add(stripped); + } + } + keys.addAll(extensionStrippedKeys); + return keys; } From 8529985a0ebb7fcfe3927ca119cd30304e8c967e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Mar 2026 13:54:09 +0000 Subject: [PATCH 117/145] chore: update AltStore source to v3.8.6 --- apps.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps.json b/apps.json index 81613e65..fd8aa9e2 100644 --- a/apps.json +++ b/apps.json @@ -7,12 +7,12 @@ "name": "SpotiFLAC", "bundleIdentifier": "com.zarzet.spotiflac", "developerName": "zarzet", - "version": "3.8.5", - "versionDate": "2026-03-15", - "downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v3.8.5/SpotiFLAC-v3.8.5-ios-unsigned.ipa", + "version": "3.8.6", + "versionDate": "2026-03-16", + "downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v3.8.6/SpotiFLAC-v3.8.6-ios-unsigned.ipa", "localizedDescription": "Mobile version of SpotiFLAC written in Flutter. Download Tracks in true FLAC from Tidal, Qobuz, & Amazon Music.", "iconURL": "https://raw.githubusercontent.com/zarzet/SpotiFLAC-Mobile/main/assets/images/logo.png", - "size": 33673615 + "size": 33676960 } ] } From c66d13c9fde31fecc5413dd585da34a9de6cd203 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 21:02:03 +0700 Subject: [PATCH 118/145] bump version to 3.8.6+112 --- lib/constants/app_info.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 88a98c6e..854285d6 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -3,8 +3,8 @@ import 'package:flutter/foundation.dart'; /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.8.5'; - static const String buildNumber = '111'; + static const String version = '3.8.6'; + static const String buildNumber = '112'; static const String fullVersion = '$version+$buildNumber'; /// Shows "Internal" in debug builds, actual version in release. diff --git a/pubspec.yaml b/pubspec.yaml index c54be740..540615c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Deezer publish_to: "none" -version: 3.8.5+111 +version: 3.8.6+112 environment: sdk: ^3.10.0 From 9bebed506b08adcd8d792c01f45d544c303956a9 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 22:35:17 +0700 Subject: [PATCH 119/145] fix: honor local library auto-scan cooldown --- lib/main.dart | 8 +++---- lib/providers/local_library_provider.dart | 13 ++++------ lib/utils/local_library_scan_prefs.dart | 29 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 lib/utils/local_library_scan_prefs.dart diff --git a/lib/main.dart b/lib/main.dart index 7ead4a81..5ac51db6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/services/notification_service.dart'; import 'package:spotiflac_android/services/share_intent_service.dart'; import 'package:spotiflac_android/services/cover_cache_manager.dart'; +import 'package:spotiflac_android/utils/local_library_scan_prefs.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -100,8 +101,6 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> bool _localLibraryWarmupScheduled = false; bool _autoScanTriggeredOnLaunch = false; - static const _lastScannedAtKey = 'local_library_last_scanned_at'; - @override void initState() { super.initState(); @@ -200,10 +199,9 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> // Determine cooldown based on auto-scan mode. final now = DateTime.now(); final prefs = await SharedPreferences.getInstance(); - final lastScannedMs = prefs.getInt(_lastScannedAtKey); + final lastScanned = readLocalLibraryLastScannedAt(prefs); - if (lastScannedMs != null) { - final lastScanned = DateTime.fromMillisecondsSinceEpoch(lastScannedMs); + if (lastScanned != null) { final elapsed = now.difference(lastScanned); switch (settings.localLibraryAutoScan) { diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index 78ac4f6f..02fa69b5 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -9,11 +9,11 @@ import 'package:spotiflac_android/services/library_database.dart'; import 'package:spotiflac_android/services/notification_service.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/logger.dart'; +import 'package:spotiflac_android/utils/local_library_scan_prefs.dart'; import 'package:spotiflac_android/utils/path_match_keys.dart'; final _log = AppLogger('LocalLibrary'); -const _lastScannedAtKey = 'local_library_last_scanned_at'; const _excludedDownloadedCountKey = 'local_library_excluded_downloaded_count'; final _prefs = SharedPreferences.getInstance(); @@ -165,10 +165,7 @@ class LocalLibraryNotifier extends Notifier { var excludedDownloadedCount = 0; try { final prefs = await prefsFuture; - final lastScannedAtStr = prefs.getString(_lastScannedAtKey); - if (lastScannedAtStr != null && lastScannedAtStr.isNotEmpty) { - lastScannedAt = DateTime.tryParse(lastScannedAtStr); - } + lastScannedAt = readLocalLibraryLastScannedAt(prefs); excludedDownloadedCount = prefs.getInt(_excludedDownloadedCountKey) ?? 0; } catch (e) { @@ -336,7 +333,7 @@ class LocalLibraryNotifier extends Notifier { final now = DateTime.now(); try { final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_lastScannedAtKey, now.toIso8601String()); + await writeLocalLibraryLastScannedAt(prefs, now); await prefs.setInt(_excludedDownloadedCountKey, skippedDownloads); _log.d('Saved lastScannedAt: $now'); } catch (e) { @@ -500,7 +497,7 @@ class LocalLibraryNotifier extends Notifier { final now = DateTime.now(); try { final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_lastScannedAtKey, now.toIso8601String()); + await writeLocalLibraryLastScannedAt(prefs, now); await prefs.setInt(_excludedDownloadedCountKey, skippedDownloads); _log.d('Saved lastScannedAt: $now'); } catch (e) { @@ -818,7 +815,7 @@ class LocalLibraryNotifier extends Notifier { try { final prefs = await SharedPreferences.getInstance(); - await prefs.remove(_lastScannedAtKey); + await clearLocalLibraryLastScannedAt(prefs); await prefs.remove(_excludedDownloadedCountKey); } catch (e) { _log.w('Failed to clear lastScannedAt: $e'); diff --git a/lib/utils/local_library_scan_prefs.dart b/lib/utils/local_library_scan_prefs.dart new file mode 100644 index 00000000..1d544f8d --- /dev/null +++ b/lib/utils/local_library_scan_prefs.dart @@ -0,0 +1,29 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +const localLibraryLastScannedAtKey = 'local_library_last_scanned_at'; + +DateTime? readLocalLibraryLastScannedAt(SharedPreferences prefs) { + final lastScannedAtStr = prefs.getString(localLibraryLastScannedAtKey); + if (lastScannedAtStr != null && lastScannedAtStr.isNotEmpty) { + return DateTime.tryParse(lastScannedAtStr); + } + + // Backward compatibility for older builds that may have stored epoch millis. + final lastScannedAtMs = prefs.getInt(localLibraryLastScannedAtKey); + if (lastScannedAtMs != null) { + return DateTime.fromMillisecondsSinceEpoch(lastScannedAtMs); + } + + return null; +} + +Future writeLocalLibraryLastScannedAt( + SharedPreferences prefs, + DateTime value, +) { + return prefs.setString(localLibraryLastScannedAtKey, value.toIso8601String()); +} + +Future clearLocalLibraryLastScannedAt(SharedPreferences prefs) { + return prefs.remove(localLibraryLastScannedAtKey); +} From 6fc9ffeb23a08522bb377f5f8861f096054d8744 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 22:46:45 +0700 Subject: [PATCH 120/145] fix: upgrade Deezer and Tidal cover art to max quality on Dart side The Dart-side _upgradeToMaxQualityCover only handled Spotify CDN URLs, causing Deezer covers to stay at 1000x1000 and Tidal at 1280x1280. Add regex-based Deezer upgrade (1800x1800) and Tidal origin resolution upgrade to match the Go backend logic. Closes #237 --- lib/providers/download_queue_provider.dart | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 6f983bf4..8c2bb5f6 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -2379,7 +2379,11 @@ class DownloadQueueNotifier extends Notifier { } } + /// Deezer CDN cover size pattern: /WxH-0-0-0-0.jpg + static final _deezerSizeRegex = RegExp(r'/(\d+)x(\d+)-\d+-\d+-\d+-\d+\.jpg$'); + String _upgradeToMaxQualityCover(String coverUrl) { + // Spotify CDN upgrade (hash-based size identifiers) const spotifySize300 = 'ab67616d00001e02'; const spotifySize640 = 'ab67616d0000b273'; const spotifySizeMax = 'ab67616d000082c1'; @@ -2388,11 +2392,29 @@ class DownloadQueueNotifier extends Notifier { if (result.contains(spotifySize300)) { result = result.replaceFirst(spotifySize300, spotifySize640); } - if (result.contains(spotifySize640)) { result = result.replaceFirst(spotifySize640, spotifySizeMax); } + // Deezer CDN upgrade (1000x1000 → 1800x1800) + if (result.contains('cdn-images.dzcdn.net')) { + final upgraded = result.replaceFirst( + _deezerSizeRegex, + '/1800x1800-000000-80-0-0.jpg', + ); + if (upgraded != result) { + _log.d('Cover URL upgraded (Deezer): 1800x1800'); + result = upgraded; + } + } + + // Tidal CDN upgrade (1280x1280 → origin) + if (result.contains('resources.tidal.com') && + result.contains('/1280x1280.jpg')) { + result = result.replaceFirst('/1280x1280.jpg', '/origin.jpg'); + _log.d('Cover URL upgraded (Tidal): origin'); + } + return result; } From ac1c7d31c942de65df3164ebf786651b9c147268 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 17 Mar 2026 14:27:12 +0700 Subject: [PATCH 121/145] fix: improve Spotify track availability resolution --- go_backend/httputil.go | 7 +- go_backend/songlink.go | 187 +++++++++++++++++++++++++++--------- go_backend/songlink_test.go | 127 ++++++++++++++++++++++++ 3 files changed, 275 insertions(+), 46 deletions(-) create mode 100644 go_backend/songlink_test.go diff --git a/go_backend/httputil.go b/go_backend/httputil.go index 36433d1e..05e4af8f 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -346,11 +346,12 @@ func calculateNextDelay(currentDelay time.Duration, config RetryConfig) time.Dur return min(nextDelay, config.MaxDelay) } -// Returns 60 seconds as default if header is missing or invalid +// Returns 0 if the header is missing or invalid so callers can keep their +// normal exponential backoff instead of stalling for an arbitrary minute. func getRetryAfterDuration(resp *http.Response) time.Duration { retryAfter := resp.Header.Get("Retry-After") if retryAfter == "" { - return 60 * time.Second + return 0 } if seconds, err := strconv.Atoi(retryAfter); err == nil { @@ -364,7 +365,7 @@ func getRetryAfterDuration(resp *http.Response) time.Duration { } } - return 60 * time.Second + return 0 } func ReadResponseBody(resp *http.Response) ([]byte, error) { diff --git a/go_backend/songlink.go b/go_backend/songlink.go index f38a8edb..2a6515b8 100644 --- a/go_backend/songlink.go +++ b/go_backend/songlink.go @@ -1,6 +1,7 @@ package gobackend import ( + "bytes" "context" "encoding/json" "fmt" @@ -14,6 +15,10 @@ type SongLinkClient struct { client *http.Client } +type songLinkPlatformLink struct { + URL string `json:"url"` +} + type TrackAvailability struct { SpotifyID string `json:"spotify_id"` Tidal bool `json:"tidal"` @@ -43,6 +48,7 @@ var ( songLinkCheckAvailabilityFromDeezer = func(s *SongLinkClient, deezerTrackID string) (*TrackAvailability, error) { return s.CheckAvailabilityFromDeezer(deezerTrackID) } + songLinkRetryConfig = DefaultRetryConfig ) func NewSongLinkClient() *SongLinkClient { @@ -130,7 +136,14 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc stri } func (s *SongLinkClient) checkTrackAvailabilityFromSpotify(spotifyTrackID string) (*TrackAvailability, error) { - songLinkRateLimiter.WaitForSlot() + availability, pageErr := s.checkTrackAvailabilityFromSpotifyPage(spotifyTrackID) + if pageErr == nil { + return availability, nil + } + + if !songLinkRateLimiter.TryAcquire() { + return nil, fmt.Errorf("song.link page lookup failed: %w (SongLink local rate limit exceeded)", pageErr) + } spotifyURL := fmt.Sprintf("https://open.spotify.com/track/%s", spotifyTrackID) apiURL := buildSongLinkURLFromTarget(spotifyURL, "") @@ -140,10 +153,10 @@ func (s *SongLinkClient) checkTrackAvailabilityFromSpotify(spotifyTrackID string return nil, fmt.Errorf("failed to create request: %w", err) } - retryConfig := DefaultRetryConfig() + retryConfig := songLinkRetryConfig() resp, err := DoRequestWithRetry(s.client, req, retryConfig) if err != nil { - return nil, fmt.Errorf("failed to check availability: %w", err) + return nil, fmt.Errorf("song.link page lookup failed: %w; SongLink API lookup failed: %w", pageErr, err) } defer resp.Body.Close() @@ -154,10 +167,10 @@ func (s *SongLinkClient) checkTrackAvailabilityFromSpotify(spotifyTrackID string return nil, fmt.Errorf("track not found on any streaming platform") } if resp.StatusCode == 429 { - return nil, fmt.Errorf("SongLink rate limit exceeded") + return nil, fmt.Errorf("song.link page lookup failed: %w; SongLink API rate limit exceeded", pageErr) } if resp.StatusCode != 200 { - return nil, fmt.Errorf("SongLink API returned status %d", resp.StatusCode) + return nil, fmt.Errorf("song.link page lookup failed: %w; SongLink API returned status %d", pageErr, resp.StatusCode) } body, err := ReadResponseBody(resp) @@ -166,59 +179,102 @@ func (s *SongLinkClient) checkTrackAvailabilityFromSpotify(spotifyTrackID string } var songLinkResp struct { - LinksByPlatform map[string]struct { - URL string `json:"url"` - } `json:"linksByPlatform"` + LinksByPlatform map[string]songLinkPlatformLink `json:"linksByPlatform"` } if err := json.Unmarshal(body, &songLinkResp); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } - availability := &TrackAvailability{ - SpotifyID: spotifyTrackID, + LogWarn("SongLink", "Spotify %s resolved via SongLink API after song.link page failure: %v", spotifyTrackID, pageErr) + return buildTrackAvailabilityFromSongLinkLinks(spotifyTrackID, songLinkResp.LinksByPlatform), nil +} + +func (s *SongLinkClient) checkTrackAvailabilityFromSpotifyPage(spotifyTrackID string) (*TrackAvailability, error) { + pageURL := fmt.Sprintf("https://song.link/s/%s", spotifyTrackID) + req, err := http.NewRequest("GET", pageURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create song.link page request: %w", err) } - if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" { - availability.Tidal = true - availability.TidalURL = tidalLink.URL - availability.TidalID = extractTidalIDFromURL(tidalLink.URL) + req.Header.Set("Accept", "text/html,application/xhtml+xml") + req.Header.Set("User-Agent", getRandomUserAgent()) + + resp, err := s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch song.link page: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == 404 { + return nil, fmt.Errorf("track not found on song.link page") + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("song.link page returned status %d", resp.StatusCode) } - if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" { - availability.Amazon = true - availability.AmazonURL = amazonLink.URL + body, err := ReadResponseBody(resp) + if err != nil { + return nil, fmt.Errorf("failed to read song.link page: %w", err) } - if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" { - availability.Deezer = true - availability.DeezerURL = deezerLink.URL - availability.DeezerID = extractDeezerIDFromURL(deezerLink.URL) + nextDataJSON, err := extractSongLinkNextDataJSON(body) + if err != nil { + return nil, err } - if qobuzLink, ok := songLinkResp.LinksByPlatform["qobuz"]; ok && qobuzLink.URL != "" { - availability.Qobuz = true - availability.QobuzURL = qobuzLink.URL - availability.QobuzID = extractQobuzIDFromURL(qobuzLink.URL) + var pageData struct { + Props struct { + PageProps struct { + PageData struct { + Sections []struct { + Links []struct { + Platform string `json:"platform"` + URL string `json:"url"` + Show bool `json:"show"` + } `json:"links"` + } `json:"sections"` + } `json:"pageData"` + } `json:"pageProps"` + } `json:"props"` + } + if err := json.Unmarshal(nextDataJSON, &pageData); err != nil { + return nil, fmt.Errorf("failed to decode song.link page data: %w", err) } - // Prefer youtubeMusic URLs — they bypass Cobalt login requirements - if ytMusicLink, ok := songLinkResp.LinksByPlatform["youtubeMusic"]; ok && ytMusicLink.URL != "" { - availability.YouTube = true - availability.YouTubeURL = ytMusicLink.URL - availability.YouTubeID = extractYouTubeIDFromURL(ytMusicLink.URL) - } - - // Fallback to regular youtube if youtubeMusic not available - if !availability.YouTube { - if youtubeLink, ok := songLinkResp.LinksByPlatform["youtube"]; ok && youtubeLink.URL != "" { - availability.YouTube = true - availability.YouTubeURL = youtubeLink.URL - availability.YouTubeID = extractYouTubeIDFromURL(youtubeLink.URL) + linksByPlatform := make(map[string]songLinkPlatformLink) + for _, section := range pageData.Props.PageProps.PageData.Sections { + for _, link := range section.Links { + if !link.Show || strings.TrimSpace(link.URL) == "" { + continue + } + linksByPlatform[link.Platform] = songLinkPlatformLink{URL: link.URL} } } - return availability, nil + if len(linksByPlatform) == 0 { + return nil, fmt.Errorf("song.link page contained no usable platform links") + } + + return buildTrackAvailabilityFromSongLinkLinks(spotifyTrackID, linksByPlatform), nil +} + +func extractSongLinkNextDataJSON(body []byte) ([]byte, error) { + const startMarker = `` + + start := bytes.Index(body, []byte(startMarker)) + if start < 0 { + return nil, fmt.Errorf("song.link page missing __NEXT_DATA__") + } + start += len(startMarker) + + end := bytes.Index(body[start:], []byte(endMarker)) + if end < 0 { + return nil, fmt.Errorf("song.link page has unterminated __NEXT_DATA__") + } + + return body[start : start+end], nil } func (s *SongLinkClient) checkTrackAvailabilityFromISRC(isrc string) (*TrackAvailability, error) { @@ -459,7 +515,7 @@ func (s *SongLinkClient) CheckAlbumAvailability(spotifyAlbumID string) (*AlbumAv return nil, fmt.Errorf("failed to create request: %w", err) } - retryConfig := DefaultRetryConfig() + retryConfig := songLinkRetryConfig() resp, err := DoRequestWithRetry(s.client, req, retryConfig) if err != nil { return nil, fmt.Errorf("failed to check album availability: %w", err) @@ -542,7 +598,7 @@ func (s *SongLinkClient) checkAvailabilityFromDeezerSongLink(deezerTrackID strin return nil, fmt.Errorf("failed to create request: %w", err) } - retryConfig := DefaultRetryConfig() + retryConfig := songLinkRetryConfig() resp, err := DoRequestWithRetry(s.client, req, retryConfig) if err != nil { return nil, fmt.Errorf("failed to check availability: %w", err) @@ -647,7 +703,7 @@ func (s *SongLinkClient) CheckAvailabilityByPlatform(platform, entityType, entit return nil, fmt.Errorf("failed to create request: %w", err) } - retryConfig := DefaultRetryConfig() + retryConfig := songLinkRetryConfig() resp, err := DoRequestWithRetry(s.client, req, retryConfig) if err != nil { return nil, fmt.Errorf("failed to check availability: %w", err) @@ -728,6 +784,51 @@ func (s *SongLinkClient) CheckAvailabilityByPlatform(platform, entityType, entit return availability, nil } +func buildTrackAvailabilityFromSongLinkLinks(spotifyTrackID string, links map[string]songLinkPlatformLink) *TrackAvailability { + availability := &TrackAvailability{ + SpotifyID: spotifyTrackID, + } + + if availability.SpotifyID == "" { + if spotifyLink, ok := links["spotify"]; ok && spotifyLink.URL != "" { + availability.SpotifyID = extractSpotifyIDFromURL(spotifyLink.URL) + } + } + if tidalLink, ok := links["tidal"]; ok && tidalLink.URL != "" { + availability.Tidal = true + availability.TidalURL = tidalLink.URL + availability.TidalID = extractTidalIDFromURL(tidalLink.URL) + } + if amazonLink, ok := links["amazonMusic"]; ok && amazonLink.URL != "" { + availability.Amazon = true + availability.AmazonURL = amazonLink.URL + } + if qobuzLink, ok := links["qobuz"]; ok && qobuzLink.URL != "" { + availability.Qobuz = true + availability.QobuzURL = qobuzLink.URL + availability.QobuzID = extractQobuzIDFromURL(qobuzLink.URL) + } + if deezerLink, ok := links["deezer"]; ok && deezerLink.URL != "" { + availability.Deezer = true + availability.DeezerURL = deezerLink.URL + availability.DeezerID = extractDeezerIDFromURL(deezerLink.URL) + } + if ytMusicLink, ok := links["youtubeMusic"]; ok && ytMusicLink.URL != "" { + availability.YouTube = true + availability.YouTubeURL = ytMusicLink.URL + availability.YouTubeID = extractYouTubeIDFromURL(ytMusicLink.URL) + } + if !availability.YouTube { + if youtubeLink, ok := links["youtube"]; ok && youtubeLink.URL != "" { + availability.YouTube = true + availability.YouTubeURL = youtubeLink.URL + availability.YouTubeID = extractYouTubeIDFromURL(youtubeLink.URL) + } + } + + return availability +} + func extractSpotifyIDFromURL(spotifyURL string) string { parts := strings.Split(spotifyURL, "/track/") if len(parts) > 1 { @@ -802,7 +903,7 @@ func (s *SongLinkClient) CheckAvailabilityFromURL(inputURL string) (*TrackAvaila return nil, fmt.Errorf("failed to create request: %w", err) } - retryConfig := DefaultRetryConfig() + retryConfig := songLinkRetryConfig() resp, err := DoRequestWithRetry(s.client, req, retryConfig) if err != nil { return nil, fmt.Errorf("failed to check availability: %w", err) diff --git a/go_backend/songlink_test.go b/go_backend/songlink_test.go new file mode 100644 index 00000000..ac085c1b --- /dev/null +++ b/go_backend/songlink_test.go @@ -0,0 +1,127 @@ +package gobackend + +import ( + "io" + "net/http" + "strings" + "testing" +) + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return fn(req) +} + +func TestGetRetryAfterDurationMissingHeaderReturnsZero(t *testing.T) { + resp := &http.Response{ + Header: make(http.Header), + } + + if got := getRetryAfterDuration(resp); got != 0 { + t.Fatalf("getRetryAfterDuration() = %v, want 0", got) + } +} + +func TestCheckTrackAvailabilityFromSpotifyPrefersSongLinkPage(t *testing.T) { + client := &SongLinkClient{ + client: &http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case req.URL.Host == "api.song.link": + t.Fatalf("api.song.link should not be called when song.link page succeeds") + return nil, nil + case req.URL.Host == "song.link" && req.URL.Path == "/s/testspotifyid": + body := `` + return &http.Response{ + StatusCode: 200, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(body)), + Request: req, + }, nil + default: + t.Fatalf("unexpected request: %s", req.URL.String()) + return nil, nil + } + }), + }, + } + + availability, err := client.CheckTrackAvailability("testspotifyid", "") + if err != nil { + t.Fatalf("CheckTrackAvailability() error = %v", err) + } + + if availability.SpotifyID != "testspotifyid" { + t.Fatalf("SpotifyID = %q, want %q", availability.SpotifyID, "testspotifyid") + } + if !availability.Deezer || availability.DeezerID != "908604612" { + t.Fatalf("Deezer availability = %+v, want DeezerID 908604612", availability) + } + if !availability.Amazon || !availability.Tidal || !availability.Qobuz || !availability.YouTube { + t.Fatalf("availability flags = %+v, want Amazon/Tidal/Qobuz/YouTube true", availability) + } + if availability.YouTubeID != "testvideoid1" { + t.Fatalf("YouTubeID = %q, want %q", availability.YouTubeID, "testvideoid1") + } +} + +func TestCheckTrackAvailabilityFromSpotifyFallsBackToAPIWhenPageFails(t *testing.T) { + origRetryConfig := songLinkRetryConfig + songLinkRetryConfig = func() RetryConfig { + return RetryConfig{ + MaxRetries: 0, + InitialDelay: 0, + MaxDelay: 0, + BackoffFactor: 1, + } + } + defer func() { + songLinkRetryConfig = origRetryConfig + }() + + client := &SongLinkClient{ + client: &http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case req.URL.Host == "song.link" && req.URL.Path == "/s/testspotifyid": + return &http.Response{ + StatusCode: 500, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader("page failure")), + Request: req, + }, nil + case req.URL.Host == "api.song.link": + body := `{"linksByPlatform":{"spotify":{"url":"https://open.spotify.com/track/testspotifyid"},"deezer":{"url":"https://www.deezer.com/track/908604612"},"amazonMusic":{"url":"https://music.amazon.com/albums/B086Q2QNLH?trackAsin=B086Q41M9C"},"tidal":{"url":"https://listen.tidal.com/track/134858527"},"qobuz":{"url":"https://open.qobuz.com/track/195125822"},"youtubeMusic":{"url":"https://music.youtube.com/watch?v=testvideoid1"}}}` + return &http.Response{ + StatusCode: 200, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(body)), + Request: req, + }, nil + default: + t.Fatalf("unexpected request: %s", req.URL.String()) + return nil, nil + } + }), + }, + } + + availability, err := client.CheckTrackAvailability("testspotifyid", "") + if err != nil { + t.Fatalf("CheckTrackAvailability() error = %v", err) + } + + if availability.SpotifyID != "testspotifyid" { + t.Fatalf("SpotifyID = %q, want %q", availability.SpotifyID, "testspotifyid") + } + if !availability.Deezer || availability.DeezerID != "908604612" { + t.Fatalf("Deezer availability = %+v, want DeezerID 908604612", availability) + } + if !availability.Amazon || !availability.Tidal || !availability.Qobuz || !availability.YouTube { + t.Fatalf("availability flags = %+v, want Amazon/Tidal/Qobuz/YouTube true", availability) + } + if availability.YouTubeID != "testvideoid1" { + t.Fatalf("YouTubeID = %q, want %q", availability.YouTubeID, "testvideoid1") + } +} From e003b15ffd362f9a6bfdcf002cf2fce89a750b33 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 17 Mar 2026 15:00:56 +0700 Subject: [PATCH 122/145] fix: skip tracks already in FLAC from queue-as-FLAC selection and fix local album track list widget identity --- lib/screens/local_album_screen.dart | 61 ++++++++++--------- lib/screens/queue_tab.dart | 48 +++++++++------ .../local_track_redownload_service.dart | 9 +++ 3 files changed, 70 insertions(+), 48 deletions(-) diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 42818a12..5fe1b77a 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -626,11 +626,13 @@ class _LocalAlbumScreenState extends ConsumerState { slivers.add( SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => - _buildTrackItem(context, colorScheme, discTracks[index]), - childCount: discTracks.length, - ), + delegate: SliverChildBuilderDelegate((context, index) { + final track = discTracks[index]; + return KeyedSubtree( + key: ValueKey(track.id), + child: _buildTrackItem(context, colorScheme, track), + ); + }, childCount: discTracks.length), ), ); } @@ -900,16 +902,19 @@ class _LocalAlbumScreenState extends ConsumerState { return false; } - Future _queueSelectedAsFlac(List allTracks) async { + List _selectedFlacEligibleItems( + List allTracks, + ) { final tracksById = {for (final t in allTracks) t.id: t}; - final selected = []; + return _selectedIds + .map((id) => tracksById[id]) + .whereType() + .where(LocalTrackRedownloadService.isFlacUpgradeEligible) + .toList(growable: false); + } - for (final id in _selectedIds) { - final item = tracksById[id]; - if (item != null) { - selected.add(item); - } - } + Future _queueSelectedAsFlac(List allTracks) async { + final selected = _selectedFlacEligibleItems(allTracks); if (selected.isEmpty) { return; @@ -962,9 +967,7 @@ class _LocalAlbumScreenState extends ConsumerState { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.queueFlacFindingProgress(i + 1, total), - ), + content: Text(context.l10n.queueFlacFindingProgress(i + 1, total)), duration: const Duration(seconds: 30), ), ); @@ -1177,8 +1180,9 @@ class _LocalAlbumScreenState extends ConsumerState { String selectedFormat = formats.first; bool isLosslessTarget = selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; - String selectedBitrate = - isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); + String selectedBitrate = isLosslessTarget + ? '320k' + : (selectedFormat == 'Opus' ? '128k' : '320k'); showModalBottomSheet( context: context, @@ -1240,8 +1244,9 @@ class _LocalAlbumScreenState extends ConsumerState { isLosslessTarget = format == 'ALAC' || format == 'FLAC'; if (!isLosslessTarget) { - selectedBitrate = - format == 'Opus' ? '128k' : '320k'; + selectedBitrate = format == 'Opus' + ? '128k' + : '320k'; } }); } @@ -1286,11 +1291,8 @@ class _LocalAlbumScreenState extends ConsumerState { const SizedBox(width: 6), Text( context.l10n.trackConvertLosslessHint, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith( - color: colorScheme.primary, - ), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colorScheme.primary), ), ], ), @@ -1371,7 +1373,8 @@ class _LocalAlbumScreenState extends ConsumerState { if (currentFormat == null || currentFormat == targetFormat) continue; // Skip lossy sources when target is lossless (pointless re-encoding) final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; - final isLosslessSource = currentFormat == 'FLAC' || currentFormat == 'M4A'; + final isLosslessSource = + currentFormat == 'FLAC' || currentFormat == 'M4A'; if (isLosslessTarget && !isLosslessSource) continue; selected.add(item); } @@ -1656,6 +1659,7 @@ class _LocalAlbumScreenState extends ConsumerState { double bottomPadding, ) { final selectedCount = _selectedIds.length; + final flacEligibleCount = _selectedFlacEligibleItems(tracks).length; final allSelected = selectedCount == tracks.length && tracks.isNotEmpty; return Container( @@ -1750,8 +1754,9 @@ class _LocalAlbumScreenState extends ConsumerState { Expanded( child: _LocalAlbumSelectionActionButton( icon: Icons.download_for_offline_outlined, - label: '${context.l10n.queueFlacAction} ($selectedCount)', - onPressed: selectedCount > 0 + label: + '${context.l10n.queueFlacAction} ($flacEligibleCount)', + onPressed: flacEligibleCount > 0 ? () => _queueSelectedAsFlac(tracks) : null, colorScheme: colorScheme, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 69b1aa44..eb82c984 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4484,14 +4484,21 @@ class _QueueTabState extends ConsumerState { return false; } + List _selectedFlacEligibleLocalItems( + List allItems, + ) { + final selectedItems = _selectedItemsFromAll(allItems); + return selectedItems + .map((item) => item.localItem) + .whereType() + .where(LocalTrackRedownloadService.isFlacUpgradeEligible) + .toList(growable: false); + } + Future _queueSelectedLocalAsFlac( List allItems, ) async { - final selectedItems = _selectedItemsFromAll(allItems); - final selectedLocalItems = selectedItems - .map((item) => item.localItem) - .whereType() - .toList(growable: false); + final selectedLocalItems = _selectedFlacEligibleLocalItems(allItems); if (selectedLocalItems.isEmpty) { return; @@ -4546,9 +4553,7 @@ class _QueueTabState extends ConsumerState { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.queueFlacFindingProgress(i + 1, total), - ), + content: Text(context.l10n.queueFlacFindingProgress(i + 1, total)), duration: const Duration(seconds: 30), ), ); @@ -4797,8 +4802,9 @@ class _QueueTabState extends ConsumerState { String selectedFormat = formats.first; bool isLosslessTarget = selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; - String selectedBitrate = - isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); + String selectedBitrate = isLosslessTarget + ? '320k' + : (selectedFormat == 'Opus' ? '128k' : '320k'); var didStartConversion = false; _hideSelectionOverlay(); @@ -4864,8 +4870,9 @@ class _QueueTabState extends ConsumerState { isLosslessTarget = format == 'ALAC' || format == 'FLAC'; if (!isLosslessTarget) { - selectedBitrate = - format == 'Opus' ? '128k' : '320k'; + selectedBitrate = format == 'Opus' + ? '128k' + : '320k'; } }); } @@ -4910,11 +4917,8 @@ class _QueueTabState extends ConsumerState { const SizedBox(width: 6), Text( context.l10n.trackConvertLosslessHint, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith( - color: colorScheme.primary, - ), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colorScheme.primary), ), ], ), @@ -5054,7 +5058,8 @@ class _QueueTabState extends ConsumerState { int successCount = 0; final total = selectedItems.length; final historyDb = HistoryDatabase.instance; - final newQuality = (targetFormat.toUpperCase() == 'ALAC' || + final newQuality = + (targetFormat.toUpperCase() == 'ALAC' || targetFormat.toUpperCase() == 'FLAC') ? '${targetFormat.toUpperCase()} Lossless' : '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; @@ -5375,6 +5380,9 @@ class _QueueTabState extends ConsumerState { final allSelected = selectedCount == unifiedItems.length && unifiedItems.isNotEmpty; final localOnlySelection = _isLocalOnlySelection(unifiedItems); + final flacEligibleCount = _selectedFlacEligibleLocalItems( + unifiedItems, + ).length; return Container( decoration: BoxDecoration( @@ -5469,8 +5477,8 @@ class _QueueTabState extends ConsumerState { child: _SelectionActionButton( icon: Icons.download_for_offline_outlined, label: - '${context.l10n.queueFlacAction} ($selectedCount)', - onPressed: selectedCount > 0 + '${context.l10n.queueFlacAction} ($flacEligibleCount)', + onPressed: flacEligibleCount > 0 ? () => _queueSelectedLocalAsFlac(unifiedItems) : null, colorScheme: colorScheme, diff --git a/lib/services/local_track_redownload_service.dart b/lib/services/local_track_redownload_service.dart index 4b8500c7..d03c6c8e 100644 --- a/lib/services/local_track_redownload_service.dart +++ b/lib/services/local_track_redownload_service.dart @@ -23,6 +23,15 @@ class LocalTrackRedownloadService { static const int _minimumConfidenceScore = 85; static const int _ambiguousScoreGap = 8; + static bool isFlacUpgradeEligible(LocalLibraryItem item) { + final format = item.format?.trim().toLowerCase(); + if (format == 'flac') { + return false; + } + + return !item.filePath.toLowerCase().endsWith('.flac'); + } + static Future resolveBestMatch( LocalLibraryItem item, { required bool includeExtensions, From c0bd10cfca4ab69afeb569e69d51ec59b0cf6ba2 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 17 Mar 2026 15:04:45 +0700 Subject: [PATCH 123/145] fix: skip already-downloaded tracks in library folder download-all --- lib/screens/library_tracks_folder_screen.dart | 77 +++++++++++++++++-- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/lib/screens/library_tracks_folder_screen.dart b/lib/screens/library_tracks_folder_screen.dart index ed8c62d6..2b792b0b 100644 --- a/lib/screens/library_tracks_folder_screen.dart +++ b/lib/screens/library_tracks_folder_screen.dart @@ -872,12 +872,54 @@ class _LibraryTracksFolderScreenState void _downloadAll(List tracks) { if (tracks.isEmpty) return; + final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); - final playlistName = widget.mode == LibraryTracksFolderMode.playlist ? playlist?.name ?? context.l10n.collectionPlaylist : null; + final localLibState = + (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) + ? ref.read(localLibraryProvider) + : null; + final playlistName = widget.mode == LibraryTracksFolderMode.playlist + ? playlist?.name ?? context.l10n.collectionPlaylist + : null; + final tracksToQueue = []; + var skippedCount = 0; + + for (final track in tracks) { + final isInHistory = + historyState.isDownloaded(track.id) || + (track.isrc != null && historyState.getByIsrc(track.isrc!) != null) || + historyState.findByTrackAndArtist(track.name, track.artistName) != + null; + final isInLocal = + localLibState?.existsInLibrary( + isrc: track.isrc, + trackName: track.name, + artistName: track.artistName, + ) ?? + false; + + if (isInHistory || isInLocal) { + skippedCount++; + } else { + tracksToQueue.add(track); + } + } + + if (tracksToQueue.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.discographySkippedDownloaded(0, skippedCount), + ), + ), + ); + return; + } + if (settings.askQualityBeforeDownload) { DownloadServicePicker.show( context, - trackName: '${tracks.length} tracks', + trackName: '${tracksToQueue.length} tracks', artistName: switch (widget.mode) { LibraryTracksFolderMode.wishlist => context.l10n.collectionWishlist, LibraryTracksFolderMode.loved => context.l10n.collectionLoved, @@ -886,12 +928,24 @@ class _LibraryTracksFolderScreenState onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, service, qualityOverride: quality, playlistName: playlistName); + .addMultipleToQueue( + tracksToQueue, + service, + qualityOverride: quality, + playlistName: playlistName, + ); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - context.l10n.snackbarAddedTracksToQueue(tracks.length), + skippedCount > 0 + ? context.l10n.discographySkippedDownloaded( + tracksToQueue.length, + skippedCount, + ) + : context.l10n.snackbarAddedTracksToQueue( + tracksToQueue.length, + ), ), ), ); @@ -900,10 +954,21 @@ class _LibraryTracksFolderScreenState } else { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, settings.defaultService, playlistName: playlistName); + .addMultipleToQueue( + tracksToQueue, + settings.defaultService, + playlistName: playlistName, + ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), + content: Text( + skippedCount > 0 + ? context.l10n.discographySkippedDownloaded( + tracksToQueue.length, + skippedCount, + ) + : context.l10n.snackbarAddedTracksToQueue(tracksToQueue.length), + ), ), ); } From 7d11d67cd2b457783e3081bcd79602dc4fbbc88f Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 17 Mar 2026 15:07:05 +0700 Subject: [PATCH 124/145] chore: bump version to 3.8.7+113 --- lib/constants/app_info.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 88a98c6e..486601f3 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -3,8 +3,8 @@ import 'package:flutter/foundation.dart'; /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.8.5'; - static const String buildNumber = '111'; + static const String version = '3.8.7'; + static const String buildNumber = '113'; static const String fullVersion = '$version+$buildNumber'; /// Shows "Internal" in debug builds, actual version in release. diff --git a/pubspec.yaml b/pubspec.yaml index c54be740..1aabd893 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Deezer publish_to: "none" -version: 3.8.5+111 +version: 3.8.7+113 environment: sdk: ^3.10.0 From 814deca19d3f4b2a6fe4c1e8d553cb179ea2eb53 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 17 Mar 2026 15:19:46 +0700 Subject: [PATCH 125/145] fix: hide queue-as-FLAC button when all selected tracks are already FLAC --- lib/screens/local_album_screen.dart | 22 +++++++++++----------- lib/screens/queue_tab.dart | 6 ++---- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 5fe1b77a..bffcb089 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -1751,18 +1751,18 @@ class _LocalAlbumScreenState extends ConsumerState { Row( children: [ - Expanded( - child: _LocalAlbumSelectionActionButton( - icon: Icons.download_for_offline_outlined, - label: - '${context.l10n.queueFlacAction} ($flacEligibleCount)', - onPressed: flacEligibleCount > 0 - ? () => _queueSelectedAsFlac(tracks) - : null, - colorScheme: colorScheme, + if (flacEligibleCount > 0) ...[ + Expanded( + child: _LocalAlbumSelectionActionButton( + icon: Icons.download_for_offline_outlined, + label: + '${context.l10n.queueFlacAction} ($flacEligibleCount)', + onPressed: () => _queueSelectedAsFlac(tracks), + colorScheme: colorScheme, + ), ), - ), - const SizedBox(width: 8), + const SizedBox(width: 8), + ], Expanded( child: _LocalAlbumSelectionActionButton( icon: Icons.auto_fix_high_outlined, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index eb82c984..26ba7861 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -5472,15 +5472,13 @@ class _QueueTabState extends ConsumerState { // Action buttons row: Share/Re-enrich, Convert, Delete Row( children: [ - if (localOnlySelection) ...[ + if (localOnlySelection && flacEligibleCount > 0) ...[ Expanded( child: _SelectionActionButton( icon: Icons.download_for_offline_outlined, label: '${context.l10n.queueFlacAction} ($flacEligibleCount)', - onPressed: flacEligibleCount > 0 - ? () => _queueSelectedLocalAsFlac(unifiedItems) - : null, + onPressed: () => _queueSelectedLocalAsFlac(unifiedItems), colorScheme: colorScheme, ), ), From 66a89d9e8e988afee444610dd9003c8e0e7a68f8 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 17 Mar 2026 15:26:51 +0700 Subject: [PATCH 126/145] fix: properly stop active downloads when pausing the queue --- lib/providers/download_queue_provider.dart | 166 +++++++++++++++++++-- 1 file changed, 156 insertions(+), 10 deletions(-) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 8c2bb5f6..5898445c 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1005,6 +1005,7 @@ class DownloadQueueNotifier extends Notifier { int _lastNotifPercent = -1; int _lastNotifQueueCount = -1; final Set _locallyCancelledItemIds = {}; + final Set _pausePendingItemIds = {}; double _normalizeProgressForUi(double value) { final clamped = value.clamp(0.0, 1.0).toDouble(); @@ -1324,6 +1325,10 @@ class DownloadQueueNotifier extends Notifier { if (localItem == null) { continue; } + if (_isPausePending(itemId)) { + PlatformBridge.clearItemProgress(itemId).catchError((_) {}); + continue; + } if (localItem.status == DownloadStatus.skipped) { PlatformBridge.clearItemProgress(itemId).catchError((_) {}); continue; @@ -2123,12 +2128,42 @@ class DownloadQueueNotifier extends Notifier { return resolved?.status == DownloadStatus.skipped; } + bool _isPausePending(String id) => _pausePendingItemIds.contains(id); + + void _requeueItemForPause(String id) { + final updatedItems = state.items + .map((item) { + if (item.id != id) return item; + if (item.status == DownloadStatus.completed || + item.status == DownloadStatus.failed || + item.status == DownloadStatus.skipped) { + return item; + } + return item.copyWith( + status: DownloadStatus.queued, + progress: 0, + speedMBps: 0, + bytesReceived: 0, + ); + }) + .toList(growable: false); + + final currentDownload = state.currentDownload?.id == id + ? null + : state.currentDownload; + state = state.copyWith( + items: updatedItems, + currentDownload: currentDownload, + ); + } + void _requestNativeCancel(String id) { PlatformBridge.cancelDownload(id).catchError((_) {}); PlatformBridge.clearItemProgress(id).catchError((_) {}); } void cancelItem(String id) { + _pausePendingItemIds.remove(id); _locallyCancelledItemIds.add(id); updateItemStatus(id, DownloadStatus.skipped); _requestNativeCancel(id); @@ -2161,6 +2196,7 @@ class DownloadQueueNotifier extends Notifier { .toList(growable: false); if (activeIds.isNotEmpty) { + _pausePendingItemIds.addAll(activeIds); _locallyCancelledItemIds.addAll(activeIds); for (final id in activeIds) { _requestNativeCancel(id); @@ -2173,11 +2209,29 @@ class DownloadQueueNotifier extends Notifier { if (!wasProcessing) { _locallyCancelledItemIds.clear(); } + _pausePendingItemIds.clear(); } void pauseQueue() { if (state.isProcessing && !state.isPaused) { - state = state.copyWith(isPaused: true); + final activeIds = state.items + .where( + (item) => + item.status == DownloadStatus.downloading || + item.status == DownloadStatus.finalizing, + ) + .map((item) => item.id) + .toSet(); + + if (activeIds.isNotEmpty) { + _pausePendingItemIds.addAll(activeIds); + for (final id in activeIds) { + _requestNativeCancel(id); + _requeueItemForPause(id); + } + } + + state = state.copyWith(isPaused: true, currentDownload: null); _notificationService.cancelDownloadNotification(); _log.i('Queue paused'); } @@ -3268,7 +3322,11 @@ class DownloadQueueNotifier extends Notifier { } final queuedItems = state.items - .where((item) => item.status == DownloadStatus.queued) + .where( + (item) => + item.status == DownloadStatus.queued && + !_pausePendingItemIds.contains(item.id), + ) .toList(); if (queuedItems.isEmpty && activeDownloads.isEmpty) { @@ -3313,11 +3371,13 @@ class DownloadQueueNotifier extends Notifier { _stopProgressPolling(); final remainingIds = state.items.map((item) => item.id).toSet(); _locallyCancelledItemIds.removeWhere((id) => !remainingIds.contains(id)); + _pausePendingItemIds.removeWhere((id) => !remainingIds.contains(id)); } Future _downloadSingleItem(DownloadItem item) async { _log.d('Processing: ${item.track.name} by ${item.track.artistName}'); _log.d('Cover URL: ${item.track.coverUrl}'); + var pausedDuringThisRun = false; final currentItem = _findItemById(item.id) ?? item; if (_isLocallyCancelled(item.id, item: currentItem)) { @@ -3325,11 +3385,33 @@ class DownloadQueueNotifier extends Notifier { return; } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + _requeueItemForPause(item.id); + _log.i('Download is pause-pending before start, skipping'); + return; + } + state = state.copyWith(currentDownload: item); updateItemStatus(item.id, DownloadStatus.downloading); try { + bool shouldAbortWork(String stage) { + final current = _findItemById(item.id); + if (_isLocallyCancelled(item.id, item: current)) { + _log.i('Download was cancelled $stage, skipping'); + return true; + } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + _requeueItemForPause(item.id); + _log.i('Download pause requested $stage, re-queueing'); + return true; + } + return false; + } + final settings = ref.read(settingsProvider); final metadataEmbeddingEnabled = settings.embedMetadata; @@ -3410,6 +3492,10 @@ class DownloadQueueNotifier extends Notifier { _log.w('Failed to enrich metadata: $e'); _log.w('Stack trace: $stack'); } + + if (shouldAbortWork('during metadata enrichment')) { + return; + } } _log.d('Track coverUrl after enrichment: ${trackToDownload.coverUrl}'); @@ -3523,6 +3609,10 @@ class DownloadQueueNotifier extends Notifier { } catch (e) { _log.w('Failed to search Deezer by ISRC: $e'); } + + if (shouldAbortWork('during Deezer ISRC lookup')) { + return; + } } // Fallback: Use SongLink to convert Spotify ID to Deezer ID @@ -3623,6 +3713,10 @@ class DownloadQueueNotifier extends Notifier { } catch (e) { _log.w('Failed to convert Spotify to Deezer via SongLink: $e'); } + + if (shouldAbortWork('during SongLink availability lookup')) { + return; + } } if (deezerTrackId != null && deezerTrackId.isNotEmpty) { @@ -3642,6 +3736,10 @@ class DownloadQueueNotifier extends Notifier { } catch (e) { _log.w('Failed to fetch extended metadata from Deezer: $e'); } + + if (shouldAbortWork('during extended metadata lookup')) { + return; + } } Map result; @@ -3760,8 +3858,7 @@ class DownloadQueueNotifier extends Notifier { ); } - if (_isLocallyCancelled(item.id)) { - _log.i('Download was cancelled before native download start, skipping'); + if (shouldAbortWork('before native download start')) { return; } @@ -3803,10 +3900,8 @@ class DownloadQueueNotifier extends Notifier { _log.d('Result: $result'); final itemAfterResult = _findItemById(item.id); - final cancelledAfterResult = - itemAfterResult == null || - _isLocallyCancelled(item.id, item: itemAfterResult); - if (cancelledAfterResult) { + if (itemAfterResult == null || + _isLocallyCancelled(item.id, item: itemAfterResult)) { _log.i('Download was cancelled, skipping result processing'); final filePath = result['file_path'] as String?; if (filePath != null && result['success'] == true) { @@ -3816,6 +3911,18 @@ class DownloadQueueNotifier extends Notifier { return; } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + final filePath = result['file_path'] as String?; + if (filePath != null && result['success'] == true) { + await deleteFile(filePath); + _log.d('Deleted paused download file: $filePath'); + } + _requeueItemForPause(item.id); + _log.i('Download pause requested after result, re-queueing'); + return; + } + if (result['success'] == true) { var filePath = result['file_path'] as String?; final reportedFileName = result['file_name'] as String?; @@ -4349,6 +4456,19 @@ class DownloadQueueNotifier extends Notifier { return; } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + if (filePath != null) { + await deleteFile(filePath); + _log.d( + 'Deleted paused download file during finalization: $filePath', + ); + } + _requeueItemForPause(item.id); + _log.i('Download pause requested during finalization, re-queueing'); + return; + } + // SAF downloads should end with content URI. If we still have a // transient FD path, recover URI from SAF metadata to keep history // dedup/exclusion stable. @@ -4616,11 +4736,26 @@ class DownloadQueueNotifier extends Notifier { return; } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + _requeueItemForPause(item.id); + _log.i('Download pause requested after backend failure, re-queueing'); + return; + } + final errorMsg = result['error'] as String? ?? 'Download failed'; final errorTypeStr = result['error_type'] as String? ?? 'unknown'; if (errorTypeStr == 'cancelled') { - _log.i('Download was cancelled by backend, skipping error handling'); - updateItemStatus(item.id, DownloadStatus.skipped); + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + _requeueItemForPause(item.id); + _log.i('Download was paused by backend cancellation, re-queueing'); + } else { + _log.i( + 'Download was cancelled by backend, skipping error handling', + ); + updateItemStatus(item.id, DownloadStatus.skipped); + } return; } @@ -4679,6 +4814,13 @@ class DownloadQueueNotifier extends Notifier { return; } + if (_isPausePending(item.id)) { + pausedDuringThisRun = true; + _requeueItemForPause(item.id); + _log.i('Download pause requested after exception, re-queueing'); + return; + } + _log.e('Exception: $e', e, stackTrace); String errorMsg = e.toString(); @@ -4704,6 +4846,10 @@ class DownloadQueueNotifier extends Notifier { } catch (cleanupErr) { _log.e('Post-exception connection cleanup failed: $cleanupErr'); } + } finally { + if (pausedDuringThisRun) { + _pausePendingItemIds.remove(item.id); + } } } } From 5ccd06cc680eca3e8a287dd4b11e7a664fc76d01 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 17 Mar 2026 23:54:49 +0700 Subject: [PATCH 127/145] fix: stabilize library scan IDs, pause queue behavior, and scan race condition - Generate stable SHA-1 based IDs for SAF-scanned library items to prevent null ID crashes on the Dart side - Suppress false queue-complete notification when user pauses instead of finishing the queue, and break out of parallel loop immediately when paused with no active downloads - Use SQLite as the single source of truth for library scan results to fix a race condition where auto-scan could fire before provider state finished loading, dropping unchanged rows --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 16 ++++++++++-- lib/providers/download_queue_provider.dart | 20 +++++++++++--- lib/providers/local_library_provider.dart | 26 ++++++++++++++----- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 5ffe598f..ada75b95 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -30,6 +30,7 @@ import org.json.JSONObject import java.io.File import java.io.FileInputStream import java.io.FileOutputStream +import java.security.MessageDigest import java.util.Locale class MainActivity: FlutterFragmentActivity() { @@ -111,6 +112,13 @@ class MainActivity: FlutterFragmentActivity() { } } + private fun buildStableLibraryId(filePath: String): String { + val digest = MessageDigest.getInstance("SHA-1") + val bytes = digest.digest(filePath.toByteArray(Charsets.UTF_8)) + val hex = bytes.joinToString("") { "%02x".format(it) } + return "lib_$hex" + } + data class SafScanProgress( var totalFiles: Int = 0, var scannedFiles: Int = 0, @@ -1263,7 +1271,9 @@ class MainActivity: FlutterFragmentActivity() { } else { try { val lastModified = try { doc.lastModified() } catch (_: Exception) { 0L } - metadataObj.put("filePath", doc.uri.toString()) + val stableUri = doc.uri.toString() + metadataObj.put("id", buildStableLibraryId(stableUri)) + metadataObj.put("filePath", stableUri) metadataObj.put("fileModTime", lastModified) results.put(metadataObj) } catch (_: Exception) { @@ -1680,7 +1690,9 @@ class MainActivity: FlutterFragmentActivity() { } else { try { val safeLastModified = try { doc.lastModified() } catch (_: Exception) { lastModified } - metadataObj.put("filePath", doc.uri.toString()) + val stableUri = doc.uri.toString() + metadataObj.put("id", buildStableLibraryId(stableUri)) + metadataObj.put("filePath", stableUri) metadataObj.put("fileModTime", safeLastModified) metadataObj.put("lastModified", safeLastModified) results.put(metadataObj) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 5898445c..bbac0b17 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -3248,6 +3248,7 @@ class DownloadQueueNotifier extends Notifier { } _log.d('Concurrent downloads: ${state.concurrentDownloads}'); await _processQueueParallel(); + final stoppedWhilePaused = state.isPaused; _stopProgressPolling(); @@ -3273,7 +3274,7 @@ class DownloadQueueNotifier extends Notifier { _log.i( 'Queue stats - completed: $_completedInSession, failed: $_failedInSession, totalAtStart: $_totalQueuedAtStart', ); - if (_totalQueuedAtStart > 0) { + if (!stoppedWhilePaused && _totalQueuedAtStart > 0) { await _notificationService.showQueueComplete( completedCount: _completedInSession, failedCount: _failedInSession, @@ -3288,13 +3289,17 @@ class DownloadQueueNotifier extends Notifier { } } - _log.i('Queue processing finished'); + if (stoppedWhilePaused) { + _log.i('Queue processing paused'); + } else { + _log.i('Queue processing finished'); + } state = state.copyWith(isProcessing: false, currentDownload: null); final hasQueuedItems = state.items.any( (item) => item.status == DownloadStatus.queued, ); - if (hasQueuedItems) { + if (hasQueuedItems && !state.isPaused) { _log.i( 'Found queued items after processing finished, restarting queue...', ); @@ -3310,8 +3315,15 @@ class DownloadQueueNotifier extends Notifier { while (true) { if (state.isPaused) { + if (activeDownloads.isEmpty) { + _log.d('Queue is paused and no active downloads remain'); + break; + } _log.d('Queue is paused, waiting for active downloads...'); - await Future.delayed(_queueSchedulingInterval); + await Future.any([ + Future.wait(activeDownloads.values), + Future.delayed(_queueSchedulingInterval), + ]); continue; } diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index 02fa69b5..ab5416c7 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -329,6 +329,11 @@ class LocalLibraryNotifier extends Notifier { if (items.isNotEmpty) { await _db.upsertBatch(items.map((e) => e.toJson()).toList()); } + final persistedItems = + (await _db.getAll()) + .map(LocalLibraryItem.fromJson) + .toList(growable: false) + ..sort(_compareLibraryItems); final now = DateTime.now(); try { @@ -341,7 +346,7 @@ class LocalLibraryNotifier extends Notifier { } state = state.copyWith( - items: items, + items: persistedItems, isScanning: false, scanProgress: 100, lastScannedAt: now, @@ -350,11 +355,11 @@ class LocalLibraryNotifier extends Notifier { ); _log.i( - 'Full scan complete: ${items.length} tracks found, ' + 'Full scan complete: ${persistedItems.length} tracks found, ' '$skippedDownloads already in downloads', ); await _showScanCompleteNotification( - totalTracks: items.length, + totalTracks: persistedItems.length, excludedDownloadedCount: skippedDownloads, errorCount: state.scanErrorCount, ); @@ -439,8 +444,14 @@ class LocalLibraryNotifier extends Notifier { '$skippedCount skipped, ${deletedPaths.length} deleted, $totalFiles total', ); + // Build the incremental merge base from SQLite, not the current + // provider state. Startup auto-scan can fire before `state.items` has + // finished loading, which would otherwise drop unchanged rows from the + // in-memory library until a manual full rescan. + final existingJson = await _db.getAll(); final currentByPath = { - for (final item in state.items) item.filePath: item, + for (final item in existingJson.map(LocalLibraryItem.fromJson)) + item.filePath: item, }; final existingDownloadedPaths = []; currentByPath.removeWhere((path, _) { @@ -491,8 +502,11 @@ class LocalLibraryNotifier extends Notifier { _log.i('Deleted $deleteCount items from database'); } - final items = currentByPath.values.toList(growable: false) - ..sort(_compareLibraryItems); + final items = + (await _db.getAll()) + .map(LocalLibraryItem.fromJson) + .toList(growable: false) + ..sort(_compareLibraryItems); final now = DateTime.now(); try { From 855d0e3ffceccca5e685e7a4810c375fb9c5b41f Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 18 Mar 2026 00:15:44 +0700 Subject: [PATCH 128/145] feat: add zcc09 as supporter (thank you) --- lib/screens/settings/donate_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index b810f003..a9fbaff5 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -164,7 +164,7 @@ class _RecentDonorsCard extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - const donorNames = ['micahRichie', 'a fan', 'mc nuggets jimmy', 'CJBGR']; + const donorNames = ['micahRichie', 'a fan', 'mc nuggets jimmy', 'CJBGR', 'zcc09']; // Match SettingsGroup color logic final cardColor = isDark @@ -480,7 +480,7 @@ int _cr(String v) { } // Highlighted supporters (hashes of names). -const _cv = {1211573191}; +const _cv = {1211573191, 1003219236}; class _SupporterChip extends StatelessWidget { final String name; From 75db2f162b8c357399660db0644bcfac3a38648f Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 18 Mar 2026 01:06:22 +0700 Subject: [PATCH 129/145] fix: improve extension download reliability and Qobuz API integration - Add dedicated long-timeout download client (24h) for extension file downloads, preventing timeouts on large lossless audio files - Skip unnecessary SongLink Deezer prelookup when an extension download provider handles the track, reducing latency and avoiding spurious API failures - Prefer native track ID over Spotify ID when a source/provider is set, ensuring extension providers receive their own IDs correctly - Update Qobuz MusicDL API endpoint and switch payload URL to open.qobuz.com - Extract buildQobuzMusicDLPayload helper and add test coverage --- go_backend/extension_runtime.go | 28 +++++++++++++--------- go_backend/extension_runtime_file.go | 7 +++++- go_backend/qobuz.go | 22 ++++++++++------- go_backend/qobuz_test.go | 27 ++++++++++++++++++++- lib/providers/download_queue_provider.dart | 17 +++++++++++-- lib/providers/track_provider.dart | 14 +++++++---- 6 files changed, 86 insertions(+), 29 deletions(-) diff --git a/go_backend/extension_runtime.go b/go_backend/extension_runtime.go index c45b52c1..58068a42 100644 --- a/go_backend/extension_runtime.go +++ b/go_backend/extension_runtime.go @@ -81,13 +81,14 @@ func SetExtensionTokens(extensionID string, accessToken, refreshToken string, ex } type ExtensionRuntime struct { - extensionID string - manifest *ExtensionManifest - settings map[string]interface{} - httpClient *http.Client - cookieJar http.CookieJar - dataDir string - vm *goja.Runtime + extensionID string + manifest *ExtensionManifest + settings map[string]interface{} + httpClient *http.Client + downloadClient *http.Client + cookieJar http.CookieJar + dataDir string + vm *goja.Runtime storageMu sync.RWMutex storageCache map[string]interface{} @@ -132,13 +133,20 @@ func NewExtensionRuntime(ext *LoadedExtension) *ExtensionRuntime { storageFlushDelay: defaultStorageFlushDelay, } + runtime.httpClient = newExtensionHTTPClient(ext, jar, 30*time.Second) + runtime.downloadClient = newExtensionHTTPClient(ext, jar, DownloadTimeout) + + return runtime +} + +func newExtensionHTTPClient(ext *LoadedExtension, jar http.CookieJar, timeout time.Duration) *http.Client { // Extension sandbox enforces HTTPS-only domains. Do not apply global // allow_http scheme downgrade here, because some extension APIs (e.g. // spotify-web) will redirect http -> https and can end up in 301 loops. // We still reuse sharedTransport so insecure TLS compatibility mode remains effective. client := &http.Client{ Transport: sharedTransport, - Timeout: 30 * time.Second, + Timeout: timeout, Jar: jar, } client.CheckRedirect = func(req *http.Request, via []*http.Request) error { @@ -165,9 +173,7 @@ func NewExtensionRuntime(ext *LoadedExtension) *ExtensionRuntime { } return nil } - runtime.httpClient = client - - return runtime + return client } type RedirectBlockedError struct { diff --git a/go_backend/extension_runtime_file.go b/go_backend/extension_runtime_file.go index 414f174c..92312c98 100644 --- a/go_backend/extension_runtime_file.go +++ b/go_backend/extension_runtime_file.go @@ -174,7 +174,12 @@ func (r *ExtensionRuntime) fileDownload(call goja.FunctionCall) goja.Value { req.Header.Set("User-Agent", "SpotiFLAC-Extension/1.0") } - resp, err := r.httpClient.Do(req) + client := r.downloadClient + if client == nil { + client = r.httpClient + } + + resp, err := client.Do(req) if err != nil { return r.vm.ToValue(map[string]interface{}{ "success": false, diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index e354f479..8a87cd28 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -49,9 +49,10 @@ const ( qobuzArtistGetBaseURL = "https://www.qobuz.com/api.json/0.2/artist/get?artist_id=" qobuzPlaylistGetBaseURL = "https://www.qobuz.com/api.json/0.2/playlist/get?playlist_id=" qobuzStoreSearchBaseURL = "https://www.qobuz.com/us-en/search/tracks/" + qobuzTrackOpenBaseURL = "https://open.qobuz.com/track/" qobuzTrackPlayBaseURL = "https://play.qobuz.com/track/" qobuzStoreBaseURL = "https://www.qobuz.com/us-en" - qobuzDownloadAPIURL = "https://www.musicdl.me/api/qobuz/download" + qobuzDownloadAPIURL = "https://dl.musicdl.me/qobuz/download" qobuzDabMusicAPIURL = "https://dabmusic.xyz/api/stream?trackId=" qobuzDeebAPIURL = "https://dab.yeet.su/api/stream?trackId=" qobuzAfkarAPIURL = "https://qbz.afkarxyz.qzz.io/api/track/" @@ -1631,19 +1632,23 @@ func fetchQobuzURLWithRetry(provider qobuzAPIProvider, trackID int64, quality st return fetchQobuzURLSingleAttempt(provider, trackID, quality, timeout, "") } +func buildQobuzMusicDLPayload(trackID int64, quality string) ([]byte, error) { + requestQuality := mapQobuzQualityCodeToAPI(quality) + payload := map[string]any{ + "quality": requestQuality, + "upload_to_r2": false, + "url": fmt.Sprintf("%s%d", qobuzTrackOpenBaseURL, trackID), + } + return json.Marshal(payload) +} + func fetchQobuzURLSingleAttempt(provider qobuzAPIProvider, trackID int64, quality string, timeout time.Duration, country string) (qobuzDownloadInfo, error) { var lastErr error retryDelay := qobuzRetryDelay var payloadBytes []byte if provider.Kind == qobuzAPIKindMusicDL { - requestQuality := mapQobuzQualityCodeToAPI(quality) - payload := map[string]any{ - "quality": requestQuality, - "upload_to_r2": false, - "url": fmt.Sprintf("%s%d", qobuzTrackPlayBaseURL, trackID), - } var err error - payloadBytes, err = json.Marshal(payload) + payloadBytes, err = buildQobuzMusicDLPayload(trackID, quality) if err != nil { return qobuzDownloadInfo{}, fmt.Errorf("failed to encode qobuz request: %w", err) } @@ -1688,7 +1693,6 @@ func fetchQobuzURLSingleAttempt(provider qobuzAPIProvider, trackID int64, qualit } if provider.Kind == qobuzAPIKindMusicDL { req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Debug-Key", getQobuzDebugKey()) } resp, err := DoRequestWithUserAgent(client, req) diff --git a/go_backend/qobuz_test.go b/go_backend/qobuz_test.go index 9d15d318..df3b84ac 100644 --- a/go_backend/qobuz_test.go +++ b/go_backend/qobuz_test.go @@ -1,6 +1,9 @@ package gobackend -import "testing" +import ( + "encoding/json" + "testing" +) func TestParseQobuzURL(t *testing.T) { tests := []struct { @@ -195,6 +198,28 @@ func TestGetQobuzDebugKey(t *testing.T) { } } +func TestBuildQobuzMusicDLPayloadUsesOpenTrackURL(t *testing.T) { + payloadBytes, err := buildQobuzMusicDLPayload(374610875, "7") + if err != nil { + t.Fatalf("buildQobuzMusicDLPayload returned error: %v", err) + } + + var payload map[string]any + if err := json.Unmarshal(payloadBytes, &payload); err != nil { + t.Fatalf("payload is not valid JSON: %v", err) + } + + if got := payload["url"]; got != "https://open.qobuz.com/track/374610875" { + t.Fatalf("payload url = %v, want open.qobuz.com track URL", got) + } + if got := payload["quality"]; got != "hi-res" { + t.Fatalf("payload quality = %v, want hi-res", got) + } + if got := payload["upload_to_r2"]; got != false { + t.Fatalf("payload upload_to_r2 = %v, want false", got) + } +} + func TestExtractQobuzAlbumIDsFromArtistHTML(t *testing.T) { body := []byte(` diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index bbac0b17..8901c647 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -3594,6 +3594,15 @@ class DownloadQueueNotifier extends Notifier { String? genre; String? label; String? copyright; + final extensionState = ref.read(extensionProvider); + final selectedExtensionDownloadProvider = + settings.useExtensionProviders && + extensionState.extensions.any( + (e) => + e.enabled && + e.hasDownloadProvider && + e.id.toLowerCase() == item.service.toLowerCase(), + ); String? deezerTrackId = trackToDownload.deezerId; if (deezerTrackId == null && trackToDownload.id.startsWith('deezer:')) { @@ -3628,7 +3637,8 @@ class DownloadQueueNotifier extends Notifier { } // Fallback: Use SongLink to convert Spotify ID to Deezer ID - if (deezerTrackId == null && + if (!selectedExtensionDownloadProvider && + deezerTrackId == null && trackToDownload.id.isNotEmpty && !trackToDownload.id.startsWith('deezer:') && !trackToDownload.id.startsWith('extension:')) { @@ -3729,6 +3739,10 @@ class DownloadQueueNotifier extends Notifier { if (shouldAbortWork('during SongLink availability lookup')) { return; } + } else if (selectedExtensionDownloadProvider && deezerTrackId == null) { + _log.d( + 'Skipping Flutter SongLink Deezer prelookup for extension provider: ${item.service}', + ); } if (deezerTrackId != null && deezerTrackId.isNotEmpty) { @@ -3756,7 +3770,6 @@ class DownloadQueueNotifier extends Notifier { Map result; - final extensionState = ref.read(extensionProvider); final hasActiveExtensions = extensionState.extensions.any( (e) => e.enabled, ); diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index f0552eda..8947dba0 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -958,9 +958,16 @@ class TrackNotifier extends Notifier { final durationMs = _extractDurationMs(data); final itemType = data['item_type']?.toString(); + final effectiveSource = + source ?? data['source']?.toString() ?? data['provider_id']?.toString(); + final spotifyId = (data['spotify_id'] ?? '').toString(); + final nativeId = (data['id'] ?? '').toString(); + final preferredId = effectiveSource != null && effectiveSource.isNotEmpty + ? (nativeId.isNotEmpty ? nativeId : spotifyId) + : (spotifyId.isNotEmpty ? spotifyId : nativeId); return Track( - id: (data['spotify_id'] ?? data['id'] ?? '').toString(), + id: preferredId, name: (data['name'] ?? '').toString(), artistName: (data['artists'] ?? data['artist'] ?? '').toString(), albumName: (data['album_name'] ?? data['album'] ?? '').toString(), @@ -974,10 +981,7 @@ class TrackNotifier extends Notifier { discNumber: data['disc_number'] as int?, releaseDate: data['release_date']?.toString(), totalTracks: data['total_tracks'] as int?, - source: - source ?? - data['source']?.toString() ?? - data['provider_id']?.toString(), + source: effectiveSource, albumType: data['album_type']?.toString(), itemType: itemType, ); From eb143a41fc80859dc3d337c3176c39b9f784b0fa Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 18 Mar 2026 01:12:16 +0700 Subject: [PATCH 130/145] refactor: remove redundant comments and fix setMetadataSource bug - Fix setMetadataSource always returning 'deezer' regardless of input parameter - Remove self-evident doc comments that restate method/class names across app_theme, dynamic_color_wrapper, cover_cache_manager, history_database, library_database, and download_service_picker - Remove stale migration inline notes (// 12 -> 16, // 20 -> 16, etc.) from app_theme - Remove trivial section-label comments in queue_tab batch conversion method - Remove duplicate 'wait up to 5 seconds' comment in main_shell --- lib/providers/settings_provider.dart | 3 +-- lib/screens/main_shell.dart | 1 - lib/screens/queue_tab.dart | 5 ---- lib/services/cover_cache_manager.dart | 2 -- lib/services/history_database.dart | 11 -------- lib/services/library_database.dart | 5 +--- lib/theme/app_theme.dart | 32 +++++++++--------------- lib/theme/dynamic_color_wrapper.dart | 3 --- lib/widgets/download_service_picker.dart | 2 -- 9 files changed, 14 insertions(+), 50 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 33706c57..fcab3631 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -406,8 +406,7 @@ class SettingsNotifier extends Notifier { } void setMetadataSource(String source) { - final normalized = source == 'deezer' ? 'deezer' : 'deezer'; - state = state.copyWith(metadataSource: normalized); + state = state.copyWith(metadataSource: source); _saveSettings(); } diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 70c21461..0440a08c 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -83,7 +83,6 @@ class _MainShellState extends ConsumerState { final extState = ref.read(extensionProvider); if (!extState.isInitialized) { _log.d('Waiting for extensions to initialize before handling URL...'); - // Wait up to 5 seconds for extensions to initialize for (int i = 0; i < 50; i++) { await Future.delayed(const Duration(milliseconds: 100)); if (!mounted) return; diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 26ba7861..18a78c16 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -5082,7 +5082,6 @@ class _QueueTabState extends ConsumerState { ); try { - // Read metadata from file final metadata = { 'TITLE': item.trackName, 'ARTIST': item.artistName, @@ -5111,7 +5110,6 @@ class _QueueTabState extends ConsumerState { 1000, ); - // Extract cover art String? coverPath; try { final tempDir = await getTemporaryDirectory(); @@ -5126,7 +5124,6 @@ class _QueueTabState extends ConsumerState { } } catch (_) {} - // Handle SAF vs regular file String workingPath = item.filePath; final isSaf = isContentUri(item.filePath); String? safTempPath; @@ -5139,7 +5136,6 @@ class _QueueTabState extends ConsumerState { workingPath = safTempPath; } - // Convert final newPath = await FFmpegService.convertAudioFormat( inputPath: workingPath, targetFormat: targetFormat.toLowerCase(), @@ -5149,7 +5145,6 @@ class _QueueTabState extends ConsumerState { deleteOriginal: !isSaf, ); - // Cleanup cover temp if (coverPath != null) { try { await File(coverPath).delete(); diff --git a/lib/services/cover_cache_manager.dart b/lib/services/cover_cache_manager.dart index 81cb6c06..0af65464 100644 --- a/lib/services/cover_cache_manager.dart +++ b/lib/services/cover_cache_manager.dart @@ -37,7 +37,6 @@ class CoverCacheManager { final appDir = await getApplicationSupportDirectory(); _cachePath = p.join(appDir.path, 'cover_cache'); - // Ensure cache directory exists await Directory(_cachePath!).create(recursive: true); debugPrint('CoverCacheManager: Initializing at $_cachePath'); @@ -48,7 +47,6 @@ class CoverCacheManager { debugPrint('CoverCacheManager: Initialized successfully'); } catch (e) { debugPrint('CoverCacheManager: Failed to initialize: $e'); - // Will fallback to DefaultCacheManager } } diff --git a/lib/services/history_database.dart b/lib/services/history_database.dart index 75bfaada..9e648bc7 100644 --- a/lib/services/history_database.dart +++ b/lib/services/history_database.dart @@ -12,7 +12,6 @@ final Future _prefs = SharedPreferences.getInstance(); /// Cached current iOS container path for path normalization String? _currentContainerPath; -/// SQLite database service for download history /// Provides O(1) lookups by spotify_id and isrc with proper indexing class HistoryDatabase { static final HistoryDatabase instance = HistoryDatabase._init(); @@ -78,7 +77,6 @@ class HistoryDatabase { ) '''); - // Indexes for fast lookups await db.execute('CREATE INDEX idx_spotify_id ON history(spotify_id)'); await db.execute('CREATE INDEX idx_isrc ON history(isrc)'); await db.execute( @@ -171,7 +169,6 @@ class HistoryDatabase { try { final db = await database; - // Get all items with iOS paths final rows = await db.query('history', columns: ['id', 'file_path']); int updatedCount = 0; final batch = db.batch(); @@ -198,7 +195,6 @@ class HistoryDatabase { await batch.commit(noResult: true); } - // Save current container path await prefs.setString('ios_last_container_path', _currentContainerPath!); _log.i('iOS path migration complete: $updatedCount paths updated'); @@ -323,7 +319,6 @@ class HistoryDatabase { }; } - /// Insert or update a history item Future upsert(Map json) async { final db = await database; await db.insert( @@ -345,7 +340,6 @@ class HistoryDatabase { return rows.map(_dbRowToJson).toList(); } - /// Get item by ID Future?> getById(String id) async { final db = await database; final rows = await db.query( @@ -403,26 +397,22 @@ class HistoryDatabase { return rows.map((r) => r['spotify_id'] as String).toSet(); } - /// Delete by ID Future deleteById(String id) async { final db = await database; await db.delete('history', where: 'id = ?', whereArgs: [id]); } - /// Delete by Spotify ID Future deleteBySpotifyId(String spotifyId) async { final db = await database; await db.delete('history', where: 'spotify_id = ?', whereArgs: [spotifyId]); } - /// Clear all history Future clearAll() async { final db = await database; await db.delete('history'); _log.i('Cleared all history'); } - /// Get total count Future getCount() async { final db = await database; final result = await db.rawQuery('SELECT COUNT(*) as count FROM history'); @@ -459,7 +449,6 @@ class HistoryDatabase { return null; } - /// Close database Future close() async { final db = await database; await db.close(); diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index 16955171..47b3ab19 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -123,7 +123,7 @@ class LibraryDatabase { return await openDatabase( path, - version: 4, // Bumped version for bitrate column + version: 4, onConfigure: (db) async { await db.rawQuery('PRAGMA journal_mode = WAL'); await db.execute('PRAGMA synchronous = NORMAL'); @@ -331,13 +331,11 @@ class LibraryDatabase { String? trackName, String? artistName, }) async { - // First try ISRC if available if (isrc != null && isrc.isNotEmpty) { final byIsrc = await getByIsrc(isrc); if (byIsrc != null) return byIsrc; } - // Then try name matching if (trackName != null && artistName != null) { final matches = await findByTrackAndArtist(trackName, artistName); if (matches.isNotEmpty) return matches.first; @@ -523,7 +521,6 @@ class LibraryDatabase { return rows.map((r) => r['file_path'] as String).toSet(); } - /// Delete multiple items by their file paths Future deleteByPaths(List filePaths) async { if (filePaths.isEmpty) return 0; final db = await database; diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 2aba9e85..c13b54ee 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -35,7 +35,6 @@ class AppTheme { ); } - /// Create dark theme static ThemeData dark({ ColorScheme? dynamicScheme, Color? seedColor, @@ -88,12 +87,11 @@ class AppTheme { ), ); - /// Card theme static CardThemeData _cardTheme(ColorScheme scheme) => CardThemeData( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - ), // 12 -> 16 + ), color: scheme.surfaceContainerLow, surfaceTintColor: scheme.surfaceTint, ); @@ -104,18 +102,17 @@ class AppTheme { elevation: 1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - ), // 20 -> 16 + ), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ); - /// Filled button theme static FilledButtonThemeData _filledButtonTheme(ColorScheme scheme) => FilledButtonThemeData( style: FilledButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - ), // 20 -> 16 + ), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ); @@ -125,18 +122,17 @@ class AppTheme { style: OutlinedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - ), // 20 -> 16 + ), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ); - /// Text button theme static TextButtonThemeData _textButtonTheme(ColorScheme scheme) => TextButtonThemeData( style: TextButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - ), // 20 -> 16 + ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ); @@ -149,40 +145,39 @@ class AppTheme { foregroundColor: scheme.onPrimaryContainer, ); - /// Input decoration theme static InputDecorationTheme _inputDecorationTheme(ColorScheme scheme) => InputDecorationTheme( filled: true, fillColor: scheme.surfaceContainerHighest.withValues( alpha: 0.3, - ), // Added transparency + ), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), // 12 -> 16 + borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), // 12 -> 16 + borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), // 12 -> 16 + borderRadius: BorderRadius.circular(16), borderSide: BorderSide(color: scheme.primary, width: 2), ), errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), // 12 -> 16 + borderRadius: BorderRadius.circular(16), borderSide: BorderSide(color: scheme.error, width: 1), ), contentPadding: const EdgeInsets.symmetric( horizontal: 20, vertical: 16, - ), // consistent padding + ), ); static ListTileThemeData _listTileTheme(ColorScheme scheme) => ListTileThemeData( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - ), // 12 -> 16 + ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), ); @@ -193,7 +188,6 @@ class AppTheme { surfaceTintColor: scheme.surfaceTint, ); - /// Navigation bar theme static NavigationBarThemeData _navigationBarTheme( ColorScheme scheme, { bool isAmoled = false, @@ -213,7 +207,6 @@ class AppTheme { contentTextStyle: TextStyle(color: scheme.onInverseSurface), ); - /// Progress indicator theme static ProgressIndicatorThemeData _progressIndicatorTheme( ColorScheme scheme, ) => ProgressIndicatorThemeData( @@ -243,7 +236,6 @@ class AppTheme { }), ); - /// Chip theme static ChipThemeData _chipTheme(ColorScheme scheme) => ChipThemeData( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), backgroundColor: scheme.surfaceContainerLow, diff --git a/lib/theme/dynamic_color_wrapper.dart b/lib/theme/dynamic_color_wrapper.dart index 6ee84b86..77237502 100644 --- a/lib/theme/dynamic_color_wrapper.dart +++ b/lib/theme/dynamic_color_wrapper.dart @@ -4,7 +4,6 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:spotiflac_android/providers/theme_provider.dart'; import 'package:spotiflac_android/theme/app_theme.dart'; -/// Wrapper widget that provides dynamic color support from device wallpaper class DynamicColorWrapper extends ConsumerWidget { final Widget Function(ThemeData light, ThemeData dark, ThemeMode mode) builder; @@ -23,7 +22,6 @@ class DynamicColorWrapper extends ConsumerWidget { ColorScheme darkScheme; if (themeSettings.useDynamicColor && lightDynamic != null && darkDynamic != null) { - // Use dynamic colors from wallpaper (Android 12+) lightScheme = lightDynamic; darkScheme = darkDynamic; } else { @@ -38,7 +36,6 @@ class DynamicColorWrapper extends ConsumerWidget { ); } - // Apply AMOLED mode if enabled (pure black background) if (themeSettings.useAmoled) { darkScheme = _applyAmoledColors(darkScheme); } diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index 977a31b6..3b8bac12 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -22,7 +22,6 @@ class BuiltInService { }); } -/// Default quality options for built-in services /// Default quality options for each built-in service const _builtInServices = [ BuiltInService( @@ -98,7 +97,6 @@ const _builtInServices = [ ), ]; -/// A reusable widget for selecting download service (built-in + extensions) class DownloadServicePicker extends ConsumerStatefulWidget { final String? trackName; final String? artistName; From 4a492aeefc2e3a732dd13a05bba9fc63f6bed5e7 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 18 Mar 2026 01:23:55 +0700 Subject: [PATCH 131/145] chore: bump version to 3.8.8+114 --- lib/constants/app_info.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 486601f3..b57c894b 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -3,8 +3,8 @@ import 'package:flutter/foundation.dart'; /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.8.7'; - static const String buildNumber = '113'; + static const String version = '3.8.8'; + static const String buildNumber = '114'; static const String fullVersion = '$version+$buildNumber'; /// Shows "Internal" in debug builds, actual version in release. diff --git a/pubspec.yaml b/pubspec.yaml index 1aabd893..1bf2b50f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Deezer publish_to: "none" -version: 3.8.7+113 +version: 3.8.8+114 environment: sdk: ^3.10.0 From acb1d957d3cafcc9da511fbc6a26eb0989f08e84 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 18 Mar 2026 17:08:16 +0700 Subject: [PATCH 132/145] feat: add McNuggets Jimmy as supporter --- lib/screens/settings/donate_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index a9fbaff5..931012c4 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -164,7 +164,7 @@ class _RecentDonorsCard extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - const donorNames = ['micahRichie', 'a fan', 'mc nuggets jimmy', 'CJBGR', 'zcc09']; + const donorNames = ['McNuggets Jimmy', 'micahRichie', 'a fan', 'CJBGR', 'zcc09']; // Match SettingsGroup color logic final cardColor = isDark @@ -480,7 +480,7 @@ int _cr(String v) { } // Highlighted supporters (hashes of names). -const _cv = {1211573191, 1003219236}; +const _cv = {1211573191, 1003219236, 560908930}; class _SupporterChip extends StatelessWidget { final String name; From 0a892011de2b2372e7f49d7c2b734a909f68d347 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 18 Mar 2026 17:11:17 +0700 Subject: [PATCH 133/145] refactor: migrate lyrics providers to Paxsenix endpoints --- go_backend/lyrics.go | 171 +++++++++++++++++----------- go_backend/lyrics_apple.go | 191 +++++++++++--------------------- go_backend/lyrics_musixmatch.go | 181 +++++++++++++++--------------- go_backend/lyrics_netease.go | 15 +-- go_backend/lyrics_qqmusic.go | 134 +++++++--------------- 5 files changed, 307 insertions(+), 385 deletions(-) diff --git a/go_backend/lyrics.go b/go_backend/lyrics.go index 1a6c4771..60d3aa85 100644 --- a/go_backend/lyrics.go +++ b/go_backend/lyrics.go @@ -3,6 +3,7 @@ package gobackend import ( "encoding/json" "fmt" + "io" "math" "net/http" "net/url" @@ -121,12 +122,12 @@ func GetLyricsProviderOrder() []string { // GetAvailableLyricsProviders returns metadata about all available providers. func GetAvailableLyricsProviders() []map[string]interface{} { return []map[string]interface{}{ - {"id": LyricsProviderSpotifyAPI, "name": "Spotify Lyrics API", "has_proxy_dependency": true, "description": "Spotify-sourced synced lyrics via community API"}, + {"id": LyricsProviderSpotifyAPI, "name": "Spotify Lyrics API", "has_proxy_dependency": true, "description": "Spotify-sourced lyrics via Paxsenix"}, {"id": LyricsProviderLRCLIB, "name": "LRCLIB", "has_proxy_dependency": false, "description": "Open-source synced lyrics database"}, - {"id": LyricsProviderNetease, "name": "Netease", "has_proxy_dependency": false, "description": "NetEase Cloud Music (good for Asian songs)"}, - {"id": LyricsProviderMusixmatch, "name": "Musixmatch", "has_proxy_dependency": true, "description": "Largest lyrics database (multi-language)"}, - {"id": LyricsProviderAppleMusic, "name": "Apple Music", "has_proxy_dependency": true, "description": "Word-by-word synced lyrics"}, - {"id": LyricsProviderQQMusic, "name": "QQ Music", "has_proxy_dependency": true, "description": "QQ Music lyrics (good for Chinese songs)"}, + {"id": LyricsProviderNetease, "name": "Netease", "has_proxy_dependency": true, "description": "NetEase Cloud Music lyrics via Paxsenix"}, + {"id": LyricsProviderMusixmatch, "name": "Musixmatch", "has_proxy_dependency": true, "description": "Musixmatch lyrics via Paxsenix"}, + {"id": LyricsProviderAppleMusic, "name": "Apple Music", "has_proxy_dependency": true, "description": "Apple Music synced lyrics via Paxsenix"}, + {"id": LyricsProviderQQMusic, "name": "QQ Music", "has_proxy_dependency": true, "description": "QQ Music lyrics via Paxsenix"}, } } @@ -431,6 +432,99 @@ func parseSpotifyRetryAfter(retryAfter string, now time.Time) time.Time { return now.Add(10 * time.Minute) } +func buildSpotifyLyricsResponse(lines []LyricsLine, syncType, plainLyrics string) (*LyricsResponse, error) { + if len(lines) == 0 { + return nil, fmt.Errorf("Spotify Lyrics API returned empty lines") + } + if syncType == "" { + if len(lines) > 0 && lines[0].StartTimeMs > 0 { + syncType = "LINE_SYNCED" + } else { + syncType = "UNSYNCED" + } + } + return &LyricsResponse{ + Lines: lines, + SyncType: syncType, + Instrumental: false, + PlainLyrics: plainLyrics, + Provider: "Spotify Lyrics API", + Source: "Spotify Lyrics API", + }, nil +} + +func plainLyricsFromTimedLines(lines []LyricsLine) string { + parts := make([]string, 0, len(lines)) + for _, line := range lines { + words := strings.TrimSpace(line.Words) + if words == "" { + continue + } + parts = append(parts, words) + } + return strings.Join(parts, "\n") +} + +func parseSpotifyLyricsResponseBody(body []byte) (*LyricsResponse, error) { + var lrcPayload string + if err := json.Unmarshal(body, &lrcPayload); err == nil { + trimmed := strings.TrimSpace(lrcPayload) + if trimmed == "" { + return nil, fmt.Errorf("Spotify Lyrics API returned empty payload") + } + + lines := parseSyncedLyrics(trimmed) + if len(lines) > 0 { + return buildSpotifyLyricsResponse(lines, "LINE_SYNCED", plainLyricsFromTimedLines(lines)) + } + + plainLines := plainTextLyricsLines(trimmed) + return buildSpotifyLyricsResponse(plainLines, "UNSYNCED", trimmed) + } + + var apiResp SpotifyLyricsAPIResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + return nil, fmt.Errorf("failed to parse Spotify Lyrics API response: %w", err) + } + + if apiResp.Error { + msg := strings.TrimSpace(apiResp.Message) + if msg == "" { + msg = "Spotify Lyrics API returned error" + } + return nil, fmt.Errorf("%s", msg) + } + + lines := make([]LyricsLine, 0, len(apiResp.Lines)) + for _, line := range apiResp.Lines { + words := strings.TrimSpace(line.Words) + if words == "" { + continue + } + startMs := parseSpotifyLyricsTimeTagToMs(line.TimeTag) + lines = append(lines, LyricsLine{ + StartTimeMs: startMs, + Words: words, + EndTimeMs: 0, + }) + } + + for i := 0; i < len(lines)-1; i++ { + nextStart := lines[i+1].StartTimeMs + if nextStart > lines[i].StartTimeMs { + lines[i].EndTimeMs = nextStart + } + } + if len(lines) > 0 { + last := len(lines) - 1 + if lines[last].EndTimeMs == 0 { + lines[last].EndTimeMs = lines[last].StartTimeMs + 5000 + } + } + + return buildSpotifyLyricsResponse(lines, apiResp.SyncType, plainLyricsFromTimedLines(lines)) +} + func (c *LyricsClient) FetchLyricsFromSpotifyAPI(spotifyID string) (*LyricsResponse, error) { now := time.Now() if limitedUntil := getSpotifyLyricsRateLimitUntil(); limitedUntil.After(now) { @@ -449,7 +543,7 @@ func (c *LyricsClient) FetchLyricsFromSpotifyAPI(spotifyID string) (*LyricsRespo spotifyID = parsed.ID } - apiURL := fmt.Sprintf("https://spotify-lyrics-api-pi.vercel.app/?trackid=%s&format=lrc", url.QueryEscape(spotifyID)) + apiURL := fmt.Sprintf("https://lyrics.paxsenix.org/spotify/lyrics?id=%s", url.QueryEscape(spotifyID)) req, err := http.NewRequest("GET", apiURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -462,13 +556,18 @@ func (c *LyricsClient) FetchLyricsFromSpotifyAPI(spotifyID string) (*LyricsRespo } defer resp.Body.Close() + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read Spotify Lyrics API response: %w", err) + } + if resp.StatusCode != 200 { if resp.StatusCode == http.StatusTooManyRequests { retryUntil := parseSpotifyRetryAfter(resp.Header.Get("Retry-After"), now) setSpotifyLyricsRateLimitUntil(retryUntil) } var payload map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&payload); err == nil { + if err := json.Unmarshal(bodyBytes, &payload); err == nil { if msg, ok := payload["message"].(string); ok && strings.TrimSpace(msg) != "" { return nil, fmt.Errorf("Spotify Lyrics API returned status %d: %s", resp.StatusCode, strings.TrimSpace(msg)) } @@ -479,63 +578,7 @@ func (c *LyricsClient) FetchLyricsFromSpotifyAPI(spotifyID string) (*LyricsRespo return nil, fmt.Errorf("Spotify Lyrics API returned status %d", resp.StatusCode) } - var apiResp SpotifyLyricsAPIResponse - if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { - return nil, fmt.Errorf("failed to parse Spotify Lyrics API response: %w", err) - } - - if apiResp.Error { - msg := strings.TrimSpace(apiResp.Message) - if msg == "" { - msg = "Spotify Lyrics API returned error" - } - return nil, fmt.Errorf("%s", msg) - } - - result := &LyricsResponse{ - Lines: make([]LyricsLine, 0, len(apiResp.Lines)), - SyncType: apiResp.SyncType, - Instrumental: false, - PlainLyrics: "", - Provider: "Spotify Lyrics API", - Source: "Spotify Lyrics API", - } - - for _, line := range apiResp.Lines { - words := strings.TrimSpace(line.Words) - if words == "" { - continue - } - startMs := parseSpotifyLyricsTimeTagToMs(line.TimeTag) - result.Lines = append(result.Lines, LyricsLine{ - StartTimeMs: startMs, - Words: words, - EndTimeMs: 0, - }) - } - - if len(result.Lines) > 1 { - for i := 0; i < len(result.Lines)-1; i++ { - nextStart := result.Lines[i+1].StartTimeMs - if nextStart > result.Lines[i].StartTimeMs { - result.Lines[i].EndTimeMs = nextStart - } - } - last := len(result.Lines) - 1 - if result.Lines[last].EndTimeMs == 0 { - result.Lines[last].EndTimeMs = result.Lines[last].StartTimeMs + 5000 - } - } - - if len(result.Lines) == 0 { - return nil, fmt.Errorf("Spotify Lyrics API returned empty lines") - } - - if result.SyncType == "" { - result.SyncType = "LINE_SYNCED" - } - - return result, nil + return parseSpotifyLyricsResponseBody(bodyBytes) } func (c *LyricsClient) findBestMatch(results []LRCLibResponse, targetDurationSec float64) *LRCLibResponse { diff --git a/go_backend/lyrics_apple.go b/go_backend/lyrics_apple.go index 538b0b89..8fe350ad 100644 --- a/go_backend/lyrics_apple.go +++ b/go_backend/lyrics_apple.go @@ -4,121 +4,25 @@ import ( "encoding/json" "fmt" "io" + "math" "net/http" "net/url" - "regexp" "strings" - "sync" "time" ) // AppleMusicClient fetches lyrics from Apple Music. -// Uses a scraped JWT token for search and a proxy for lyrics. +// Uses Paxsenix endpoints for search and lyrics. type AppleMusicClient struct { httpClient *http.Client } -// Apple Music token manager — singleton with mutex for thread safety -type appleTokenManager struct { - mu sync.Mutex - token string -} - -var globalAppleTokenManager = &appleTokenManager{} - -func (m *appleTokenManager) getToken(client *http.Client) (string, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.token != "" { - return m.token, nil - } - - // Step 1: Fetch the Apple Music beta page - req, err := http.NewRequest("GET", "https://beta.music.apple.com", nil) - if err != nil { - return "", fmt.Errorf("failed to create request: %w", err) - } - req.Header.Set("User-Agent", getRandomUserAgent()) - - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("failed to fetch Apple Music page: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read Apple Music page: %w", err) - } - - // Step 2: Find the index JS file URL - indexJsRegex := regexp.MustCompile(`/assets/index~[^/]+\.js`) - match := indexJsRegex.Find(body) - if match == nil { - return "", fmt.Errorf("could not find index JS script URL on Apple Music page") - } - - indexJsURL := "https://beta.music.apple.com" + string(match) - - // Step 3: Fetch the JS file - jsReq, err := http.NewRequest("GET", indexJsURL, nil) - if err != nil { - return "", fmt.Errorf("failed to create JS request: %w", err) - } - jsReq.Header.Set("User-Agent", getRandomUserAgent()) - - jsResp, err := client.Do(jsReq) - if err != nil { - return "", fmt.Errorf("failed to fetch Apple Music JS: %w", err) - } - defer jsResp.Body.Close() - - jsBody, err := io.ReadAll(jsResp.Body) - if err != nil { - return "", fmt.Errorf("failed to read Apple Music JS: %w", err) - } - - // Step 4: Extract JWT token (starts with eyJh) - tokenRegex := regexp.MustCompile(`eyJh[^"]*`) - tokenMatch := tokenRegex.Find(jsBody) - if tokenMatch == nil { - return "", fmt.Errorf("could not find JWT token in Apple Music JS") - } - - m.token = string(tokenMatch) - GoLog("[AppleMusic] Token obtained successfully (length: %d)\n", len(m.token)) - return m.token, nil -} - -func (m *appleTokenManager) clearToken() { - m.mu.Lock() - defer m.mu.Unlock() - m.token = "" -} - -type appleMusicSearchResponse struct { - Results struct { - Songs *struct { - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - } `json:"data"` - } `json:"songs"` - } `json:"results"` - Resources *struct { - Songs map[string]struct { - Attributes struct { - Name string `json:"name"` - ArtistName string `json:"artistName"` - AlbumName string `json:"albumName"` - URL string `json:"url"` - Artwork struct { - URL string `json:"url"` - } `json:"artwork"` - } `json:"attributes"` - } `json:"songs"` - } `json:"resources"` +type appleMusicSearchResult struct { + ID string `json:"id"` + SongName string `json:"songName"` + ArtistName string `json:"artistName"` + AlbumName string `json:"albumName"` + Duration int `json:"duration"` } // PaxResponse represents the lyrics proxy response for word-by-word / line lyrics @@ -149,32 +53,71 @@ func NewAppleMusicClient() *AppleMusicClient { } } +func selectBestAppleMusicSearchResult(results []appleMusicSearchResult, trackName, artistName string, durationSec float64) *appleMusicSearchResult { + if len(results) == 0 { + return nil + } + + normalizedTrack := strings.ToLower(strings.TrimSpace(simplifyTrackName(trackName))) + normalizedArtist := strings.ToLower(strings.TrimSpace(normalizeArtistName(artistName))) + if normalizedArtist == "" { + normalizedArtist = strings.ToLower(strings.TrimSpace(artistName)) + } + + bestIndex := 0 + bestScore := -1 + for i := range results { + result := &results[i] + score := 0 + + candidateTrack := strings.ToLower(strings.TrimSpace(simplifyTrackName(result.SongName))) + candidateArtist := strings.ToLower(strings.TrimSpace(normalizeArtistName(result.ArtistName))) + + switch { + case candidateTrack == normalizedTrack: + score += 50 + case strings.Contains(candidateTrack, normalizedTrack) || strings.Contains(normalizedTrack, candidateTrack): + score += 25 + } + + switch { + case candidateArtist == normalizedArtist: + score += 60 + case strings.Contains(candidateArtist, normalizedArtist) || strings.Contains(normalizedArtist, candidateArtist): + score += 30 + } + + if durationSec > 0 && result.Duration > 0 { + diff := math.Abs(float64(result.Duration)/1000.0 - durationSec) + if diff <= durationToleranceSec { + score += 20 + } + } + + if score > bestScore { + bestScore = score + bestIndex = i + } + } + + return &results[bestIndex] +} + // SearchSong searches for a song on Apple Music and returns its ID. -func (c *AppleMusicClient) SearchSong(trackName, artistName string) (string, error) { +func (c *AppleMusicClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) { query := trackName + " " + artistName if strings.TrimSpace(query) == "" { return "", fmt.Errorf("empty search query") } - token, err := globalAppleTokenManager.getToken(c.httpClient) - if err != nil { - return "", fmt.Errorf("apple music token error: %w", err) - } - encodedQuery := url.QueryEscape(query) - searchURL := fmt.Sprintf( - "https://amp-api.music.apple.com/v1/catalog/us/search?term=%s&types=songs&limit=5&l=en-US&platform=web&format[resources]=map&include[songs]=artists&extend=artistUrl", - encodedQuery, - ) + searchURL := fmt.Sprintf("https://lyrics.paxsenix.org/apple-music/search?q=%s", encodedQuery) req, err := http.NewRequest("GET", searchURL, nil) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Origin", "https://music.apple.com") - req.Header.Set("Referer", "https://music.apple.com/") req.Header.Set("User-Agent", getRandomUserAgent()) req.Header.Set("Accept", "application/json") @@ -184,25 +127,21 @@ func (c *AppleMusicClient) SearchSong(trackName, artistName string) (string, err } defer resp.Body.Close() - if resp.StatusCode == 401 { - globalAppleTokenManager.clearToken() - return "", fmt.Errorf("apple music token expired") - } - if resp.StatusCode != 200 { return "", fmt.Errorf("apple music search returned HTTP %d", resp.StatusCode) } - var searchResp appleMusicSearchResponse + var searchResp []appleMusicSearchResult if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { return "", fmt.Errorf("failed to decode apple music response: %w", err) } - if searchResp.Results.Songs == nil || len(searchResp.Results.Songs.Data) == 0 { + best := selectBestAppleMusicSearchResult(searchResp, trackName, artistName, durationSec) + if best == nil || strings.TrimSpace(best.ID) == "" { return "", fmt.Errorf("no songs found on apple music") } - return searchResp.Results.Songs.Data[0].ID, nil + return strings.TrimSpace(best.ID), nil } // FetchLyricsByID fetches lyrics from the paxsenix proxy using Apple Music song ID. @@ -320,7 +259,7 @@ func (c *AppleMusicClient) FetchLyrics( durationSec float64, multiPersonWordByWord bool, ) (*LyricsResponse, error) { - songID, err := c.SearchSong(trackName, artistName) + songID, err := c.SearchSong(trackName, artistName, durationSec) if err != nil { return nil, err } diff --git a/go_backend/lyrics_musixmatch.go b/go_backend/lyrics_musixmatch.go index f962a110..37dfbea7 100644 --- a/go_backend/lyrics_musixmatch.go +++ b/go_backend/lyrics_musixmatch.go @@ -3,6 +3,8 @@ package gobackend import ( "encoding/json" "fmt" + "io" + "math" "net/http" "net/url" "strings" @@ -45,100 +47,105 @@ type musixmatchLyricsResponse struct { func NewMusixmatchClient() *MusixmatchClient { return &MusixmatchClient{ httpClient: NewMetadataHTTPClient(15 * time.Second), - baseURL: "http://158.180.60.95", + baseURL: "https://lyrics.paxsenix.org/musixmatch/lyrics", } } -// searchAndGetLyrics searches for a song and retrieves its lyrics in one call. -// The Musixmatch proxy returns both search result and lyrics in a single response. -func (c *MusixmatchClient) searchAndGetLyrics(trackName, artistName string) (*musixmatchSearchResponse, error) { +func (c *MusixmatchClient) fetchLyricsPayload(trackName, artistName string, durationSec float64, lyricsType, language string) (string, error) { if strings.TrimSpace(trackName) == "" || strings.TrimSpace(artistName) == "" { - return nil, fmt.Errorf("empty track or artist name") + return "", fmt.Errorf("empty track or artist name") } - encodedArtist := url.QueryEscape(artistName) - encodedTrack := url.QueryEscape(trackName) - - fullURL := fmt.Sprintf("%s/v2/full?artist=%s&track=%s", c.baseURL, encodedArtist, encodedTrack) + params := url.Values{} + params.Set("t", trackName) + params.Set("a", artistName) + params.Set("type", lyricsType) + params.Set("format", "lrc") + if durationSec > 0 { + params.Set("d", fmt.Sprintf("%d", int(math.Round(durationSec)))) + } + if strings.TrimSpace(language) != "" { + params.Set("l", strings.ToLower(strings.TrimSpace(language))) + } + fullURL := c.baseURL + "?" + params.Encode() req, err := http.NewRequest("GET", fullURL, nil) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + return "", fmt.Errorf("failed to create request: %w", err) } + req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", getRandomUserAgent()) resp, err := c.httpClient.Do(req) if err != nil { - return nil, fmt.Errorf("musixmatch search failed: %w", err) + return "", fmt.Errorf("musixmatch request failed: %w", err) } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read musixmatch response: %w", err) + } + if resp.StatusCode != 200 { - return nil, fmt.Errorf("musixmatch proxy returned HTTP %d", resp.StatusCode) + trimmed := strings.TrimSpace(string(body)) + if errMsg, isErrorPayload := detectLyricsErrorPayload(trimmed); isErrorPayload { + return "", fmt.Errorf("musixmatch proxy returned HTTP %d: %s", resp.StatusCode, errMsg) + } + return "", fmt.Errorf("musixmatch proxy returned HTTP %d", resp.StatusCode) } - var result musixmatchSearchResponse - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("failed to decode musixmatch response: %w", err) + var lrcPayload string + if err := json.Unmarshal(body, &lrcPayload); err == nil { + lrcPayload = strings.TrimSpace(lrcPayload) + if lrcPayload == "" { + return "", fmt.Errorf("empty musixmatch lyrics payload") + } + return lrcPayload, nil } - return &result, nil + trimmed := strings.TrimSpace(string(body)) + if errMsg, isErrorPayload := detectLyricsErrorPayload(trimmed); isErrorPayload { + return "", fmt.Errorf("%s", errMsg) + } + if trimmed != "" && !strings.HasPrefix(trimmed, "{") { + return trimmed, nil + } + return "", fmt.Errorf("failed to decode musixmatch response") } // FetchLyricsInLanguage retrieves lyrics from Musixmatch for a specific language code. -func (c *MusixmatchClient) FetchLyricsInLanguage(songID int64, language string) (*LyricsResponse, error) { +func (c *MusixmatchClient) FetchLyricsInLanguage(trackName, artistName string, durationSec float64, language string) (*LyricsResponse, error) { lang := strings.ToLower(strings.TrimSpace(language)) - if songID <= 0 || lang == "" { - return nil, fmt.Errorf("invalid song id or language") + if lang == "" { + return nil, fmt.Errorf("invalid language") } - fullURL := fmt.Sprintf("%s/v2/full?id=%d&lang=%s", c.baseURL, songID, url.QueryEscape(lang)) - - req, err := http.NewRequest("GET", fullURL, nil) + lrcText, err := c.fetchLyricsPayload(trackName, artistName, durationSec, "translate", lang) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - req.Header.Set("User-Agent", getRandomUserAgent()) - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("musixmatch language fetch failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("musixmatch language endpoint returned HTTP %d", resp.StatusCode) + return nil, err } - var result musixmatchSearchResponse - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("failed to decode musixmatch language response: %w", err) + lines := parseSyncedLyrics(lrcText) + if len(lines) > 0 { + return &LyricsResponse{ + Lines: lines, + SyncType: "LINE_SYNCED", + PlainLyrics: plainLyricsFromTimedLines(lines), + Provider: "Musixmatch", + Source: fmt.Sprintf("Musixmatch (%s)", lang), + }, nil } - if result.SyncedLyrics != nil && strings.TrimSpace(result.SyncedLyrics.Lyrics) != "" { - lines := parseSyncedLyrics(result.SyncedLyrics.Lyrics) - if len(lines) > 0 { - return &LyricsResponse{ - Lines: lines, - SyncType: "LINE_SYNCED", - Provider: "Musixmatch", - Source: fmt.Sprintf("Musixmatch (%s)", lang), - }, nil - } - } - - if result.UnsyncedLyrics != nil && strings.TrimSpace(result.UnsyncedLyrics.Lyrics) != "" { - lines := plainTextLyricsLines(result.UnsyncedLyrics.Lyrics) - - if len(lines) > 0 { - return &LyricsResponse{ - Lines: lines, - SyncType: "UNSYNCED", - PlainLyrics: result.UnsyncedLyrics.Lyrics, - Provider: "Musixmatch", - Source: fmt.Sprintf("Musixmatch (%s)", lang), - }, nil - } + plainLines := plainTextLyricsLines(lrcText) + if len(plainLines) > 0 { + return &LyricsResponse{ + Lines: plainLines, + SyncType: "UNSYNCED", + PlainLyrics: lrcText, + Provider: "Musixmatch", + Source: fmt.Sprintf("Musixmatch (%s)", lang), + }, nil } return nil, fmt.Errorf("no lyrics found on musixmatch for language %s", lang) @@ -146,43 +153,39 @@ func (c *MusixmatchClient) FetchLyricsInLanguage(songID int64, language string) // FetchLyrics searches Musixmatch and returns parsed LyricsResponse. func (c *MusixmatchClient) FetchLyrics(trackName, artistName string, durationSec float64, preferredLanguage string) (*LyricsResponse, error) { - result, err := c.searchAndGetLyrics(trackName, artistName) - if err != nil { - return nil, err - } - - if preferred := strings.ToLower(strings.TrimSpace(preferredLanguage)); preferred != "" && result.ID > 0 { - localized, localizedErr := c.FetchLyricsInLanguage(result.ID, preferred) + if preferred := strings.ToLower(strings.TrimSpace(preferredLanguage)); preferred != "" { + localized, localizedErr := c.FetchLyricsInLanguage(trackName, artistName, durationSec, preferred) if localizedErr == nil { return localized, nil } GoLog("[Musixmatch] Language override '%s' failed: %v\n", preferred, localizedErr) } - if result.SyncedLyrics != nil && strings.TrimSpace(result.SyncedLyrics.Lyrics) != "" { - lines := parseSyncedLyrics(result.SyncedLyrics.Lyrics) - if len(lines) > 0 { - return &LyricsResponse{ - Lines: lines, - SyncType: "LINE_SYNCED", - Provider: "Musixmatch", - Source: "Musixmatch", - }, nil - } + lrcText, err := c.fetchLyricsPayload(trackName, artistName, durationSec, "word", "") + if err != nil { + return nil, err } - if result.UnsyncedLyrics != nil && strings.TrimSpace(result.UnsyncedLyrics.Lyrics) != "" { - lines := plainTextLyricsLines(result.UnsyncedLyrics.Lyrics) + lines := parseSyncedLyrics(lrcText) + if len(lines) > 0 { + return &LyricsResponse{ + Lines: lines, + SyncType: "LINE_SYNCED", + PlainLyrics: plainLyricsFromTimedLines(lines), + Provider: "Musixmatch", + Source: "Musixmatch", + }, nil + } - if len(lines) > 0 { - return &LyricsResponse{ - Lines: lines, - SyncType: "UNSYNCED", - PlainLyrics: result.UnsyncedLyrics.Lyrics, - Provider: "Musixmatch", - Source: "Musixmatch", - }, nil - } + plainLines := plainTextLyricsLines(lrcText) + if len(plainLines) > 0 { + return &LyricsResponse{ + Lines: plainLines, + SyncType: "UNSYNCED", + PlainLyrics: lrcText, + Provider: "Musixmatch", + Source: "Musixmatch", + }, nil } return nil, fmt.Errorf("no lyrics found on musixmatch") diff --git a/go_backend/lyrics_netease.go b/go_backend/lyrics_netease.go index f6ce6b6c..c6741e19 100644 --- a/go_backend/lyrics_netease.go +++ b/go_backend/lyrics_netease.go @@ -9,8 +9,7 @@ import ( "time" ) -// NeteaseClient fetches lyrics from NetEase Cloud Music (music.163.com). -// This is a direct public API — no proxy dependency. +// NeteaseClient fetches lyrics through Paxsenix's NetEase endpoints. type NeteaseClient struct { httpClient *http.Client } @@ -59,12 +58,9 @@ func (c *NeteaseClient) SearchSong(trackName, artistName string) (int64, error) return 0, fmt.Errorf("empty search query") } - searchURL := "http://music.163.com/api/search/pc" + searchURL := "https://lyrics.paxsenix.org/netease/search" params := url.Values{} - params.Set("s", query) - params.Set("type", "1") - params.Set("limit", "1") - params.Set("offset", "0") + params.Set("q", query) fullURL := searchURL + "?" + params.Encode() @@ -102,12 +98,9 @@ func (c *NeteaseClient) SearchSong(trackName, artistName string) (int64, error) // FetchLyricsByID fetches synced lyrics for a given Netease song ID. func (c *NeteaseClient) FetchLyricsByID(songID int64, includeTranslation, includeRomanization bool) (string, error) { - lyricsURL := "http://music.163.com/api/song/lyric" + lyricsURL := "https://lyrics.paxsenix.org/netease/lyrics" params := url.Values{} params.Set("id", fmt.Sprintf("%d", songID)) - params.Set("lv", "1") - params.Set("tv", "1") - params.Set("rv", "1") fullURL := lyricsURL + "?" + params.Encode() diff --git a/go_backend/lyrics_qqmusic.go b/go_backend/lyrics_qqmusic.go index dde3631d..95461031 100644 --- a/go_backend/lyrics_qqmusic.go +++ b/go_backend/lyrics_qqmusic.go @@ -1,45 +1,31 @@ package gobackend import ( - "bytes" "encoding/json" "fmt" "io" + "math" "net/http" - "net/url" "strings" "time" ) // QQMusicClient fetches lyrics from QQ Music. -// Search uses public QQ Music API, lyrics use the paxsenix proxy. +// Uses Paxsenix metadata lookup for lyrics. type QQMusicClient struct { httpClient *http.Client } -type qqMusicSearchResponse struct { - Data struct { - Song struct { - List []struct { - Title string `json:"title"` - Singer []struct { - Name string `json:"name"` - } `json:"singer"` - Album struct { - Name string `json:"name"` - } `json:"album"` - ID int64 `json:"id"` - } `json:"list"` - } `json:"song"` - } `json:"data"` +type qqLyricsMetadataRequest struct { + Artist []string `json:"artist"` + Album string `json:"album,omitempty"` + SongID int64 `json:"songid,omitempty"` + Title string `json:"title"` + Duration int64 `json:"duration,omitempty"` } -// QQ Music lyrics request payload for paxsenix proxy -type qqLyricsPayload struct { - Artist []string `json:"artist"` - Album string `json:"album"` - ID int64 `json:"id"` - Title string `json:"title"` +type qqLyricsMetadataResponse struct { + Lyrics []paxLyrics `json:"lyrics"` } func NewQQMusicClient() *QQMusicClient { @@ -48,79 +34,29 @@ func NewQQMusicClient() *QQMusicClient { } } -// searchSong searches QQ Music and returns the song info needed for lyrics fetch. -func (c *QQMusicClient) searchSong(trackName, artistName string) (*qqLyricsPayload, error) { - query := trackName + " " + artistName - if strings.TrimSpace(query) == "" { - return nil, fmt.Errorf("empty search query") +// fetchLyricsByMetadata asks Paxsenix to resolve and return QQ lyrics using track metadata. +func (c *QQMusicClient) fetchLyricsByMetadata(trackName, artistName string, durationSec float64) (string, error) { + payload := qqLyricsMetadataRequest{ + Artist: []string{artistName}, + Title: trackName, + } + if durationSec > 0 { + payload.Duration = int64(math.Round(durationSec)) } - searchURL := "https://c.y.qq.com/soso/fcgi-bin/client_search_cp" - params := url.Values{} - params.Set("format", "json") - params.Set("inCharset", "utf8") - params.Set("outCharset", "utf8") - params.Set("platform", "yqq.json") - params.Set("new_json", "1") - params.Set("w", query) - - fullURL := searchURL + "?" + params.Encode() - - req, err := http.NewRequest("GET", fullURL, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", getRandomUserAgent()) - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("qqmusic search failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("qqmusic search returned HTTP %d", resp.StatusCode) - } - - var searchResp qqMusicSearchResponse - if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { - return nil, fmt.Errorf("failed to decode qqmusic response: %w", err) - } - - if len(searchResp.Data.Song.List) == 0 { - return nil, fmt.Errorf("no songs found on qqmusic") - } - - song := searchResp.Data.Song.List[0] - - var artists []string - for _, singer := range song.Singer { - artists = append(artists, singer.Name) - } - - return &qqLyricsPayload{ - Artist: artists, - Album: song.Album.Name, - ID: song.ID, - Title: song.Title, - }, nil -} - -// fetchLyricsByPayload fetches lyrics from the paxsenix proxy using QQ Music song info. -func (c *QQMusicClient) fetchLyricsByPayload(payload *qqLyricsPayload) (string, error) { - lyricsURL := "https://paxsenix.alwaysdata.net/getQQLyrics.php" + lyricsURL := "https://lyrics.paxsenix.org/qq/lyrics-metadata" payloadBytes, err := json.Marshal(payload) if err != nil { return "", fmt.Errorf("failed to marshal payload: %w", err) } - req, err := http.NewRequest("POST", lyricsURL, bytes.NewReader(payloadBytes)) + req, err := http.NewRequest("POST", lyricsURL, strings.NewReader(string(payloadBytes))) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", getRandomUserAgent()) resp, err := c.httpClient.Do(req) @@ -146,6 +82,17 @@ func (c *QQMusicClient) fetchLyricsByPayload(payload *qqLyricsPayload) (string, return bodyStr, nil } +func formatQQLyricsMetadataToLRC(rawJSON string, multiPersonWordByWord bool) (string, error) { + var response qqLyricsMetadataResponse + if err := json.Unmarshal([]byte(rawJSON), &response); err != nil { + return "", fmt.Errorf("failed to parse qq metadata lyrics response") + } + if len(response.Lyrics) == 0 { + return "", fmt.Errorf("qq metadata lyrics response was empty") + } + return formatPaxContent("Syllable", response.Lyrics, multiPersonWordByWord), nil +} + // FetchLyrics searches QQ Music and returns parsed LyricsResponse. func (c *QQMusicClient) FetchLyrics( trackName, @@ -153,12 +100,7 @@ func (c *QQMusicClient) FetchLyrics( durationSec float64, multiPersonWordByWord bool, ) (*LyricsResponse, error) { - payload, err := c.searchSong(trackName, artistName) - if err != nil { - return nil, err - } - - rawLyrics, err := c.fetchLyricsByPayload(payload) + rawLyrics, err := c.fetchLyricsByMetadata(trackName, artistName, durationSec) if err != nil { return nil, err } @@ -166,11 +108,13 @@ func (c *QQMusicClient) FetchLyrics( return nil, fmt.Errorf("qqmusic proxy returned non-lyric payload: %s", errMsg) } - // Try to parse as pax format (word-by-word or line) - lrcText, err := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord) + lrcText, err := formatQQLyricsMetadataToLRC(rawLyrics, multiPersonWordByWord) if err != nil { - // If pax parsing fails, try to use as direct LRC text - lrcText = rawLyrics + if fallback, fallbackErr := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord); fallbackErr == nil { + lrcText = fallback + } else { + lrcText = rawLyrics + } } lines := parseSyncedLyrics(lrcText) From 23cab1647122d8225bf153d8a9c93bdcb6b84daf Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 18 Mar 2026 17:46:09 +0700 Subject: [PATCH 134/145] feat: enable Tidal ISRC and metadata search --- go_backend/tidal.go | 134 ++++++++++++++++++++++++-- lib/screens/settings/donate_page.dart | 2 +- 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 15f17b1c..bcbca0f9 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -26,8 +26,14 @@ type TidalDownloader struct { } var ( - globalTidalDownloader *TidalDownloader - tidalDownloaderOnce sync.Once + globalTidalDownloader *TidalDownloader + tidalDownloaderOnce sync.Once + tidalGetTrackSearchPageFunc = func(t *TidalDownloader, query string, limit int) (*tidalPublicTrackSearchResponse, error) { + return t.getTrackSearchPage(query, limit) + } + tidalGetPublicTrackFunc = func(t *TidalDownloader, resourceID string) (*TidalTrack, error) { + return t.getPublicTrack(resourceID) + } ) const ( @@ -758,15 +764,101 @@ func (t *TidalDownloader) GetTrackInfoByID(trackID int64) (*TidalTrack, error) { } func (t *TidalDownloader) SearchTrackByISRC(isrc string) (*TidalTrack, error) { - return nil, fmt.Errorf("tidal ISRC search API disabled: no client credentials mode") + normalizedISRC := strings.ToUpper(strings.TrimSpace(isrc)) + if normalizedISRC == "" { + return nil, fmt.Errorf("empty tidal ISRC") + } + + page, err := tidalGetTrackSearchPageFunc(t, normalizedISRC, 20) + if err != nil { + return nil, err + } + + for i := range page.Items { + if strings.EqualFold(strings.TrimSpace(page.Items[i].ISRC), normalizedISRC) { + return &page.Items[i], nil + } + } + + return nil, fmt.Errorf("no exact tidal ISRC match found for %s", normalizedISRC) } func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, albumName, spotifyISRC string, expectedDuration int) (*TidalTrack, error) { - return nil, fmt.Errorf("tidal metadata search API disabled: no client credentials mode") + queryParts := make([]string, 0, 3) + if trimmed := strings.TrimSpace(trackName); trimmed != "" { + queryParts = append(queryParts, trimmed) + } + if trimmed := strings.TrimSpace(artistName); trimmed != "" { + queryParts = append(queryParts, trimmed) + } + if len(queryParts) == 0 { + return nil, fmt.Errorf("tidal metadata search requires track or artist name") + } + + queries := []string{strings.Join(queryParts, " ")} + if trimmedAlbum := strings.TrimSpace(albumName); trimmedAlbum != "" { + queries = append(queries, strings.Join(append(queryParts, trimmedAlbum), " ")) + } + + req := DownloadRequest{ + TrackName: strings.TrimSpace(trackName), + ArtistName: strings.TrimSpace(artistName), + AlbumName: strings.TrimSpace(albumName), + ISRC: strings.ToUpper(strings.TrimSpace(spotifyISRC)), + DurationMS: expectedDuration * 1000, + } + + seenQueries := make(map[string]struct{}, len(queries)) + for _, query := range queries { + if _, seen := seenQueries[query]; seen { + continue + } + seenQueries[query] = struct{}{} + + page, err := tidalGetTrackSearchPageFunc(t, query, 20) + if err != nil { + return nil, err + } + + var candidates []*TidalTrack + for i := range page.Items { + track := &page.Items[i] + if req.ISRC != "" && !strings.EqualFold(strings.TrimSpace(track.ISRC), req.ISRC) { + continue + } + resolved := resolvedTrackInfo{ + Title: strings.TrimSpace(track.Title), + ArtistName: tidalTrackArtistsDisplay(track), + Duration: track.Duration, + } + if trackMatchesRequest(req, resolved, "Tidal search") { + candidates = append(candidates, track) + } + } + + if len(candidates) == 0 { + continue + } + + if req.AlbumName != "" { + for _, candidate := range candidates { + if titlesMatch(req.AlbumName, candidate.Album.Title) { + return candidate, nil + } + } + } + + return candidates[0], nil + } + + if req.ISRC != "" { + return nil, fmt.Errorf("no tidal metadata match found for exact ISRC %s", req.ISRC) + } + return nil, fmt.Errorf("no tidal metadata match found") } func (t *TidalDownloader) SearchTrackByMetadata(trackName, artistName string) (*TidalTrack, error) { - return nil, fmt.Errorf("tidal metadata search API disabled: no client credentials mode") + return t.SearchTrackByMetadataWithISRC(trackName, artistName, "", "", 0) } func (t *TidalDownloader) SearchTracks(query string, limit int) ([]ExtTrackMetadata, error) { @@ -1847,6 +1939,36 @@ func resolveTidalTrackForRequest(req DownloadRequest, downloader *TidalDownloade } } + if !gotTidalID && req.ISRC != "" { + GoLog("[%s] Trying direct Tidal ISRC search: %s\n", logPrefix, req.ISRC) + directTrack, directErr := downloader.SearchTrackByISRC(req.ISRC) + if directErr == nil && directTrack != nil && directTrack.ID > 0 { + trackID = directTrack.ID + gotTidalID = true + GoLog("[%s] Got Tidal ID %d from direct ISRC search\n", logPrefix, trackID) + } else if directErr != nil { + GoLog("[%s] Direct Tidal ISRC search failed: %v\n", logPrefix, directErr) + } + } + + if !gotTidalID && req.ISRC != "" && req.TrackName != "" && req.ArtistName != "" { + GoLog("[%s] Trying Tidal public metadata search with ISRC\n", logPrefix) + searchTrack, searchErr := downloader.SearchTrackByMetadataWithISRC( + req.TrackName, + req.ArtistName, + req.AlbumName, + req.ISRC, + expectedDurationSec, + ) + if searchErr == nil && searchTrack != nil && searchTrack.ID > 0 { + trackID = searchTrack.ID + gotTidalID = true + GoLog("[%s] Got Tidal ID %d from public metadata search\n", logPrefix, trackID) + } else if searchErr != nil { + GoLog("[%s] Tidal public metadata search failed: %v\n", logPrefix, searchErr) + } + } + if !gotTidalID && (req.SpotifyID != "" || req.DeezerID != "") { GoLog("[%s] Trying SongLink for Tidal ID...\n", logPrefix) @@ -1912,7 +2034,7 @@ func resolveTidalTrackForRequest(req DownloadRequest, downloader *TidalDownloade } // Verify the resolved track matches the request. - actualTrack, fetchErr := downloader.getPublicTrack(strconv.FormatInt(trackID, 10)) + actualTrack, fetchErr := tidalGetPublicTrackFunc(downloader, strconv.FormatInt(trackID, 10)) if fetchErr != nil { GoLog("[%s] Warning: could not fetch Tidal track %d for verification: %v\n", logPrefix, trackID, fetchErr) // Continue without verification — better than failing entirely. diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index 931012c4..97f515ca 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -164,7 +164,7 @@ class _RecentDonorsCard extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - const donorNames = ['McNuggets Jimmy', 'micahRichie', 'a fan', 'CJBGR', 'zcc09']; + const donorNames = ['McNuggets Jimmy', 'zcc09', 'micahRichie', 'a fan', 'CJBGR']; // Match SettingsGroup color logic final cardColor = isDark From 2df8fd6282ef3f58cf416ee7924658c4c3dce0a9 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 22 Mar 2026 22:42:33 +0700 Subject: [PATCH 135/145] feat: add normalizeLooseArtistName with diacritic folding for resilient artist matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Unicode NFD decomposition to strip combining marks so variants like "Özkent" and "Ozkent" are treated as equivalent. Apply the new helper in both tidal.go and qobuz.go artistsMatch functions. --- go_backend/qobuz.go | 4 ++-- go_backend/tidal.go | 4 ++-- go_backend/title_match_utils.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 8a87cd28..2a4d35c8 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -479,8 +479,8 @@ func parseQobuzURL(input string) (string, string, error) { } func qobuzArtistsMatch(expectedArtist, foundArtist string) bool { - normExpected := strings.ToLower(strings.TrimSpace(expectedArtist)) - normFound := strings.ToLower(strings.TrimSpace(foundArtist)) + normExpected := normalizeLooseArtistName(expectedArtist) + normFound := normalizeLooseArtistName(foundArtist) if normExpected == normFound { return true diff --git a/go_backend/tidal.go b/go_backend/tidal.go index bcbca0f9..ea286f22 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1629,8 +1629,8 @@ type TidalDownloadResult struct { } func artistsMatch(spotifyArtist, tidalArtist string) bool { - normSpotify := strings.ToLower(strings.TrimSpace(spotifyArtist)) - normTidal := strings.ToLower(strings.TrimSpace(tidalArtist)) + normSpotify := normalizeLooseArtistName(spotifyArtist) + normTidal := normalizeLooseArtistName(tidalArtist) if normSpotify == normTidal { return true diff --git a/go_backend/title_match_utils.go b/go_backend/title_match_utils.go index 22b0eebd..cecf462d 100644 --- a/go_backend/title_match_utils.go +++ b/go_backend/title_match_utils.go @@ -3,6 +3,8 @@ package gobackend import ( "strings" "unicode" + + "golang.org/x/text/unicode/norm" ) // normalizeLooseTitle collapses separators/punctuation so titles like @@ -33,6 +35,37 @@ func normalizeLooseTitle(title string) string { return strings.Join(strings.Fields(b.String()), " ") } +// normalizeLooseArtistName folds diacritics and common separators so artist +// verification is resilient to variants like "Özkent" vs "Ozkent". +func normalizeLooseArtistName(name string) string { + trimmed := strings.TrimSpace(strings.ToLower(name)) + if trimmed == "" { + return "" + } + + decomposed := norm.NFD.String(trimmed) + + var b strings.Builder + b.Grow(len(decomposed)) + + for _, r := range decomposed { + switch { + case unicode.Is(unicode.Mn, r), unicode.Is(unicode.Mc, r), unicode.Is(unicode.Me, r): + continue + case unicode.IsLetter(r), unicode.IsNumber(r): + b.WriteRune(r) + case unicode.IsSpace(r): + b.WriteByte(' ') + case r == '/', r == '\\', r == '_', r == '-', r == '|', r == '.', r == '&', r == '+': + b.WriteByte(' ') + default: + // Drop remaining punctuation/symbols for loose artist matching. + } + } + + return strings.Join(strings.Fields(b.String()), " ") +} + func hasAlphaNumericRunes(value string) bool { for _, r := range value { if unicode.IsLetter(r) || unicode.IsNumber(r) { From aca0bbb819b281f77775b1e41270b5f92f4be380 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 22 Mar 2026 22:42:50 +0700 Subject: [PATCH 136/145] chore: remove security_hardening_test.go Tests for sanitizeSensitiveLogText, validateExtensionAuthURL, validateDomain, and buildStoreExtensionDestPath are no longer maintained alongside the main source and have been removed. --- go_backend/security_hardening_test.go | 80 --------------------------- 1 file changed, 80 deletions(-) delete mode 100644 go_backend/security_hardening_test.go diff --git a/go_backend/security_hardening_test.go b/go_backend/security_hardening_test.go deleted file mode 100644 index 559ccbd8..00000000 --- a/go_backend/security_hardening_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package gobackend - -import ( - "path/filepath" - "strings" - "testing" -) - -func TestSanitizeSensitiveLogText(t *testing.T) { - input := "access_token=abc123 Authorization:Bearer xyz456 https://api.example.com/cb?refresh_token=zzz" - redacted := sanitizeSensitiveLogText(input) - - if strings.Contains(redacted, "abc123") || strings.Contains(redacted, "xyz456") || strings.Contains(redacted, "zzz") { - t.Fatalf("expected sensitive values to be redacted, got: %s", redacted) - } - if !strings.Contains(redacted, "[REDACTED]") { - t.Fatalf("expected redaction marker in output, got: %s", redacted) - } -} - -func TestValidateExtensionAuthURL(t *testing.T) { - if err := validateExtensionAuthURL("https://accounts.example.com/oauth/authorize"); err != nil { - t.Fatalf("expected valid auth URL, got error: %v", err) - } - - blocked := []string{ - "http://accounts.example.com/oauth/authorize", - "https://user:pass@accounts.example.com/oauth/authorize", - "https://localhost/oauth/authorize", - } - - for _, rawURL := range blocked { - if err := validateExtensionAuthURL(rawURL); err == nil { - t.Fatalf("expected URL to be blocked: %s", rawURL) - } - } -} - -func TestValidateDomainRejectsEmbeddedCredentials(t *testing.T) { - ext := &LoadedExtension{ - ID: "test-ext", - Manifest: &ExtensionManifest{ - Name: "test-ext", - Permissions: ExtensionPermissions{ - Network: []string{"api.example.com"}, - }, - }, - DataDir: t.TempDir(), - } - - runtime := NewExtensionRuntime(ext) - if err := runtime.validateDomain("https://user:pass@api.example.com/resource"); err == nil { - t.Fatal("expected embedded URL credentials to be rejected") - } -} - -func TestBuildStoreExtensionDestPath(t *testing.T) { - baseDir := t.TempDir() - - destPath, err := buildStoreExtensionDestPath(baseDir, "../evil/name") - if err != nil { - t.Fatalf("expected sanitized path to be generated, got error: %v", err) - } - - if !isPathWithinBase(baseDir, destPath) { - t.Fatalf("expected destination path to remain under base dir: %s", destPath) - } - - baseName := filepath.Base(destPath) - if strings.Contains(baseName, "/") || strings.Contains(baseName, `\`) { - t.Fatalf("expected filename to be sanitized, got: %s", baseName) - } - if !strings.HasSuffix(baseName, ".spotiflac-ext") { - t.Fatalf("expected .spotiflac-ext suffix, got: %s", baseName) - } - - if _, err := buildStoreExtensionDestPath(baseDir, " "); err == nil { - t.Fatal("expected empty extension id to be rejected") - } -} From 497ba342c05de06e1d1beb7d7c31272c2124b5c1 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 22 Mar 2026 22:43:03 +0700 Subject: [PATCH 137/145] feat: add createPlaylistFolder setting for playlist source folder prefix When enabled, playlist downloads are placed inside a subfolder named after the playlist before the normal folder organization structure (e.g. Playlist///). The setting is a no-op when folder organization is already set to 'By Playlist'. Includes model field, JSON serialization, settings notifier, download queue path logic, UI toggle in download settings, and localizations for all 13 languages. --- lib/l10n/app_localizations.dart | 24 ++++++ lib/l10n/app_localizations_de.dart | 16 ++++ lib/l10n/app_localizations_en.dart | 16 ++++ lib/l10n/app_localizations_es.dart | 16 ++++ lib/l10n/app_localizations_fr.dart | 16 ++++ lib/l10n/app_localizations_hi.dart | 16 ++++ lib/l10n/app_localizations_id.dart | 16 ++++ lib/l10n/app_localizations_ja.dart | 16 ++++ lib/l10n/app_localizations_ko.dart | 16 ++++ lib/l10n/app_localizations_nl.dart | 16 ++++ lib/l10n/app_localizations_pt.dart | 16 ++++ lib/l10n/app_localizations_ru.dart | 16 ++++ lib/l10n/app_localizations_tr.dart | 16 ++++ lib/l10n/app_localizations_zh.dart | 16 ++++ lib/l10n/arb/app_en.arb | 16 ++++ lib/l10n/arb/app_id.arb | 16 ++++ lib/models/settings.dart | 7 +- lib/models/settings.g.dart | 2 + lib/providers/download_queue_provider.dart | 65 ++++++++++++++--- lib/providers/settings_provider.dart | 5 ++ .../settings/download_settings_page.dart | 73 +++++++++++++------ 21 files changed, 380 insertions(+), 36 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0ea69f2b..d8e291ca 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4659,6 +4659,30 @@ abstract class AppLocalizations { /// **'Artist Name Filters'** String get downloadArtistNameFilters; + /// Setting title for adding a playlist folder prefix before the normal organization structure + /// + /// In en, this message translates to: + /// **'Create playlist source folder'** + String get downloadCreatePlaylistSourceFolder; + + /// Subtitle when playlist source folder prefix is enabled + /// + /// In en, this message translates to: + /// **'Playlist downloads use Playlist/ plus your normal folder structure.'** + String get downloadCreatePlaylistSourceFolderEnabled; + + /// Subtitle when playlist source folder prefix is disabled + /// + /// In en, this message translates to: + /// **'Playlist downloads use the normal folder structure only.'** + String get downloadCreatePlaylistSourceFolderDisabled; + + /// Subtitle when playlist folder prefix setting is redundant because folder organization is already by playlist + /// + /// In en, this message translates to: + /// **'By Playlist already places downloads inside a playlist folder.'** + String get downloadCreatePlaylistSourceFolderRedundant; + /// Setting title for SongLink country region /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 7ad45e21..b58fd78c 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2712,6 +2712,22 @@ class AppLocalizationsDe extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 13208d5b..328a4a9d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2685,6 +2685,22 @@ class AppLocalizationsEn extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 113c08f1..6308f8f4 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2685,6 +2685,22 @@ class AppLocalizationsEs extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 6521f755..f16c40f8 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2686,6 +2686,22 @@ class AppLocalizationsFr extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 04227142..2022128f 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2684,6 +2684,22 @@ class AppLocalizationsHi extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index b23768e3..dce7f058 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2692,6 +2692,22 @@ class AppLocalizationsId extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Buat folder sumber playlist'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Unduhan dari playlist memakai Playlist/ lalu struktur folder normal Anda.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Unduhan dari playlist hanya memakai struktur folder normal.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'Mode Berdasarkan Playlist sudah menaruh unduhan ke dalam folder playlist.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index caf7d16e..5f1fc709 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2671,6 +2671,22 @@ class AppLocalizationsJa extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 1cac05e7..5d4f256f 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2664,6 +2664,22 @@ class AppLocalizationsKo extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index ac2c8b57..ef14b913 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2684,6 +2684,22 @@ class AppLocalizationsNl extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 3c5c8896..d1fb718a 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2685,6 +2685,22 @@ class AppLocalizationsPt extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 31a1a1dc..47398466 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2743,6 +2743,22 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 518bcf77..7c77c93e 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2696,6 +2696,22 @@ class AppLocalizationsTr extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index d36cf154..5ce4df91 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2685,6 +2685,22 @@ class AppLocalizationsZh extends AppLocalizations { @override String get downloadArtistNameFilters => 'Artist Name Filters'; + @override + String get downloadCreatePlaylistSourceFolder => + 'Create playlist source folder'; + + @override + String get downloadCreatePlaylistSourceFolderEnabled => + 'Playlist downloads use Playlist/ plus your normal folder structure.'; + + @override + String get downloadCreatePlaylistSourceFolderDisabled => + 'Playlist downloads use the normal folder structure only.'; + + @override + String get downloadCreatePlaylistSourceFolderRedundant => + 'By Playlist already places downloads inside a playlist folder.'; + @override String get downloadSongLinkRegion => 'SongLink Region'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7055840d..69aa2068 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -3586,6 +3586,22 @@ "@downloadArtistNameFilters": { "description": "Setting title for artist folder filter options" }, + "downloadCreatePlaylistSourceFolder": "Create playlist source folder", + "@downloadCreatePlaylistSourceFolder": { + "description": "Setting title for adding a playlist folder prefix before the normal organization structure" + }, + "downloadCreatePlaylistSourceFolderEnabled": "Playlist downloads use Playlist/ plus your normal folder structure.", + "@downloadCreatePlaylistSourceFolderEnabled": { + "description": "Subtitle when playlist source folder prefix is enabled" + }, + "downloadCreatePlaylistSourceFolderDisabled": "Playlist downloads use the normal folder structure only.", + "@downloadCreatePlaylistSourceFolderDisabled": { + "description": "Subtitle when playlist source folder prefix is disabled" + }, + "downloadCreatePlaylistSourceFolderRedundant": "By Playlist already places downloads inside a playlist folder.", + "@downloadCreatePlaylistSourceFolderRedundant": { + "description": "Subtitle when playlist folder prefix setting is redundant because folder organization is already by playlist" + }, "downloadSongLinkRegion": "SongLink Region", "@downloadSongLinkRegion": { "description": "Setting title for SongLink country region" diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index ff764ec8..bb02bc50 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -1797,6 +1797,22 @@ "@downloadUseAlbumArtistForFolders": { "description": "Setting - choose whether artist folders use Album Artist or Track Artist" }, + "downloadCreatePlaylistSourceFolder": "Buat folder sumber playlist", + "@downloadCreatePlaylistSourceFolder": { + "description": "Setting title for adding a playlist folder prefix before the normal organization structure" + }, + "downloadCreatePlaylistSourceFolderEnabled": "Unduhan dari playlist memakai Playlist/ lalu struktur folder normal Anda.", + "@downloadCreatePlaylistSourceFolderEnabled": { + "description": "Subtitle when playlist source folder prefix is enabled" + }, + "downloadCreatePlaylistSourceFolderDisabled": "Unduhan dari playlist hanya memakai struktur folder normal.", + "@downloadCreatePlaylistSourceFolderDisabled": { + "description": "Subtitle when playlist source folder prefix is disabled" + }, + "downloadCreatePlaylistSourceFolderRedundant": "Mode Berdasarkan Playlist sudah menaruh unduhan ke dalam folder playlist.", + "@downloadCreatePlaylistSourceFolderRedundant": { + "description": "Subtitle when playlist folder prefix setting is redundant because folder organization is already by playlist" + }, "downloadUsePrimaryArtistOnly": "Primary artist only for folders", "@downloadUsePrimaryArtistOnly": { "description": "Setting - strip featured artists from folder name" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 87c31e1b..c8848ab7 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -20,6 +20,7 @@ class AppSettings { final String updateChannel; final bool hasSearchedBefore; final String folderOrganization; + final bool createPlaylistFolder; final bool useAlbumArtistForFolders; final bool usePrimaryArtistOnly; // Strip featured artists from folder name final bool filterContributingArtistsInAlbumArtist; @@ -96,6 +97,7 @@ class AppSettings { this.updateChannel = 'stable', this.hasSearchedBefore = false, this.folderOrganization = 'none', + this.createPlaylistFolder = false, this.useAlbumArtistForFolders = true, this.usePrimaryArtistOnly = false, this.filterContributingArtistsInAlbumArtist = false, @@ -159,6 +161,7 @@ class AppSettings { String? updateChannel, bool? hasSearchedBefore, String? folderOrganization, + bool? createPlaylistFolder, bool? useAlbumArtistForFolders, bool? usePrimaryArtistOnly, bool? filterContributingArtistsInAlbumArtist, @@ -215,6 +218,7 @@ class AppSettings { updateChannel: updateChannel ?? this.updateChannel, hasSearchedBefore: hasSearchedBefore ?? this.hasSearchedBefore, folderOrganization: folderOrganization ?? this.folderOrganization, + createPlaylistFolder: createPlaylistFolder ?? this.createPlaylistFolder, useAlbumArtistForFolders: useAlbumArtistForFolders ?? this.useAlbumArtistForFolders, usePrimaryArtistOnly: usePrimaryArtistOnly ?? this.usePrimaryArtistOnly, @@ -255,8 +259,7 @@ class AppSettings { localLibraryBookmark: localLibraryBookmark ?? this.localLibraryBookmark, localLibraryShowDuplicates: localLibraryShowDuplicates ?? this.localLibraryShowDuplicates, - localLibraryAutoScan: - localLibraryAutoScan ?? this.localLibraryAutoScan, + localLibraryAutoScan: localLibraryAutoScan ?? this.localLibraryAutoScan, hasCompletedTutorial: hasCompletedTutorial ?? this.hasCompletedTutorial, lyricsProviders: lyricsProviders ?? this.lyricsProviders, lyricsIncludeTranslationNetease: diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 2b6a35cf..026569dd 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -23,6 +23,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( updateChannel: json['updateChannel'] as String? ?? 'stable', hasSearchedBefore: json['hasSearchedBefore'] as bool? ?? false, folderOrganization: json['folderOrganization'] as String? ?? 'none', + createPlaylistFolder: json['createPlaylistFolder'] as bool? ?? false, useAlbumArtistForFolders: json['useAlbumArtistForFolders'] as bool? ?? true, usePrimaryArtistOnly: json['usePrimaryArtistOnly'] as bool? ?? false, filterContributingArtistsInAlbumArtist: @@ -100,6 +101,7 @@ Map _$AppSettingsToJson( 'updateChannel': instance.updateChannel, 'hasSearchedBefore': instance.hasSearchedBefore, 'folderOrganization': instance.folderOrganization, + 'createPlaylistFolder': instance.createPlaylistFolder, 'useAlbumArtistForFolders': instance.useAlbumArtistForFolders, 'usePrimaryArtistOnly': instance.usePrimaryArtistOnly, 'filterContributingArtistsInAlbumArtist': diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 8901c647..4a308850 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1651,12 +1651,23 @@ class DownloadQueueNotifier extends Notifier { String folderOrganization, { bool separateSingles = false, String albumFolderStructure = 'artist_album', + bool createPlaylistFolder = false, bool useAlbumArtistForFolders = true, bool usePrimaryArtistOnly = false, bool filterContributingArtistsInAlbumArtist = false, String? playlistName, }) async { String baseDir = state.outputDir; + if (createPlaylistFolder && + folderOrganization != 'playlist' && + playlistName != null && + playlistName.isNotEmpty) { + final playlistFolder = _sanitizeFolderName(playlistName); + if (playlistFolder.isNotEmpty) { + baseDir = '$baseDir${Platform.pathSeparator}$playlistFolder'; + await _ensureDirExists(baseDir, label: 'Playlist folder'); + } + } final normalizedAlbumArtist = normalizeOptionalString(track.albumArtist); var folderArtist = useAlbumArtistForFolders ? normalizedAlbumArtist ?? track.artistName @@ -1809,11 +1820,19 @@ class DownloadQueueNotifier extends Notifier { String folderOrganization, { bool separateSingles = false, String albumFolderStructure = 'artist_album', + bool createPlaylistFolder = false, bool useAlbumArtistForFolders = true, bool usePrimaryArtistOnly = false, bool filterContributingArtistsInAlbumArtist = false, String? playlistName, }) async { + final playlistPrefix = + createPlaylistFolder && + folderOrganization != 'playlist' && + playlistName != null && + playlistName.isNotEmpty + ? _sanitizeFolderName(playlistName) + : ''; final normalizedAlbumArtist = normalizeOptionalString(track.albumArtist); var folderArtist = useAlbumArtistForFolders ? normalizedAlbumArtist ?? track.artistName @@ -1833,34 +1852,40 @@ class DownloadQueueNotifier extends Notifier { if (albumFolderStructure == 'artist_album_singles') { if (isSingle) { - return '$artistName/Singles'; + return _joinRelativePath(playlistPrefix, '$artistName/Singles'); } final albumName = _sanitizeFolderName(track.albumName); - return '$artistName/$albumName'; + return _joinRelativePath(playlistPrefix, '$artistName/$albumName'); } if (isSingle) { - return 'Singles'; + return _joinRelativePath(playlistPrefix, 'Singles'); } final albumName = _sanitizeFolderName(track.albumName); final year = _extractYear(track.releaseDate); switch (albumFolderStructure) { case 'album_only': - return 'Albums/$albumName'; + return _joinRelativePath(playlistPrefix, 'Albums/$albumName'); case 'artist_year_album': final yearAlbum = year != null ? '[$year] $albumName' : albumName; - return 'Albums/$artistName/$yearAlbum'; + return _joinRelativePath( + playlistPrefix, + 'Albums/$artistName/$yearAlbum', + ); case 'year_album': final yearAlbum = year != null ? '[$year] $albumName' : albumName; - return 'Albums/$yearAlbum'; + return _joinRelativePath(playlistPrefix, 'Albums/$yearAlbum'); default: - return 'Albums/$artistName/$albumName'; + return _joinRelativePath( + playlistPrefix, + 'Albums/$artistName/$albumName', + ); } } if (folderOrganization == 'none') { - return ''; + return playlistPrefix; } switch (folderOrganization) { @@ -1870,18 +1895,30 @@ class DownloadQueueNotifier extends Notifier { } return ''; case 'artist': - return _sanitizeFolderName(folderArtist); + return _joinRelativePath( + playlistPrefix, + _sanitizeFolderName(folderArtist), + ); case 'album': - return _sanitizeFolderName(track.albumName); + return _joinRelativePath( + playlistPrefix, + _sanitizeFolderName(track.albumName), + ); case 'artist_album': final artistName = _sanitizeFolderName(folderArtist); final albumName = _sanitizeFolderName(track.albumName); - return '$artistName/$albumName'; + return _joinRelativePath(playlistPrefix, '$artistName/$albumName'); default: - return ''; + return playlistPrefix; } } + String _joinRelativePath(String prefix, String suffix) { + if (prefix.isEmpty) return suffix; + if (suffix.isEmpty) return prefix; + return '$prefix/$suffix'; + } + String _determineOutputExt(String quality, String service) { if (service.toLowerCase() == 'youtube') { if (quality.toLowerCase().contains('mp3')) { @@ -3547,6 +3584,7 @@ class DownloadQueueNotifier extends Notifier { settings.folderOrganization, separateSingles: settings.separateSingles, albumFolderStructure: settings.albumFolderStructure, + createPlaylistFolder: settings.createPlaylistFolder, useAlbumArtistForFolders: settings.useAlbumArtistForFolders, usePrimaryArtistOnly: settings.usePrimaryArtistOnly, filterContributingArtistsInAlbumArtist: @@ -3562,6 +3600,7 @@ class DownloadQueueNotifier extends Notifier { settings.folderOrganization, separateSingles: settings.separateSingles, albumFolderStructure: settings.albumFolderStructure, + createPlaylistFolder: settings.createPlaylistFolder, useAlbumArtistForFolders: settings.useAlbumArtistForFolders, usePrimaryArtistOnly: settings.usePrimaryArtistOnly, filterContributingArtistsInAlbumArtist: @@ -3905,10 +3944,12 @@ class DownloadQueueNotifier extends Notifier { settings.folderOrganization, separateSingles: settings.separateSingles, albumFolderStructure: settings.albumFolderStructure, + createPlaylistFolder: settings.createPlaylistFolder, useAlbumArtistForFolders: settings.useAlbumArtistForFolders, usePrimaryArtistOnly: settings.usePrimaryArtistOnly, filterContributingArtistsInAlbumArtist: settings.filterContributingArtistsInAlbumArtist, + playlistName: item.playlistName, ); final fallbackResult = await runDownload( useSaf: false, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index fcab3631..44251a03 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -375,6 +375,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setCreatePlaylistFolder(bool enabled) { + state = state.copyWith(createPlaylistFolder: enabled); + _saveSettings(); + } + void setUseAlbumArtistForFolders(bool enabled) { state = state.copyWith(useAlbumArtistForFolders: enabled); _saveSettings(); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index fb583df7..218871f0 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -6,6 +6,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; @@ -436,7 +437,8 @@ class _DownloadSettingsPageState extends ConsumerState { ], SettingsItem( title: context.l10n.youtubeOpusBitrateTitle, - subtitle: '${settings.youtubeOpusBitrate}kbps (128/256/320)', + subtitle: + '${settings.youtubeOpusBitrate}kbps (128/256/320)', onTap: () => _showYoutubeBitratePicker( context: context, title: context.l10n.youtubeOpusBitrateTitle, @@ -515,8 +517,12 @@ class _DownloadSettingsPageState extends ConsumerState { icon: Icons.translate_outlined, title: context.l10n.downloadNeteaseIncludeTranslation, subtitle: settings.lyricsIncludeTranslationNetease - ? context.l10n.downloadNeteaseIncludeTranslationEnabled - : context.l10n.downloadNeteaseIncludeTranslationDisabled, + ? context + .l10n + .downloadNeteaseIncludeTranslationEnabled + : context + .l10n + .downloadNeteaseIncludeTranslationDisabled, value: settings.lyricsIncludeTranslationNetease, onChanged: (value) => ref .read(settingsProvider.notifier) @@ -526,8 +532,12 @@ class _DownloadSettingsPageState extends ConsumerState { icon: Icons.text_fields_outlined, title: context.l10n.downloadNeteaseIncludeRomanization, subtitle: settings.lyricsIncludeRomanizationNetease - ? context.l10n.downloadNeteaseIncludeRomanizationEnabled - : context.l10n.downloadNeteaseIncludeRomanizationDisabled, + ? context + .l10n + .downloadNeteaseIncludeRomanizationEnabled + : context + .l10n + .downloadNeteaseIncludeRomanizationDisabled, value: settings.lyricsIncludeRomanizationNetease, onChanged: (value) => ref .read(settingsProvider.notifier) @@ -627,6 +637,15 @@ class _DownloadSettingsPageState extends ConsumerState { settings.folderOrganization, ), ), + SettingsSwitchItem( + icon: Icons.playlist_play_outlined, + title: context.l10n.downloadCreatePlaylistSourceFolder, + subtitle: _getPlaylistFolderSubtitle(settings), + value: settings.createPlaylistFolder, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setCreatePlaylistFolder(value), + ), SettingsSwitchItem( icon: Icons.person_search_outlined, title: context.l10n.downloadUseAlbumArtistForFolders, @@ -642,7 +661,7 @@ class _DownloadSettingsPageState extends ConsumerState { .read(settingsProvider.notifier) .setUseAlbumArtistForFolders(value), ), - SettingsItem( + SettingsItem( icon: Icons.filter_alt_outlined, title: context.l10n.downloadArtistNameFilters, subtitle: _getArtistFolderFilterSubtitle( @@ -1407,6 +1426,16 @@ class _DownloadSettingsPageState extends ConsumerState { } } + String _getPlaylistFolderSubtitle(AppSettings settings) { + if (settings.folderOrganization == 'playlist') { + return context.l10n.downloadCreatePlaylistSourceFolderRedundant; + } + if (settings.createPlaylistFolder) { + return context.l10n.downloadCreatePlaylistSourceFolderEnabled; + } + return context.l10n.downloadCreatePlaylistSourceFolderDisabled; + } + String _getArtistFolderFilterSubtitle( BuildContext context, { required bool usePrimaryArtistOnly, @@ -1776,17 +1805,17 @@ class _DownloadSettingsPageState extends ConsumerState { children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - context.l10n.downloadSongLinkRegion, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + child: Text( + context.l10n.downloadSongLinkRegion, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - context.l10n.downloadSongLinkRegionDesc, + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.downloadSongLinkRegionDesc, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -1847,12 +1876,12 @@ class _DownloadSettingsPageState extends ConsumerState { children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - context.l10n.downloadFolderOrganization, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), + child: Text( + context.l10n.downloadFolderOrganization, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), From c57c8a4267fef9368a205f56203ca0f658f8c254 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 22 Mar 2026 23:00:42 +0700 Subject: [PATCH 138/145] feat: implement full M4A tag read engine with atom path fallback and freeform fix Add ReadM4ATags() that parses all standard iTunes atoms (title, artist, album, album artist, date, genre, composer, comment, copyright, lyrics, track/disc number) and freeform '----' atoms (ISRC, label, lyrics). Fix two pre-existing bugs in the M4A atom traversal: - findM4AIlstAtom: now tries moov>udta>meta>ilst first, then falls back to moov>meta>ilst so files from Tidal/Qobuz/Apple Music are handled - readM4AFreeformValue: 'name' atom payload is raw UTF-8 after 4-byte flags, not a nested 'data' atom; fix reads it directly so ISRC/label freeform tags are no longer silently dropped Refactor extractLyricsFromM4A and extractCoverFromM4A to reuse the new helpers (findM4AIlstAtom, readM4ADataAtomPayload) instead of duplicating the atom traversal logic. Add extractAnyCoverArtWithHint M4A case that previously returned a hardcoded 'not yet supported' error. --- go_backend/audio_metadata.go | 14 +- go_backend/metadata.go | 328 +++++++++++++++++++++++++---------- 2 files changed, 250 insertions(+), 92 deletions(-) diff --git a/go_backend/audio_metadata.go b/go_backend/audio_metadata.go index e1e15925..7a906f8e 100644 --- a/go_backend/audio_metadata.go +++ b/go_backend/audio_metadata.go @@ -1594,7 +1594,19 @@ func extractAnyCoverArtWithHint(filePath, displayNameHint string) ([]byte, strin return extractOggCoverArt(filePath) case ".m4a": - return nil, "", fmt.Errorf("M4A cover extraction not yet supported") + data, err := extractCoverFromM4A(filePath) + if err != nil { + return nil, "", err + } + mimeType := "image/jpeg" + if len(data) >= 8 && + data[0] == 0x89 && + data[1] == 0x50 && + data[2] == 0x4E && + data[3] == 0x47 { + mimeType = "image/png" + } + return data, mimeType, nil default: return nil, "", fmt.Errorf("unsupported format: %s", ext) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index d4d28e37..fdd856ec 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -589,78 +589,117 @@ func ExtractLyrics(filePath string) (string, error) { return extractLyricsFromSidecarLRC(filePath) } -func extractLyricsFromM4A(filePath string) (string, error) { +func ReadM4ATags(filePath string) (*AudioMetadata, error) { f, err := os.Open(filePath) if err != nil { - return "", err + return nil, err } defer f.Close() fi, err := f.Stat() + if err != nil { + return nil, err + } + + ilst, err := findM4AIlstAtom(f, fi.Size()) + if err != nil { + return nil, err + } + + metadata := &AudioMetadata{} + start := ilst.offset + ilst.headerSize + end := ilst.offset + ilst.size + for pos := start; pos+8 <= end; { + header, err := readAtomHeaderAt(f, pos, fi.Size()) + if err != nil { + return nil, err + } + if header.size == 0 { + header.size = end - pos + } + if header.size < header.headerSize { + return nil, fmt.Errorf("invalid atom size for %s", header.typ) + } + + switch header.typ { + case "\xa9nam": + metadata.Title, _ = readM4ATextValue(f, header, fi.Size()) + case "\xa9ART": + metadata.Artist, _ = readM4ATextValue(f, header, fi.Size()) + case "\xa9alb": + metadata.Album, _ = readM4ATextValue(f, header, fi.Size()) + case "aART": + metadata.AlbumArtist, _ = readM4ATextValue(f, header, fi.Size()) + case "\xa9day": + metadata.Date, _ = readM4ATextValue(f, header, fi.Size()) + metadata.Year = metadata.Date + case "\xa9gen": + metadata.Genre, _ = readM4ATextValue(f, header, fi.Size()) + case "\xa9wrt": + metadata.Composer, _ = readM4ATextValue(f, header, fi.Size()) + case "\xa9cmt": + metadata.Comment, _ = readM4ATextValue(f, header, fi.Size()) + case "cprt": + metadata.Copyright, _ = readM4ATextValue(f, header, fi.Size()) + case "\xa9lyr": + metadata.Lyrics, _ = readM4ATextValue(f, header, fi.Size()) + case "trkn": + metadata.TrackNumber, _ = readM4AIndexValue(f, header, fi.Size()) + case "disk": + metadata.DiscNumber, _ = readM4AIndexValue(f, header, fi.Size()) + case "----": + name, value, freeformErr := readM4AFreeformValue(f, header, fi.Size()) + if freeformErr == nil { + switch strings.ToUpper(strings.TrimSpace(name)) { + case "ISRC": + metadata.ISRC = value + case "LABEL", "ORGANIZATION": + metadata.Label = value + case "COMMENT": + if metadata.Comment == "" { + metadata.Comment = value + } + case "COMPOSER": + if metadata.Composer == "" { + metadata.Composer = value + } + case "COPYRIGHT": + if metadata.Copyright == "" { + metadata.Copyright = value + } + case "LYRICS", "UNSYNCEDLYRICS": + if metadata.Lyrics == "" { + metadata.Lyrics = value + } + } + } + } + + pos += header.size + } + + if metadata.Title == "" && + metadata.Artist == "" && + metadata.Album == "" && + metadata.AlbumArtist == "" && + metadata.Lyrics == "" && + metadata.TrackNumber == 0 && + metadata.DiscNumber == 0 { + return nil, fmt.Errorf("no M4A tags found") + } + + return metadata, nil +} + +func extractLyricsFromM4A(filePath string) (string, error) { + metadata, err := ReadM4ATags(filePath) if err != nil { return "", err } - fileSize := fi.Size() - - moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize) - if err != nil || !found { - return "", fmt.Errorf("moov not found") + if metadata == nil || strings.TrimSpace(metadata.Lyrics) == "" { + return "", fmt.Errorf("no lyrics found in file") } - - bodyStart := moov.offset + moov.headerSize - bodySize := moov.size - moov.headerSize - - udta, found, err := findAtomInRange(f, bodyStart, bodySize, "udta", fileSize) - if err != nil || !found { - return "", fmt.Errorf("udta not found") - } - - bodyStart = udta.offset + udta.headerSize - bodySize = udta.size - udta.headerSize - - meta, found, err := findAtomInRange(f, bodyStart, bodySize, "meta", fileSize) - if err != nil || !found { - return "", fmt.Errorf("meta not found") - } - - // meta atom has 4-byte version/flags after the header - bodyStart = meta.offset + meta.headerSize + 4 - bodySize = meta.size - meta.headerSize - 4 - - ilst, found, err := findAtomInRange(f, bodyStart, bodySize, "ilst", fileSize) - if err != nil || !found { - return "", fmt.Errorf("ilst not found") - } - - bodyStart = ilst.offset + ilst.headerSize - bodySize = ilst.size - ilst.headerSize - - lyr, found, err := findAtomInRange(f, bodyStart, bodySize, "\xa9lyr", fileSize) - if err != nil || !found { - return "", fmt.Errorf("lyrics atom not found") - } - - dataStart := lyr.offset + lyr.headerSize - dataSize := lyr.size - lyr.headerSize - - dataAtom, found, err := findAtomInRange(f, dataStart, dataSize, "data", fileSize) - if err != nil || !found { - return "", fmt.Errorf("data atom not found in lyrics") - } - - // data atom: 8 bytes header + 4 bytes type indicator + 4 bytes locale = skip 8 - textStart := dataAtom.offset + dataAtom.headerSize + 8 - textLen := dataAtom.size - dataAtom.headerSize - 8 - if textLen <= 0 { - return "", fmt.Errorf("empty lyrics") - } - - buf := make([]byte, textLen) - if _, err := f.ReadAt(buf, textStart); err != nil { - return "", err - } - - return string(buf), nil + return metadata.Lyrics, nil } func extractCoverFromM4A(filePath string) ([]byte, error) { @@ -676,37 +715,13 @@ func extractCoverFromM4A(filePath string) ([]byte, error) { } fileSize := fi.Size() - moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize) - if err != nil || !found { - return nil, fmt.Errorf("moov not found") + ilst, err := findM4AIlstAtom(f, fileSize) + if err != nil { + return nil, err } - bodyStart := moov.offset + moov.headerSize - bodySize := moov.size - moov.headerSize - - udta, found, err := findAtomInRange(f, bodyStart, bodySize, "udta", fileSize) - if err != nil || !found { - return nil, fmt.Errorf("udta not found") - } - - bodyStart = udta.offset + udta.headerSize - bodySize = udta.size - udta.headerSize - - meta, found, err := findAtomInRange(f, bodyStart, bodySize, "meta", fileSize) - if err != nil || !found { - return nil, fmt.Errorf("meta not found") - } - - bodyStart = meta.offset + meta.headerSize + 4 - bodySize = meta.size - meta.headerSize - 4 - - ilst, found, err := findAtomInRange(f, bodyStart, bodySize, "ilst", fileSize) - if err != nil || !found { - return nil, fmt.Errorf("ilst not found") - } - - bodyStart = ilst.offset + ilst.headerSize - bodySize = ilst.size - ilst.headerSize + bodyStart := ilst.offset + ilst.headerSize + bodySize := ilst.size - ilst.headerSize covr, found, err := findAtomInRange(f, bodyStart, bodySize, "covr", fileSize) if err != nil || !found { @@ -736,6 +751,137 @@ func extractCoverFromM4A(filePath string) ([]byte, error) { return buf, nil } +// findM4AIlstAtom locates the ilst atom that holds all iTunes-style tags. +// It tries two common layouts: +// 1. moov > udta > meta > ilst (iTunes, FFmpeg default) +// 2. moov > meta > ilst (some encoders omit the udta wrapper) +func findM4AIlstAtom(f *os.File, fileSize int64) (atomHeader, error) { + moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize) + if err != nil || !found { + return atomHeader{}, fmt.Errorf("moov not found") + } + + moovBodyStart := moov.offset + moov.headerSize + moovBodySize := moov.size - moov.headerSize + + // Path 1: moov > udta > meta > ilst + if udta, ok, _ := findAtomInRange(f, moovBodyStart, moovBodySize, "udta", fileSize); ok { + udtaBodyStart := udta.offset + udta.headerSize + udtaBodySize := udta.size - udta.headerSize + if meta, ok2, _ := findAtomInRange(f, udtaBodyStart, udtaBodySize, "meta", fileSize); ok2 { + metaBodyStart := meta.offset + meta.headerSize + 4 + metaBodySize := meta.size - meta.headerSize - 4 + if ilst, ok3, _ := findAtomInRange(f, metaBodyStart, metaBodySize, "ilst", fileSize); ok3 { + return ilst, nil + } + } + } + + // Path 2: moov > meta > ilst (no udta wrapper) + if meta, ok, _ := findAtomInRange(f, moovBodyStart, moovBodySize, "meta", fileSize); ok { + metaBodyStart := meta.offset + meta.headerSize + 4 + metaBodySize := meta.size - meta.headerSize - 4 + if ilst, ok2, _ := findAtomInRange(f, metaBodyStart, metaBodySize, "ilst", fileSize); ok2 { + return ilst, nil + } + } + + return atomHeader{}, fmt.Errorf("ilst not found (tried moov>udta>meta>ilst and moov>meta>ilst)") +} + +func readM4ADataAtomPayload(f *os.File, dataAtom atomHeader) ([]byte, error) { + payloadStart := dataAtom.offset + dataAtom.headerSize + 8 + payloadLen := dataAtom.size - dataAtom.headerSize - 8 + if payloadLen <= 0 { + return nil, fmt.Errorf("empty data atom in %s", dataAtom.typ) + } + + buf := make([]byte, payloadLen) + if _, err := f.ReadAt(buf, payloadStart); err != nil { + return nil, err + } + return buf, nil +} + +func readM4ADataPayload(f *os.File, parent atomHeader, fileSize int64) ([]byte, error) { + dataStart := parent.offset + parent.headerSize + dataSize := parent.size - parent.headerSize + + dataAtom, found, err := findAtomInRange(f, dataStart, dataSize, "data", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("data atom not found in %s", parent.typ) + } + return readM4ADataAtomPayload(f, dataAtom) +} + +func readM4ATextValue(f *os.File, parent atomHeader, fileSize int64) (string, error) { + payload, err := readM4ADataPayload(f, parent, fileSize) + if err != nil { + return "", err + } + return strings.TrimSpace(strings.TrimRight(string(payload), "\x00")), nil +} + +func readM4AIndexValue(f *os.File, parent atomHeader, fileSize int64) (int, error) { + payload, err := readM4ADataPayload(f, parent, fileSize) + if err != nil { + return 0, err + } + if len(payload) < 4 { + return 0, fmt.Errorf("index payload too short in %s", parent.typ) + } + return int(binary.BigEndian.Uint16(payload[2:4])), nil +} + +func readM4AFreeformValue(f *os.File, parent atomHeader, fileSize int64) (string, string, error) { + start := parent.offset + parent.headerSize + end := parent.offset + parent.size + + var nameValue string + var dataValue string + for pos := start; pos+8 <= end; { + header, err := readAtomHeaderAt(f, pos, fileSize) + if err != nil { + return "", "", err + } + if header.size == 0 { + header.size = end - pos + } + if header.size < header.headerSize { + return "", "", fmt.Errorf("invalid atom size for %s", header.typ) + } + + switch header.typ { + case "mean": + // Domain qualifier (e.g. "com.apple.iTunes") — not needed, skip. + case "name": + // The "name" atom payload is: 4-byte version/flags, then raw UTF-8 text. + // It does NOT contain a nested "data" atom, so read the payload directly. + payloadStart := header.offset + header.headerSize + 4 + payloadLen := header.size - header.headerSize - 4 + if payloadLen > 0 { + buf := make([]byte, payloadLen) + if _, readErr := f.ReadAt(buf, payloadStart); readErr == nil { + nameValue = strings.TrimSpace(strings.TrimRight(string(buf), "\x00")) + } + } + case "data": + payload, payloadErr := readM4ADataAtomPayload(f, header) + if payloadErr == nil { + dataValue = strings.TrimSpace(strings.TrimRight(string(payload), "\x00")) + } + } + + pos += header.size + } + + if nameValue == "" || dataValue == "" { + return "", "", fmt.Errorf("freeform M4A tag incomplete") + } + + return nameValue, dataValue, nil +} + func extractLyricsFromSidecarLRC(filePath string) (string, error) { ext := filepath.Ext(filePath) base := strings.TrimSuffix(filePath, ext) From 4cf885a52e6243823bb94ac47bfec3b56cf9c483 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 22 Mar 2026 23:00:55 +0700 Subject: [PATCH 139/145] feat: populate M4A metadata in ReadFileMetadata and library scan ReadFileMetadata now fills all tag fields (title, artist, album, ISRC, lyrics, genre, label, copyright, composer, comment, track/disc number) for M4A files using the new ReadM4ATags helper, matching the existing behavior for FLAC, MP3, and Ogg. scanM4AFile reads tags via ReadM4ATags instead of falling back to the filename, and applies applyDefaultLibraryMetadata for missing fields (consistent with FLAC/MP3 scan path). Remove the '&& ext != ".m4a"' guard in cover cache so M4A cover art is extracted and cached during library scans. --- go_backend/exports.go | 30 ++++++++++++++++++++++++++++-- go_backend/library_scan.go | 21 +++++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/go_backend/exports.go b/go_backend/exports.go index e65b0197..d0a86ccc 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -739,6 +739,26 @@ func ReadFileMetadata(filePath string) (string, error) { } } } else if isM4A { + meta, err := ReadM4ATags(filePath) + if err == nil && meta != nil { + result["title"] = meta.Title + result["artist"] = meta.Artist + result["album"] = meta.Album + result["album_artist"] = meta.AlbumArtist + result["date"] = meta.Date + if meta.Date == "" { + result["date"] = meta.Year + } + result["track_number"] = meta.TrackNumber + result["disc_number"] = meta.DiscNumber + result["isrc"] = meta.ISRC + result["lyrics"] = meta.Lyrics + result["genre"] = meta.Genre + result["label"] = meta.Label + result["copyright"] = meta.Copyright + result["composer"] = meta.Composer + result["comment"] = meta.Comment + } quality, qualityErr := GetM4AQuality(filePath) if qualityErr == nil { result["bit_depth"] = quality.BitDepth @@ -1960,8 +1980,15 @@ func ReEnrichFile(requestJSON string) (string, error) { } }() - // Fetch lyrics + // Preserve existing lyrics when online enrichment does not return a replacement. var lyricsLRC string + existingLyrics, existingLyricsErr := ExtractLyrics(req.FilePath) + if existingLyricsErr == nil && strings.TrimSpace(existingLyrics) != "" { + lyricsLRC = existingLyrics + GoLog("[ReEnrich] Preserving existing embedded/sidecar lyrics\n") + } + + // Fetch lyrics if req.EmbedLyrics { client := NewLyricsClient() durationSec := float64(req.DurationMs) / 1000.0 @@ -2042,7 +2069,6 @@ func ReEnrichFile(requestJSON string) (string, error) { return string(jsonBytes), nil } - // MP3/Opus: return metadata map for Dart to use FFmpeg // Don't cleanup cover temp — Dart needs it for FFmpeg embed cleanupCover = false result := map[string]interface{}{ diff --git a/go_backend/library_scan.go b/go_backend/library_scan.go index 20cfd626..ee8874d1 100644 --- a/go_backend/library_scan.go +++ b/go_backend/library_scan.go @@ -293,7 +293,7 @@ func scanAudioFileWithKnownModTimeAndDisplayName(filePath, displayNameHint, scan libraryCoverCacheMu.RLock() coverCacheDir := libraryCoverCacheDir libraryCoverCacheMu.RUnlock() - if coverCacheDir != "" && ext != ".m4a" { + if coverCacheDir != "" { coverPath, err := SaveCoverToCacheWithHint(filePath, displayNameHint, coverCacheDir) if err == nil && coverPath != "" { result.CoverPath = coverPath @@ -373,13 +373,30 @@ func scanFLACFile(filePath string, result *LibraryScanResult) (*LibraryScanResul } func scanM4AFile(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { + metadata, err := ReadM4ATags(filePath) + if err == nil && metadata != nil { + result.TrackName = metadata.Title + result.ArtistName = metadata.Artist + result.AlbumName = metadata.Album + result.AlbumArtist = metadata.AlbumArtist + result.ISRC = metadata.ISRC + result.TrackNumber = metadata.TrackNumber + result.DiscNumber = metadata.DiscNumber + result.ReleaseDate = metadata.Date + if result.ReleaseDate == "" { + result.ReleaseDate = metadata.Year + } + result.Genre = metadata.Genre + } + quality, err := GetM4AQuality(filePath) if err == nil { result.BitDepth = quality.BitDepth result.SampleRate = quality.SampleRate } - return scanFromFilename(filePath, "", result) + applyDefaultLibraryMetadata(filePath, "", result) + return result, nil } func scanMP3File(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { From 12be560cb8f5370d272ff649e2a4492410fb22ba Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 22 Mar 2026 23:01:32 +0700 Subject: [PATCH 140/145] feat: add M4A metadata/cover embed support across all Flutter screens Add FFmpegService.embedMetadataToM4a() for writing tags and cover art into M4A files via FFmpeg. Fix two bugs in the same function: - Remove '-disposition:v:0 attached_pic' which is only valid for Matroska/WebM containers and causes FFmpeg to error on MP4/M4A - Apply same fix to _convertToAlac which had the identical issue Add M4A handling (isM4A branch) to all four embed call-sites: track_metadata_screen (lyrics embed, re-enrich, edit metadata sheet, format conversion), queue_tab, local_album_screen, and downloaded_album_screen. Add 'LYRICS'/'UNSYNCEDLYRICS' to _mapMetadataForTagEmbed so existing lyrics survive a re-enrich cycle on M4A/MP3/Opus files. Preserve existing lyrics before overwriting tags in the edit metadata sheet (best-effort readFileMetadata before FFmpeg pass). Extract mergePlatformMetadataForTagEmbed() into lyrics_metadata_helper to deduplicate the identical metadata-mapping loops that existed in queue_tab, local_album_screen, downloaded_album_screen, and track_metadata_screen. Wire ensureLyricsMetadataForConversion into the format conversion path in track_metadata_screen so lyrics are carried through conversions. Add ISRC and LABEL/ORGANIZATION mappings to _convertToM4aTags. --- lib/screens/downloaded_album_screen.dart | 27 +++--- lib/screens/local_album_screen.dart | 18 ++-- lib/screens/queue_tab.dart | 21 ++-- lib/screens/track_metadata_screen.dart | 71 ++++++++++---- lib/services/ffmpeg_service.dart | 117 +++++++++++++++++++++-- lib/utils/lyrics_metadata_helper.dart | 35 +++++++ 6 files changed, 235 insertions(+), 54 deletions(-) diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index ecbdbbe7..0d40173a 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -946,8 +946,9 @@ class _DownloadedAlbumScreenState extends ConsumerState { String selectedFormat = formats.first; bool isLosslessTarget = selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; - String selectedBitrate = - isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); + String selectedBitrate = isLosslessTarget + ? '320k' + : (selectedFormat == 'Opus' ? '128k' : '320k'); showModalBottomSheet( context: context, @@ -1009,8 +1010,9 @@ class _DownloadedAlbumScreenState extends ConsumerState { isLosslessTarget = format == 'ALAC' || format == 'FLAC'; if (!isLosslessTarget) { - selectedBitrate = - format == 'Opus' ? '128k' : '320k'; + selectedBitrate = format == 'Opus' + ? '128k' + : '320k'; } }); } @@ -1055,11 +1057,8 @@ class _DownloadedAlbumScreenState extends ConsumerState { const SizedBox(width: 6), Text( context.l10n.trackConvertLosslessHint, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith( - color: colorScheme.primary, - ), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colorScheme.primary), ), ], ), @@ -1175,7 +1174,8 @@ class _DownloadedAlbumScreenState extends ConsumerState { int successCount = 0; final total = selected.length; final historyDb = HistoryDatabase.instance; - final newQuality = (targetFormat.toUpperCase() == 'ALAC' || + final newQuality = + (targetFormat.toUpperCase() == 'ALAC' || targetFormat.toUpperCase() == 'FLAC') ? '${targetFormat.toUpperCase()} Lossless' : '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; @@ -1206,12 +1206,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { try { final result = await PlatformBridge.readFileMetadata(item.filePath); if (result['error'] == null) { - result.forEach((key, value) { - if (key == 'error' || value == null) return; - final v = value.toString().trim(); - if (v.isEmpty) return; - metadata[key.toUpperCase()] = v; - }); + mergePlatformMetadataForTagEmbed(target: metadata, source: result); } } catch (_) {} await ensureLyricsMetadataForConversion( diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index bffcb089..5c3c8ea3 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -820,6 +820,11 @@ class _LocalAlbumScreenState extends ConsumerState { final format = item.format?.toLowerCase(); final lowerPath = item.filePath.toLowerCase(); final isMp3 = format == 'mp3' || lowerPath.endsWith('.mp3'); + final isM4A = + format == 'm4a' || + format == 'aac' || + lowerPath.endsWith('.m4a') || + lowerPath.endsWith('.aac'); final isOpus = format == 'opus' || format == 'ogg' || @@ -833,6 +838,12 @@ class _LocalAlbumScreenState extends ConsumerState { coverPath: effectiveCoverPath, metadata: metadata, ); + } else if (isM4A) { + ffmpegResult = await FFmpegService.embedMetadataToM4a( + m4aPath: ffmpegTarget, + coverPath: effectiveCoverPath, + metadata: metadata, + ); } else if (isOpus) { ffmpegResult = await FFmpegService.embedMetadataToOpus( opusPath: ffmpegTarget, @@ -1450,12 +1461,7 @@ class _LocalAlbumScreenState extends ConsumerState { try { final result = await PlatformBridge.readFileMetadata(item.filePath); if (result['error'] == null) { - result.forEach((key, value) { - if (key == 'error' || value == null) return; - final v = value.toString().trim(); - if (v.isEmpty) return; - metadata[key.toUpperCase()] = v; - }); + mergePlatformMetadataForTagEmbed(target: metadata, source: result); } } catch (_) {} await ensureLyricsMetadataForConversion( diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 18a78c16..0e632411 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4400,6 +4400,11 @@ class _QueueTabState extends ConsumerState { final format = item.format?.toLowerCase(); final lowerPath = item.filePath.toLowerCase(); final isMp3 = format == 'mp3' || lowerPath.endsWith('.mp3'); + final isM4A = + format == 'm4a' || + format == 'aac' || + lowerPath.endsWith('.m4a') || + lowerPath.endsWith('.aac'); final isOpus = format == 'opus' || format == 'ogg' || @@ -4413,6 +4418,12 @@ class _QueueTabState extends ConsumerState { coverPath: effectiveCoverPath, metadata: metadata, ); + } else if (isM4A) { + ffmpegResult = await FFmpegService.embedMetadataToM4a( + m4aPath: ffmpegTarget, + coverPath: effectiveCoverPath, + metadata: metadata, + ); } else if (isOpus) { ffmpegResult = await FFmpegService.embedMetadataToOpus( opusPath: ffmpegTarget, @@ -5090,12 +5101,7 @@ class _QueueTabState extends ConsumerState { try { final result = await PlatformBridge.readFileMetadata(item.filePath); if (result['error'] == null) { - result.forEach((key, value) { - if (key == 'error' || value == null) return; - final v = value.toString().trim(); - if (v.isEmpty) return; - metadata[key.toUpperCase()] = v; - }); + mergePlatformMetadataForTagEmbed(target: metadata, source: result); } } catch (_) {} await ensureLyricsMetadataForConversion( @@ -5473,7 +5479,8 @@ class _QueueTabState extends ConsumerState { icon: Icons.download_for_offline_outlined, label: '${context.l10n.queueFlacAction} ($flacEligibleCount)', - onPressed: () => _queueSelectedLocalAsFlac(unifiedItems), + onPressed: () => + _queueSelectedLocalAsFlac(unifiedItems), colorScheme: colorScheme, ), ), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 744acf54..81102a3e 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -20,6 +20,7 @@ import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/services/ffmpeg_service.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/logger.dart'; +import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart'; import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; @@ -1778,6 +1779,7 @@ class _TrackMetadataScreenState extends ConsumerState { final isFlac = lower.endsWith('.flac'); final isMp3 = lower.endsWith('.mp3'); final isOpus = lower.endsWith('.opus') || lower.endsWith('.ogg'); + final isM4A = lower.endsWith('.m4a') || lower.endsWith('.aac'); bool success = false; String? error; @@ -1803,7 +1805,7 @@ class _TrackMetadataScreenState extends ConsumerState { } else { error = result['error']?.toString() ?? l10nFailedToEmbedLyrics; } - } else if (isMp3 || isOpus) { + } else if (isMp3 || isOpus || isM4A) { final metadata = _buildFallbackMetadata(); try { final result = await PlatformBridge.readFileMetadata(workingPath); @@ -1838,6 +1840,12 @@ class _TrackMetadataScreenState extends ConsumerState { coverPath: coverPath, metadata: metadata, ); + } else if (isM4A) { + ffmpegResult = await FFmpegService.embedMetadataToM4a( + m4aPath: workingPath, + coverPath: coverPath, + metadata: metadata, + ); } else { ffmpegResult = await FFmpegService.embedMetadataToOpus( opusPath: workingPath, @@ -2321,6 +2329,12 @@ class _TrackMetadataScreenState extends ConsumerState { coverPath: effectiveCoverPath, metadata: metadata, ); + } else if (lower.endsWith('.m4a') || lower.endsWith('.aac')) { + ffmpegResult = await FFmpegService.embedMetadataToM4a( + m4aPath: ffmpegTarget, + coverPath: effectiveCoverPath, + metadata: metadata, + ); } else if (lower.endsWith('.opus') || lower.endsWith('.ogg')) { ffmpegResult = await FFmpegService.embedMetadataToOpus( opusPath: ffmpegTarget, @@ -2737,6 +2751,8 @@ class _TrackMetadataScreenState extends ConsumerState { put('COPYRIGHT', source['copyright']); put('COMPOSER', source['composer']); put('COMMENT', source['comment']); + put('LYRICS', source['lyrics']); + put('UNSYNCEDLYRICS', source['lyrics']); final trackNumber = source['track_number']; if (trackNumber != null && trackNumber.toString() != '0') { @@ -2796,8 +2812,7 @@ class _TrackMetadataScreenState extends ConsumerState { void _showConvertSheet(BuildContext context) { final currentFormat = _currentFileFormat; - final isLosslessSource = - currentFormat == 'FLAC' || currentFormat == 'M4A'; + final isLosslessSource = currentFormat == 'FLAC' || currentFormat == 'M4A'; // Build available target formats based on source final formats = []; @@ -2879,8 +2894,9 @@ class _TrackMetadataScreenState extends ConsumerState { isLosslessTarget = format == 'ALAC' || format == 'FLAC'; if (!isLosslessTarget) { - selectedBitrate = - format == 'Opus' ? '128k' : '320k'; + selectedBitrate = format == 'Opus' + ? '128k' + : '320k'; } }); } @@ -2929,11 +2945,8 @@ class _TrackMetadataScreenState extends ConsumerState { const SizedBox(width: 6), Text( context.l10n.trackConvertLosslessHint, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith( - color: colorScheme.primary, - ), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colorScheme.primary), ), ], ), @@ -3499,22 +3512,29 @@ class _TrackMetadataScreenState extends ConsumerState { SnackBar(content: Text(context.l10n.trackConvertConverting)), ); + final settings = ref.read(settingsProvider); + final shouldEmbedLyrics = + settings.embedLyrics && settings.lyricsMode != 'external'; final metadata = _buildFallbackMetadata(); try { final result = await PlatformBridge.readFileMetadata(cleanFilePath); if (result['error'] == null) { - result.forEach((key, value) { - if (key == 'error' || value == null) return; - final normalizedValue = value.toString().trim(); - if (normalizedValue.isEmpty) return; - metadata[key.toUpperCase()] = normalizedValue; - }); + mergePlatformMetadataForTagEmbed(target: metadata, source: result); } else { _log.w('readFileMetadata returned error, using fallback metadata'); } } catch (e) { _log.w('readFileMetadata threw, using fallback metadata: $e'); } + await ensureLyricsMetadataForConversion( + metadata: metadata, + sourcePath: cleanFilePath, + shouldEmbedLyrics: shouldEmbedLyrics, + trackName: trackName, + artistName: artistName, + spotifyId: _spotifyId ?? '', + durationMs: (duration ?? 0) * 1000, + ); String? coverPath; try { @@ -4921,6 +4941,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { final lower = widget.filePath.toLowerCase(); final isMp3 = lower.endsWith('.mp3'); final isOpus = lower.endsWith('.opus') || lower.endsWith('.ogg'); + final isM4A = lower.endsWith('.m4a') || lower.endsWith('.aac'); final vorbisMap = {}; if (metadata['title']?.isNotEmpty == true) { @@ -4964,6 +4985,18 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { if (metadata['comment']?.isNotEmpty == true) { vorbisMap['COMMENT'] = metadata['comment']!; } + try { + final existingMetadata = await PlatformBridge.readFileMetadata( + ffmpegTarget, + ); + final existingLyrics = existingMetadata['lyrics']?.toString().trim(); + if (existingLyrics != null && existingLyrics.isNotEmpty) { + vorbisMap['LYRICS'] = existingLyrics; + vorbisMap['UNSYNCEDLYRICS'] = existingLyrics; + } + } catch (_) { + // Lyrics preservation is best-effort. + } String? existingCoverPath = _selectedCoverPath ?? _currentCoverPath; String? extractedCoverPath; @@ -4997,6 +5030,12 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { coverPath: existingCoverPath, metadata: vorbisMap, ); + } else if (isM4A) { + ffmpegResult = await FFmpegService.embedMetadataToM4a( + m4aPath: ffmpegTarget, + coverPath: existingCoverPath, + metadata: vorbisMap, + ); } else if (isOpus) { ffmpegResult = await FFmpegService.embedMetadataToOpus( opusPath: ffmpegTarget, diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index f5608c9f..bf43717c 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1106,6 +1106,88 @@ class FFmpegService { return null; } + static Future embedMetadataToM4a({ + required String m4aPath, + String? coverPath, + Map? metadata, + }) async { + final tempDir = await getTemporaryDirectory(); + final tempOutput = _nextTempEmbedPath(tempDir.path, '.m4a'); + + final cmdBuffer = StringBuffer(); + cmdBuffer.write('-i "$m4aPath" '); + + final hasCover = coverPath != null && await File(coverPath).exists(); + if (hasCover) { + cmdBuffer.write('-i "$coverPath" '); + } + + cmdBuffer.write('-map 0:a '); + cmdBuffer.write('-map_metadata -1 '); + + // For M4A/MP4, cover art is mapped as a video stream and stored in the + // 'covr' atom automatically by FFmpeg. The '-disposition attached_pic' + // flag is only valid for Matroska/WebM containers and must NOT be used here. + if (hasCover) { + cmdBuffer.write('-map 1:v -c:v copy '); + } + + cmdBuffer.write('-c:a copy '); + + if (metadata != null) { + final m4aMetadata = _convertToM4aTags(metadata); + for (final entry in m4aMetadata.entries) { + final sanitizedValue = entry.value.replaceAll('"', '\\"'); + cmdBuffer.write('-metadata ${entry.key}="$sanitizedValue" '); + } + } + + cmdBuffer.write('"$tempOutput" -y'); + + final command = cmdBuffer.toString(); + _log.d( + 'Executing FFmpeg M4A embed command: ${_previewCommandForLog(command)}', + ); + + final result = await _execute(command); + + if (result.success) { + try { + final tempFile = File(tempOutput); + final originalFile = File(m4aPath); + + if (await tempFile.exists()) { + if (await originalFile.exists()) { + await originalFile.delete(); + } + await tempFile.copy(m4aPath); + await tempFile.delete(); + + _log.d('M4A metadata embedded successfully'); + return m4aPath; + } else { + _log.e('Temp M4A output file not found: $tempOutput'); + return null; + } + } catch (e) { + _log.e('Failed to replace M4A file after metadata embed: $e'); + return null; + } + } + + try { + final tempFile = File(tempOutput); + if (await tempFile.exists()) { + await tempFile.delete(); + } + } catch (e) { + _log.w('Failed to cleanup temp M4A file: $e'); + } + + _log.e('M4A Metadata embed failed: ${result.output}'); + return null; + } + static Future _createMetadataBlockPicture(String imagePath) async { try { final file = File(imagePath); @@ -1330,7 +1412,8 @@ class FFmpegService { cmdBuffer.write('-i "$inputPath" '); // Cover art as second input for M4A attached picture - final hasCover = coverPath != null && + final hasCover = + coverPath != null && coverPath.trim().isNotEmpty && await File(coverPath).exists(); if (hasCover) { @@ -1338,8 +1421,10 @@ class FFmpegService { } cmdBuffer.write('-map 0:a '); + // M4A/MP4 containers store cover art in the 'covr' atom automatically. + // '-disposition attached_pic' is only for Matroska/WebM and must NOT be used here. if (hasCover) { - cmdBuffer.write('-map 1:v -c:v copy -disposition:v:0 attached_pic '); + cmdBuffer.write('-map 1:v -c:v copy '); } cmdBuffer.write('-c:a alac '); cmdBuffer.write('-map_metadata -1 '); @@ -1389,7 +1474,8 @@ class FFmpegService { final cmdBuffer = StringBuffer(); cmdBuffer.write('-i "$inputPath" '); - final hasCover = coverPath != null && + final hasCover = + coverPath != null && coverPath.trim().isNotEmpty && await File(coverPath).exists(); if (hasCover) { @@ -1508,9 +1594,7 @@ class FFmpegService { } /// Map Vorbis comment keys to M4A/MP4 metadata tag names for FFmpeg. - static Map _convertToM4aTags( - Map metadata, - ) { + static Map _convertToM4aTags(Map metadata) { final m4aMap = {}; for (final entry in metadata.entries) { @@ -1548,6 +1632,9 @@ class FFmpegService { case 'GENRE': m4aMap['genre'] = value; break; + case 'ISRC': + m4aMap['isrc'] = value; + break; case 'COMPOSER': m4aMap['composer'] = value; break; @@ -1557,6 +1644,10 @@ class FFmpegService { case 'COPYRIGHT': m4aMap['copyright'] = value; break; + case 'LABEL': + case 'ORGANIZATION': + m4aMap['organization'] = value; + break; case 'LYRICS': case 'UNSYNCEDLYRICS': m4aMap['lyrics'] = value; @@ -1648,7 +1739,11 @@ class FFmpegService { final outputPaths = []; final inputExt = audioPath.toLowerCase().split('.').last; // For lossless formats, keep as FLAC; for others, keep original format - final outputExt = (inputExt == 'flac' || inputExt == 'wav' || inputExt == 'ape' || inputExt == 'wv') + final outputExt = + (inputExt == 'flac' || + inputExt == 'wav' || + inputExt == 'ape' || + inputExt == 'wv') ? 'flac' : inputExt; @@ -1681,7 +1776,9 @@ class FFmpegService { cmdBuffer.write('-c:a copy '); } - final artist = track.artist.isNotEmpty ? track.artist : (albumMetadata['artist'] ?? ''); + final artist = track.artist.isNotEmpty + ? track.artist + : (albumMetadata['artist'] ?? ''); final album = albumMetadata['album'] ?? ''; final genre = albumMetadata['genre'] ?? ''; final date = albumMetadata['date'] ?? ''; @@ -1706,7 +1803,9 @@ class FFmpegService { cmdBuffer.write('"$outputPath" -y'); final command = cmdBuffer.toString(); - _log.d('CUE split track ${track.number}: ${_previewCommandForLog(command)}'); + _log.d( + 'CUE split track ${track.number}: ${_previewCommandForLog(command)}', + ); final result = await _execute(command); if (!result.success) { diff --git a/lib/utils/lyrics_metadata_helper.dart b/lib/utils/lyrics_metadata_helper.dart index 2e17c397..c20eb411 100644 --- a/lib/utils/lyrics_metadata_helper.dart +++ b/lib/utils/lyrics_metadata_helper.dart @@ -74,3 +74,38 @@ Future ensureLyricsMetadataForConversion({ metadata['LYRICS'] = lyrics; metadata['UNSYNCEDLYRICS'] = lyrics; } + +void mergePlatformMetadataForTagEmbed({ + required Map target, + required Map source, +}) { + void put(String key, dynamic value) { + final normalized = value?.toString().trim(); + if (normalized == null || normalized.isEmpty) return; + target[key] = normalized; + } + + put('TITLE', source['title']); + put('ARTIST', source['artist']); + put('ALBUM', source['album']); + put('ALBUMARTIST', source['album_artist']); + put('DATE', source['date']); + put('ISRC', source['isrc']); + put('GENRE', source['genre']); + put('ORGANIZATION', source['label']); + put('COPYRIGHT', source['copyright']); + put('COMPOSER', source['composer']); + put('COMMENT', source['comment']); + put('LYRICS', source['lyrics']); + put('UNSYNCEDLYRICS', source['lyrics']); + + final trackNumber = source['track_number']; + if (trackNumber != null && trackNumber.toString() != '0') { + put('TRACKNUMBER', trackNumber); + } + + final discNumber = source['disc_number']; + if (discNumber != null && discNumber.toString() != '0') { + put('DISCNUMBER', discNumber); + } +} From 98fdc0ed7ccb5a158e7bcc6c38937c06d20d04a0 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 22 Mar 2026 23:31:47 +0700 Subject: [PATCH 141/145] feat: restore Tidal HIGH (AAC 320kbps) lossy quality option (closes #242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requested by @okinaau in issue #242 — brings back the ability to download tracks in lossy format for users on low storage devices. HIGH quality fetches the AAC M4A stream directly from the Tidal server (no lossless download + re-encode), then converts to MP3 or Opus via FFmpeg based on the tidalHighFormat setting (mp3_320, opus_256, or opus_128). - go_backend/tidal.go: restore outputExt .m4a, filename logic, duplicate-check guard, HIGH M4A lyrics/LRC handling, and bitDepth=0/sampleRate=44100 for HIGH quality result - settings.dart + settings.g.dart: re-add tidalHighFormat field (default mp3_320) with JSON serialization - settings_provider.dart: re-add setTidalHighFormat(), remove migration that force-migrated HIGH to LOSSLESS - download_queue_provider.dart: restore HIGH conversion logic for both SAF and non-SAF paths using FFmpegService.convertM4aToLossy - download_settings_page.dart: restore Lossy 320kbps quality tile, format sub-picker tile, _getTidalHighFormatLabel helper, and _showTidalHighFormatPicker bottom sheet - l10n: add 10 keys (downloadLossy320, downloadLossyFormat, downloadLossy320Format, downloadLossy320FormatDesc, downloadLossyMp3, downloadLossyMp3Subtitle, downloadLossyOpus256/Subtitle, downloadLossyOpus128/Subtitle) to ARB and all 13 generated files --- go_backend/tidal.go | 40 +- lib/l10n/app_localizations.dart | 60 +++ lib/l10n/app_localizations_de.dart | 32 ++ lib/l10n/app_localizations_en.dart | 32 ++ lib/l10n/app_localizations_es.dart | 32 ++ lib/l10n/app_localizations_fr.dart | 32 ++ lib/l10n/app_localizations_hi.dart | 32 ++ lib/l10n/app_localizations_id.dart | 32 ++ lib/l10n/app_localizations_ja.dart | 32 ++ lib/l10n/app_localizations_ko.dart | 32 ++ lib/l10n/app_localizations_nl.dart | 32 ++ lib/l10n/app_localizations_pt.dart | 32 ++ lib/l10n/app_localizations_ru.dart | 32 ++ lib/l10n/app_localizations_tr.dart | 32 ++ lib/l10n/app_localizations_zh.dart | 32 ++ lib/l10n/arb/app_en.arb | 40 ++ lib/models/settings.dart | 5 + lib/models/settings.g.dart | 2 + lib/providers/download_queue_provider.dart | 370 +++++++++++++----- lib/providers/settings_provider.dart | 9 +- .../settings/download_settings_page.dart | 130 +++++- 21 files changed, 970 insertions(+), 102 deletions(-) diff --git a/go_backend/tidal.go b/go_backend/tidal.go index ea286f22..de8b1039 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -2109,7 +2109,11 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { outputExt := strings.TrimSpace(req.OutputExt) if outputExt == "" { - outputExt = ".flac" + if quality == "HIGH" { + outputExt = ".m4a" + } else { + outputExt = ".flac" + } } else if !strings.HasPrefix(outputExt, ".") { outputExt = "." + outputExt } @@ -2123,7 +2127,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { } m4aPath = outputPath } else { - if outputExt == ".m4a" { + if outputExt == ".m4a" || quality == "HIGH" { filename = sanitizeFilename(filename) + ".m4a" outputPath = filepath.Join(req.OutputDir, filename) m4aPath = outputPath @@ -2136,8 +2140,10 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { if fileInfo, statErr := os.Stat(outputPath); statErr == nil && fileInfo.Size() > 0 { return TidalDownloadResult{FilePath: "EXISTS:" + outputPath}, nil } - if fileInfo, statErr := os.Stat(m4aPath); statErr == nil && fileInfo.Size() > 0 { - return TidalDownloadResult{FilePath: "EXISTS:" + m4aPath}, nil + if quality != "HIGH" { + if fileInfo, statErr := os.Stat(m4aPath); statErr == nil && fileInfo.Size() > 0 { + return TidalDownloadResult{FilePath: "EXISTS:" + m4aPath}, nil + } } } @@ -2293,7 +2299,27 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { fmt.Println("[Tidal] No lyrics available from parallel fetch") } } else if (isSafOutput && actualExt == ".m4a") || (!isSafOutput && strings.HasSuffix(actualOutputPath, ".m4a")) { - fmt.Println("[Tidal] Skipping metadata embedding for M4A file (will be handled after FFmpeg conversion)") + if quality == "HIGH" { + GoLog("[Tidal] HIGH quality M4A - skipping metadata embedding (file from server is already valid)\n") + + if req.EmbedMetadata && req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { + lyricsMode := req.LyricsMode + if lyricsMode == "" { + lyricsMode = "embed" + } + + if !isSafOutput && (lyricsMode == "external" || lyricsMode == "both") { + GoLog("[Tidal] Saving external LRC file for M4A (mode: %s)...\n", lyricsMode) + if lrcPath, lrcErr := SaveLRCFile(actualOutputPath, parallelResult.LyricsLRC); lrcErr != nil { + GoLog("[Tidal] Warning: failed to save LRC file: %v\n", lrcErr) + } else { + GoLog("[Tidal] LRC file saved: %s\n", lrcPath) + } + } + } + } else { + fmt.Println("[Tidal] Skipping metadata embedding for M4A file (will be handled after FFmpeg conversion)") + } } if !isSafOutput { @@ -2302,6 +2328,10 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { bitDepth := downloadInfo.BitDepth sampleRate := downloadInfo.SampleRate + if quality == "HIGH" { + bitDepth = 0 + sampleRate = 44100 + } lyricsLRC := "" if req.EmbedMetadata && req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { lyricsLRC = parallelResult.LyricsLRC diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d8e291ca..d38202cc 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2596,6 +2596,66 @@ abstract class AppLocalizations { /// **'24-bit / up to 192kHz'** String get qualityHiResFlacMaxSubtitle; + /// Quality option label for Tidal lossy 320kbps + /// + /// In en, this message translates to: + /// **'Lossy 320kbps'** + String get downloadLossy320; + + /// Setting title to pick output format for Tidal lossy downloads + /// + /// In en, this message translates to: + /// **'Lossy Format'** + String get downloadLossyFormat; + + /// Title of the Tidal lossy format picker bottom sheet + /// + /// In en, this message translates to: + /// **'Lossy 320kbps Format'** + String get downloadLossy320Format; + + /// Description in the Tidal lossy format picker + /// + /// In en, this message translates to: + /// **'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'** + String get downloadLossy320FormatDesc; + + /// Tidal lossy format option - MP3 320kbps + /// + /// In en, this message translates to: + /// **'MP3 320kbps'** + String get downloadLossyMp3; + + /// Subtitle for MP3 320kbps Tidal lossy option + /// + /// In en, this message translates to: + /// **'Best compatibility, ~10MB per track'** + String get downloadLossyMp3Subtitle; + + /// Tidal lossy format option - Opus 256kbps + /// + /// In en, this message translates to: + /// **'Opus 256kbps'** + String get downloadLossyOpus256; + + /// Subtitle for Opus 256kbps Tidal lossy option + /// + /// In en, this message translates to: + /// **'Best quality Opus, ~8MB per track'** + String get downloadLossyOpus256Subtitle; + + /// Tidal lossy format option - Opus 128kbps + /// + /// In en, this message translates to: + /// **'Opus 128kbps'** + String get downloadLossyOpus128; + + /// Subtitle for Opus 128kbps Tidal lossy option + /// + /// In en, this message translates to: + /// **'Smallest size, ~4MB per track'** + String get downloadLossyOpus128Subtitle; + /// Note about quality availability /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b58fd78c..7d2e6008 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1413,6 +1413,38 @@ class AppLocalizationsDe extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-Bit / bis 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Die eigentliche Qualität hängt von der Verfügbarkeit des Dienstes ab'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 328a4a9d..cc37fb89 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1389,6 +1389,38 @@ class AppLocalizationsEn extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Actual quality depends on track availability from the service'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 6308f8f4..d99eec4d 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1389,6 +1389,38 @@ class AppLocalizationsEs extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Actual quality depends on track availability from the service'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index f16c40f8..5b1c8d81 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1391,6 +1391,38 @@ class AppLocalizationsFr extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Actual quality depends on track availability from the service'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 2022128f..7c92f95c 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1389,6 +1389,38 @@ class AppLocalizationsHi extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Actual quality depends on track availability from the service'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index dce7f058..8b088f35 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1396,6 +1396,38 @@ class AppLocalizationsId extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / hingga 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Kualitas sebenarnya tergantung ketersediaan lagu dari layanan'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 5f1fc709..5c454b45 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1379,6 +1379,38 @@ class AppLocalizationsJa extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / 最大 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => '実際の品質はサービスからのトラックの可用性に依存します'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 5d4f256f..3dc6f4f9 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1369,6 +1369,38 @@ class AppLocalizationsKo extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Actual quality depends on track availability from the service'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index ef14b913..74e93a95 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1389,6 +1389,38 @@ class AppLocalizationsNl extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Actual quality depends on track availability from the service'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index d1fb718a..b53d2e07 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1389,6 +1389,38 @@ class AppLocalizationsPt extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Actual quality depends on track availability from the service'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 47398466..810b35a9 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1414,6 +1414,38 @@ class AppLocalizationsRu extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-бит / до 192кГц'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Фактическое качество зависит от доступности треков в сервисе'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 7c77c93e..63a54638 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1401,6 +1401,38 @@ class AppLocalizationsTr extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Actual quality depends on track availability from the service'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 5ce4df91..31adb1ff 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1389,6 +1389,38 @@ class AppLocalizationsZh extends AppLocalizations { @override String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + @override + String get downloadLossy320 => 'Lossy 320kbps'; + + @override + String get downloadLossyFormat => 'Lossy Format'; + + @override + String get downloadLossy320Format => 'Lossy 320kbps Format'; + + @override + String get downloadLossy320FormatDesc => + 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; + + @override + String get downloadLossyMp3 => 'MP3 320kbps'; + + @override + String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + + @override + String get downloadLossyOpus256 => 'Opus 256kbps'; + + @override + String get downloadLossyOpus256Subtitle => + 'Best quality Opus, ~8MB per track'; + + @override + String get downloadLossyOpus128 => 'Opus 128kbps'; + + @override + String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; + @override String get qualityNote => 'Actual quality depends on track availability from the service'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 69aa2068..7797e048 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1825,6 +1825,46 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, + "downloadLossy320": "Lossy 320kbps", + "@downloadLossy320": { + "description": "Quality option label for Tidal lossy 320kbps" + }, + "downloadLossyFormat": "Lossy Format", + "@downloadLossyFormat": { + "description": "Setting title to pick output format for Tidal lossy downloads" + }, + "downloadLossy320Format": "Lossy 320kbps Format", + "@downloadLossy320Format": { + "description": "Title of the Tidal lossy format picker bottom sheet" + }, + "downloadLossy320FormatDesc": "Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.", + "@downloadLossy320FormatDesc": { + "description": "Description in the Tidal lossy format picker" + }, + "downloadLossyMp3": "MP3 320kbps", + "@downloadLossyMp3": { + "description": "Tidal lossy format option - MP3 320kbps" + }, + "downloadLossyMp3Subtitle": "Best compatibility, ~10MB per track", + "@downloadLossyMp3Subtitle": { + "description": "Subtitle for MP3 320kbps Tidal lossy option" + }, + "downloadLossyOpus256": "Opus 256kbps", + "@downloadLossyOpus256": { + "description": "Tidal lossy format option - Opus 256kbps" + }, + "downloadLossyOpus256Subtitle": "Best quality Opus, ~8MB per track", + "@downloadLossyOpus256Subtitle": { + "description": "Subtitle for Opus 256kbps Tidal lossy option" + }, + "downloadLossyOpus128": "Opus 128kbps", + "@downloadLossyOpus128": { + "description": "Tidal lossy format option - Opus 128kbps" + }, + "downloadLossyOpus128Subtitle": "Smallest size, ~4MB per track", + "@downloadLossyOpus128Subtitle": { + "description": "Subtitle for Opus 128kbps Tidal lossy option" + }, "qualityNote": "Actual quality depends on track availability from the service", "@qualityNote": { "description": "Note about quality availability" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index c8848ab7..32b3b76c 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -39,6 +39,8 @@ class AppSettings { final bool showExtensionStore; final String locale; final String lyricsMode; + final String + tidalHighFormat; // Format for Tidal HIGH quality: 'mp3_320', 'opus_256', or 'opus_128' final int youtubeOpusBitrate; // YouTube Opus bitrate (supported: 128/256/320 kbps) final int @@ -116,6 +118,7 @@ class AppSettings { this.showExtensionStore = true, this.locale = 'system', this.lyricsMode = 'embed', + this.tidalHighFormat = 'mp3_320', this.youtubeOpusBitrate = 256, this.youtubeMp3Bitrate = 320, this.useAllFilesAccess = false, @@ -181,6 +184,7 @@ class AppSettings { bool? showExtensionStore, String? locale, String? lyricsMode, + String? tidalHighFormat, int? youtubeOpusBitrate, int? youtubeMp3Bitrate, bool? useAllFilesAccess, @@ -245,6 +249,7 @@ class AppSettings { showExtensionStore: showExtensionStore ?? this.showExtensionStore, locale: locale ?? this.locale, lyricsMode: lyricsMode ?? this.lyricsMode, + tidalHighFormat: tidalHighFormat ?? this.tidalHighFormat, youtubeOpusBitrate: youtubeOpusBitrate ?? this.youtubeOpusBitrate, youtubeMp3Bitrate: youtubeMp3Bitrate ?? this.youtubeMp3Bitrate, useAllFilesAccess: useAllFilesAccess ?? this.useAllFilesAccess, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 026569dd..85244329 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -45,6 +45,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( showExtensionStore: json['showExtensionStore'] as bool? ?? true, locale: json['locale'] as String? ?? 'system', lyricsMode: json['lyricsMode'] as String? ?? 'embed', + tidalHighFormat: json['tidalHighFormat'] as String? ?? 'mp3_320', youtubeOpusBitrate: (json['youtubeOpusBitrate'] as num?)?.toInt() ?? 256, youtubeMp3Bitrate: (json['youtubeMp3Bitrate'] as num?)?.toInt() ?? 320, useAllFilesAccess: json['useAllFilesAccess'] as bool? ?? false, @@ -121,6 +122,7 @@ Map _$AppSettingsToJson( 'showExtensionStore': instance.showExtensionStore, 'locale': instance.locale, 'lyricsMode': instance.lyricsMode, + 'tidalHighFormat': instance.tidalHighFormat, 'youtubeOpusBitrate': instance.youtubeOpusBitrate, 'youtubeMp3Bitrate': instance.youtubeMp3Bitrate, 'useAllFilesAccess': instance.useAllFilesAccess, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 4a308850..c07f50a6 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1927,7 +1927,7 @@ class DownloadQueueNotifier extends Notifier { return '.opus'; } if (service.toLowerCase() == 'tidal' && quality == 'HIGH') { - return '.flac'; // HIGH quality no longer available; fallback to FLAC + return '.m4a'; } return '.flac'; } @@ -4163,50 +4163,72 @@ class DownloadQueueNotifier extends Notifier { final currentFilePath = filePath; if (isContentUriPath && effectiveSafMode) { - _log.d('M4A file detected (SAF), converting to FLAC...'); - final tempPath = await _copySafToTemp(currentFilePath); - if (tempPath != null) { - String? flacPath; - try { - final length = await File(tempPath).length(); - if (length < 1024) { - _log.w('Temp M4A is too small (<1KB), skipping conversion'); - } else { + if (quality == 'HIGH') { + final tidalHighFormat = settings.tidalHighFormat; + _log.i( + 'Tidal HIGH quality (SAF), converting M4A to $tidalHighFormat...', + ); + + final tempPath = await _copySafToTemp(currentFilePath); + if (tempPath != null) { + String? convertedPath; + try { updateItemStatus( item.id, DownloadStatus.downloading, progress: 0.95, ); - flacPath = await FFmpegService.convertM4aToFlac(tempPath); - if (flacPath != null) { - _log.d('Converted to FLAC (temp): $flacPath'); - _log.d('Embedding metadata and cover to converted FLAC...'); - final finalTrack = _buildTrackForMetadataEmbedding( - trackToDownload, - result, - resolvedAlbumArtist, + + final format = + tidalHighFormat.startsWith('opus') ? 'opus' : 'mp3'; + convertedPath = await FFmpegService.convertM4aToLossy( + tempPath, + format: format, + bitrate: tidalHighFormat, + deleteOriginal: false, + ); + + if (convertedPath != null) { + _log.i( + 'Successfully converted M4A to $format (temp): $convertedPath', + ); + _log.i('Embedding metadata to $format...'); + updateItemStatus( + item.id, + DownloadStatus.downloading, + progress: 0.99, ); final backendGenre = result['genre'] as String?; final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; - await _embedMetadataAndCover( - flacPath, - finalTrack, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - writeExternalLrc: false, - ); + if (format == 'mp3') { + await _embedMetadataToMp3( + convertedPath, + trackToDownload, + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + ); + } else { + await _embedMetadataToOpus( + convertedPath, + trackToDownload, + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + ); + } - final newFileName = '${safBaseName ?? 'track'}.flac'; + final newExt = format == 'opus' ? '.opus' : '.mp3'; + final newFileName = '${safBaseName ?? 'track'}$newExt'; final newUri = await _writeTempToSaf( treeUri: settings.downloadTreeUri, relativeDir: effectiveOutputDir, fileName: newFileName, - mimeType: _mimeTypeForExt('.flac'), - srcPath: flacPath, + mimeType: _mimeTypeForExt(newExt), + srcPath: convertedPath, ); if (newUri != null) { @@ -4215,60 +4237,57 @@ class DownloadQueueNotifier extends Notifier { } filePath = newUri; finalSafFileName = newFileName; + final bitrateDisplay = tidalHighFormat.contains('_') + ? '${tidalHighFormat.split('_').last}kbps' + : '320kbps'; + actualQuality = '${format.toUpperCase()} $bitrateDisplay'; } else { - _log.w('Failed to write FLAC to SAF, keeping M4A'); + _log.w( + 'Failed to write converted $format to SAF, keeping M4A', + ); + actualQuality = 'AAC 320kbps'; } } else { - _log.w('FFmpeg conversion returned null, keeping M4A file'); + _log.w( + 'M4A to $format conversion failed, keeping M4A file', + ); + actualQuality = 'AAC 320kbps'; + } + } catch (e) { + _log.w('SAF M4A conversion failed: $e'); + actualQuality = 'AAC 320kbps'; + } finally { + try { + await File(tempPath).delete(); + } catch (_) {} + if (convertedPath != null) { + try { + await File(convertedPath).delete(); + } catch (_) {} } } - } catch (e) { - _log.w('SAF M4A->FLAC conversion failed: $e'); - } finally { - // Clean up temp files - try { - await File(tempPath).delete(); - } catch (_) {} - if (flacPath != null) { - try { - await File(flacPath).delete(); - } catch (_) {} - } } - } - } else { - _log.d( - 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', - ); - - try { - final file = File(currentFilePath); - if (!await file.exists()) { - _log.e('File does not exist at path: $filePath'); - } else { - final length = await file.length(); - _log.i('File size before conversion: ${length / 1024} KB'); - - if (length < 1024) { - _log.w( - 'File is too small (<1KB), skipping conversion. Download might be corrupt.', - ); - } else { - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.95, - ); - final flacPath = await FFmpegService.convertM4aToFlac( - currentFilePath, - ); - - if (flacPath != null) { - filePath = flacPath; - _log.d('Converted to FLAC: $flacPath'); - - _log.d('Embedding metadata and cover to converted FLAC...'); - try { + } else { + _log.d('M4A file detected (SAF), converting to FLAC...'); + final tempPath = await _copySafToTemp(currentFilePath); + if (tempPath != null) { + String? flacPath; + try { + final length = await File(tempPath).length(); + if (length < 1024) { + _log.w('Temp M4A is too small (<1KB), skipping conversion'); + } else { + updateItemStatus( + item.id, + DownloadStatus.downloading, + progress: 0.95, + ); + flacPath = await FFmpegService.convertM4aToFlac(tempPath); + if (flacPath != null) { + _log.d('Converted to FLAC (temp): $flacPath'); + _log.d( + 'Embedding metadata and cover to converted FLAC...', + ); final finalTrack = _buildTrackForMetadataEmbedding( trackToDownload, result, @@ -4279,32 +4298,199 @@ class DownloadQueueNotifier extends Notifier { final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; - if (backendGenre != null || - backendLabel != null || - backendCopyright != null) { - _log.d( - 'Extended metadata from backend - Genre: $backendGenre, Label: $backendLabel, Copyright: $backendCopyright', - ); - } - await _embedMetadataAndCover( flacPath, finalTrack, genre: backendGenre ?? genre, label: backendLabel ?? label, copyright: backendCopyright, + writeExternalLrc: false, + ); + + final newFileName = '${safBaseName ?? 'track'}.flac'; + final newUri = await _writeTempToSaf( + treeUri: settings.downloadTreeUri, + relativeDir: effectiveOutputDir, + fileName: newFileName, + mimeType: _mimeTypeForExt('.flac'), + srcPath: flacPath, + ); + + if (newUri != null) { + if (newUri != currentFilePath) { + await _deleteSafFile(currentFilePath); + } + filePath = newUri; + finalSafFileName = newFileName; + } else { + _log.w('Failed to write FLAC to SAF, keeping M4A'); + } + } else { + _log.w( + 'FFmpeg conversion returned null, keeping M4A file', ); - _log.d('Metadata and cover embedded successfully'); - } catch (e) { - _log.w('Warning: Failed to embed metadata/cover: $e'); } - } else { - _log.w('FFmpeg conversion returned null, keeping M4A file'); + } + } catch (e) { + _log.w('SAF M4A->FLAC conversion failed: $e'); + } finally { + try { + await File(tempPath).delete(); + } catch (_) {} + if (flacPath != null) { + try { + await File(flacPath).delete(); + } catch (_) {} } } } - } catch (e) { - _log.w('FFmpeg conversion process failed: $e, keeping M4A file'); + } + } else { + if (quality == 'HIGH') { + final tidalHighFormat = settings.tidalHighFormat; + _log.i( + 'Tidal HIGH quality download, converting M4A to $tidalHighFormat...', + ); + + try { + updateItemStatus( + item.id, + DownloadStatus.downloading, + progress: 0.95, + ); + + final format = + tidalHighFormat.startsWith('opus') ? 'opus' : 'mp3'; + final convertedPath = await FFmpegService.convertM4aToLossy( + currentFilePath, + format: format, + bitrate: tidalHighFormat, + deleteOriginal: true, + ); + + if (convertedPath != null) { + filePath = convertedPath; + final bitrateDisplay = tidalHighFormat.contains('_') + ? '${tidalHighFormat.split('_').last}kbps' + : '320kbps'; + actualQuality = '${format.toUpperCase()} $bitrateDisplay'; + _log.i( + 'Successfully converted M4A to $format: $convertedPath', + ); + + _log.i('Embedding metadata to $format...'); + updateItemStatus( + item.id, + DownloadStatus.downloading, + progress: 0.99, + ); + + final backendGenre = result['genre'] as String?; + final backendLabel = result['label'] as String?; + final backendCopyright = result['copyright'] as String?; + + if (format == 'mp3') { + await _embedMetadataToMp3( + convertedPath, + trackToDownload, + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + ); + } else { + await _embedMetadataToOpus( + convertedPath, + trackToDownload, + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + ); + } + _log.d('Metadata embedded successfully'); + } else { + _log.w('M4A to $format conversion failed, keeping M4A file'); + actualQuality = 'AAC 320kbps'; + } + } catch (e) { + _log.w('M4A conversion process failed: $e, keeping M4A file'); + actualQuality = 'AAC 320kbps'; + } + } else { + _log.d( + 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', + ); + + try { + final file = File(currentFilePath); + if (!await file.exists()) { + _log.e('File does not exist at path: $filePath'); + } else { + final length = await file.length(); + _log.i('File size before conversion: ${length / 1024} KB'); + + if (length < 1024) { + _log.w( + 'File is too small (<1KB), skipping conversion. Download might be corrupt.', + ); + } else { + updateItemStatus( + item.id, + DownloadStatus.downloading, + progress: 0.95, + ); + final flacPath = await FFmpegService.convertM4aToFlac( + currentFilePath, + ); + + if (flacPath != null) { + filePath = flacPath; + _log.d('Converted to FLAC: $flacPath'); + + _log.d( + 'Embedding metadata and cover to converted FLAC...', + ); + try { + final finalTrack = _buildTrackForMetadataEmbedding( + trackToDownload, + result, + resolvedAlbumArtist, + ); + + final backendGenre = result['genre'] as String?; + final backendLabel = result['label'] as String?; + final backendCopyright = result['copyright'] as String?; + + if (backendGenre != null || + backendLabel != null || + backendCopyright != null) { + _log.d( + 'Extended metadata from backend - Genre: $backendGenre, Label: $backendLabel, Copyright: $backendCopyright', + ); + } + + await _embedMetadataAndCover( + flacPath, + finalTrack, + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + ); + _log.d('Metadata and cover embedded successfully'); + } catch (e) { + _log.w('Warning: Failed to embed metadata/cover: $e'); + } + } else { + _log.w( + 'FFmpeg conversion returned null, keeping M4A file', + ); + } + } + } + } catch (e) { + _log.w( + 'FFmpeg conversion process failed: $e, keeping M4A file', + ); + } } } } else if (metadataEmbeddingEnabled && diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 44251a03..a32277e7 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -117,10 +117,6 @@ class SettingsNotifier extends Notifier { useCustomSpotifyCredentials: false, ); } - // Migration 6: Tidal HIGH quality removed — migrate to LOSSLESS - if (state.audioQuality == 'HIGH') { - state = state.copyWith(audioQuality: 'LOSSLESS'); - } state = state.copyWith(lastSeenVersion: AppInfo.version); await prefs.setInt(_migrationVersionKey, _currentMigrationVersion); await _saveSettings(); @@ -455,6 +451,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setTidalHighFormat(String format) { + state = state.copyWith(tidalHighFormat: format); + _saveSettings(); + } + void setYoutubeOpusBitrate(int bitrate) { final normalized = _normalizeYouTubeOpusBitrate(bitrate); state = state.copyWith(youtubeOpusBitrate: normalized); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 218871f0..d10851e2 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -301,6 +301,7 @@ class _DownloadSettingsPageState extends ConsumerState { final topPadding = normalizedHeaderTopPadding(context); final isBuiltInService = _builtInServices.contains(settings.defaultService); + final isTidalService = settings.defaultService == 'tidal'; return PopScope( canPop: true, @@ -408,8 +409,37 @@ class _DownloadSettingsPageState extends ConsumerState { onTap: () => ref .read(settingsProvider.notifier) .setAudioQuality('HI_RES_LOSSLESS'), - showDivider: false, + showDivider: isTidalService, ), + // Lossy 320kbps option (Tidal only) - downloads M4A AAC from server, converts to MP3/Opus + if (isTidalService) + _QualityOption( + title: context.l10n.downloadLossy320, + subtitle: _getTidalHighFormatLabel( + context, + settings.tidalHighFormat, + ), + isSelected: settings.audioQuality == 'HIGH', + onTap: () => ref + .read(settingsProvider.notifier) + .setAudioQuality('HIGH'), + showDivider: false, + ), + if (isTidalService && settings.audioQuality == 'HIGH') + SettingsItem( + icon: Icons.tune, + title: context.l10n.downloadLossyFormat, + subtitle: _getTidalHighFormatLabel( + context, + settings.tidalHighFormat, + ), + onTap: () => _showTidalHighFormatPicker( + context, + ref, + settings.tidalHighFormat, + ), + showDivider: false, + ), ], if (!isBuiltInService) ...[ Padding( @@ -1561,6 +1591,104 @@ class _DownloadSettingsPageState extends ConsumerState { return normalized.replaceAll(RegExp(r'[^a-z0-9\-_]'), ''); } + String _getTidalHighFormatLabel(BuildContext context, String format) { + switch (format) { + case 'mp3_320': + return context.l10n.downloadLossyMp3; + case 'opus_256': + return context.l10n.downloadLossyOpus256; + case 'opus_128': + return context.l10n.downloadLossyOpus128; + default: + return context.l10n.downloadLossyMp3; + } + } + + void _showTidalHighFormatPicker( + BuildContext context, + WidgetRef ref, + String current, + ) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + context.l10n.downloadLossy320Format, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.downloadLossy320FormatDesc, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ListTile( + leading: const Icon(Icons.audiotrack), + title: Text(context.l10n.downloadLossyMp3), + subtitle: Text(context.l10n.downloadLossyMp3Subtitle), + trailing: current == 'mp3_320' + ? Icon(Icons.check, color: colorScheme.primary) + : null, + onTap: () { + ref + .read(settingsProvider.notifier) + .setTidalHighFormat('mp3_320'); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.graphic_eq), + title: Text(context.l10n.downloadLossyOpus256), + subtitle: Text(context.l10n.downloadLossyOpus256Subtitle), + trailing: current == 'opus_256' + ? Icon(Icons.check, color: colorScheme.primary) + : null, + onTap: () { + ref + .read(settingsProvider.notifier) + .setTidalHighFormat('opus_256'); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.graphic_eq), + title: Text(context.l10n.downloadLossyOpus128), + subtitle: Text(context.l10n.downloadLossyOpus128Subtitle), + trailing: current == 'opus_128' + ? Icon(Icons.check, color: colorScheme.primary) + : null, + onTap: () { + ref + .read(settingsProvider.notifier) + .setTidalHighFormat('opus_128'); + Navigator.pop(context); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + void _showYoutubeBitratePicker({ required BuildContext context, required String title, From 4f365ca7fe10b4be349bb9d6ea48cb4f67b27e59 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 25 Mar 2026 13:51:38 +0700 Subject: [PATCH 142/145] feat: add built-in Tidal/Qobuz search with recommended service picker - Add SearchAll() for Tidal and Qobuz in Go backend (tracks, artists, albums) - Add searchTidalAll/searchQobuzAll platform routing for Android and iOS - Add Tidal/Qobuz options to search provider dropdown in home tab - Show (Recommended) label and auto-select service in download picker --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 22 +++ go_backend/exports.go | 30 ++++ go_backend/qobuz.go | 128 ++++++++++++++++++ go_backend/tidal.go | 115 ++++++++++++++++ ios/Runner/AppDelegate.swift | 20 +++ lib/main.dart | 10 +- lib/providers/download_queue_provider.dart | 25 +++- lib/providers/extension_provider.dart | 16 +++ lib/providers/settings_provider.dart | 4 + lib/providers/track_provider.dart | 93 +++++++++---- lib/screens/home_tab.dart | 99 +++++++++++++- .../settings/download_settings_page.dart | 8 +- .../settings/library_settings_page.dart | 58 +++++--- lib/screens/setup_screen.dart | 9 +- lib/services/platform_bridge.dart | 36 +++++ lib/services/share_intent_service.dart | 23 ++-- lib/services/update_checker.dart | 66 +++++---- lib/widgets/download_service_picker.dart | 14 +- pubspec.lock | 40 ++++++ pubspec.yaml | 1 + 20 files changed, 720 insertions(+), 97 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index ada75b95..3fba0054 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -2642,6 +2642,28 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } + // Tidal search API + "searchTidalAll" -> { + val query = call.argument("query") ?: "" + val trackLimit = call.argument("track_limit") ?: 15 + val artistLimit = call.argument("artist_limit") ?: 2 + val filter = call.argument("filter") ?: "" + val response = withContext(Dispatchers.IO) { + Gobackend.searchTidalAll(query, trackLimit.toLong(), artistLimit.toLong(), filter) + } + result.success(response) + } + // Qobuz search API + "searchQobuzAll" -> { + val query = call.argument("query") ?: "" + val trackLimit = call.argument("track_limit") ?: 15 + val artistLimit = call.argument("artist_limit") ?: 2 + val filter = call.argument("filter") ?: "" + val response = withContext(Dispatchers.IO) { + Gobackend.searchQobuzAll(query, trackLimit.toLong(), artistLimit.toLong(), filter) + } + result.success(response) + } "getDeezerRelatedArtists" -> { val artistId = call.argument("artist_id") ?: "" val limit = call.argument("limit") ?: 12 diff --git a/go_backend/exports.go b/go_backend/exports.go index d0a86ccc..457a3e6e 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1147,6 +1147,36 @@ func SearchDeezerAll(query string, trackLimit, artistLimit int, filter string) ( return string(jsonBytes), nil } +func SearchTidalAll(query string, trackLimit, artistLimit int, filter string) (string, error) { + downloader := NewTidalDownloader() + results, err := downloader.SearchAll(query, trackLimit, artistLimit, filter) + if err != nil { + return "", err + } + + jsonBytes, err := json.Marshal(results) + if err != nil { + return "", err + } + + return string(jsonBytes), nil +} + +func SearchQobuzAll(query string, trackLimit, artistLimit int, filter string) (string, error) { + downloader := NewQobuzDownloader() + results, err := downloader.SearchAll(query, trackLimit, artistLimit, filter) + if err != nil { + return "", err + } + + jsonBytes, err := json.Marshal(results) + if err != nil { + return "", err + } + + return string(jsonBytes), nil +} + func GetDeezerRelatedArtists(artistID string, limit int) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 2a4d35c8..dc7d160a 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -1307,6 +1307,134 @@ func (q *QobuzDownloader) SearchTracks(query string, limit int) ([]ExtTrackMetad return results, nil } +// SearchAll searches Qobuz for tracks, artists, and albums matching the query. +// Returns results in the same SearchAllResult format as Deezer's SearchAll. +func (q *QobuzDownloader) SearchAll(query string, trackLimit, artistLimit int, filter string) (*SearchAllResult, error) { + GoLog("[Qobuz] SearchAll: query=%q, trackLimit=%d, artistLimit=%d, filter=%q\n", query, trackLimit, artistLimit, filter) + + cleanQuery := strings.TrimSpace(query) + if cleanQuery == "" { + return nil, fmt.Errorf("empty qobuz search query") + } + + albumLimit := 5 + + if filter != "" { + switch filter { + case "track": + trackLimit = 50 + artistLimit = 0 + albumLimit = 0 + case "artist": + trackLimit = 0 + artistLimit = 20 + albumLimit = 0 + case "album": + trackLimit = 0 + artistLimit = 0 + albumLimit = 20 + } + } + + result := &SearchAllResult{ + Tracks: make([]TrackMetadata, 0, trackLimit), + Artists: make([]SearchArtistResult, 0, artistLimit), + Albums: make([]SearchAlbumResult, 0, albumLimit), + Playlists: make([]SearchPlaylistResult, 0), + } + + if trackLimit > 0 { + tracks, err := q.searchQobuzTracksWithFallback(cleanQuery, trackLimit) + if err != nil { + GoLog("[Qobuz] Track search failed: %v\n", err) + return nil, fmt.Errorf("qobuz track search failed: %w", err) + } + GoLog("[Qobuz] Got %d tracks from API\n", len(tracks)) + for i := range tracks { + result.Tracks = append(result.Tracks, qobuzTrackToTrackMetadata(&tracks[i])) + } + } + + if artistLimit > 0 { + searchURL := fmt.Sprintf("https://www.qobuz.com/api.json/0.2/artist/search?query=%s&limit=%d&app_id=%s", + url.QueryEscape(cleanQuery), artistLimit, q.appID) + req, err := http.NewRequest("GET", searchURL, nil) + if err == nil { + resp, reqErr := DoRequestWithUserAgent(q.client, req) + if reqErr == nil { + defer resp.Body.Close() + if resp.StatusCode == 200 { + var artistResp struct { + Artists struct { + Items []struct { + ID int64 `json:"id"` + Name string `json:"name"` + Image qobuzImageSet `json:"image"` + } `json:"items"` + } `json:"artists"` + } + if decErr := json.NewDecoder(resp.Body).Decode(&artistResp); decErr == nil { + GoLog("[Qobuz] Got %d artists from API\n", len(artistResp.Artists.Items)) + for _, artist := range artistResp.Artists.Items { + imageURL := qobuzFirstNonEmpty(artist.Image.Large, artist.Image.Small, artist.Image.Thumbnail) + result.Artists = append(result.Artists, SearchArtistResult{ + ID: qobuzPrefixedNumericID(artist.ID), + Name: strings.TrimSpace(artist.Name), + Images: imageURL, + }) + } + } else { + GoLog("[Qobuz] Artist search decode failed: %v\n", decErr) + } + } + } else { + GoLog("[Qobuz] Artist search request failed: %v\n", reqErr) + } + } + } + + if albumLimit > 0 { + searchURL := fmt.Sprintf("https://www.qobuz.com/api.json/0.2/album/search?query=%s&limit=%d&app_id=%s", + url.QueryEscape(cleanQuery), albumLimit, q.appID) + req, err := http.NewRequest("GET", searchURL, nil) + if err == nil { + resp, reqErr := DoRequestWithUserAgent(q.client, req) + if reqErr == nil { + defer resp.Body.Close() + if resp.StatusCode == 200 { + var albumResp struct { + Albums struct { + Items []qobuzAlbumDetails `json:"items"` + } `json:"albums"` + } + if decErr := json.NewDecoder(resp.Body).Decode(&albumResp); decErr == nil { + GoLog("[Qobuz] Got %d albums from API\n", len(albumResp.Albums.Items)) + for i := range albumResp.Albums.Items { + album := &albumResp.Albums.Items[i] + result.Albums = append(result.Albums, SearchAlbumResult{ + ID: qobuzPrefixedID(album.ID), + Name: strings.TrimSpace(album.Title), + Artists: qobuzArtistsDisplayName(album.Artists, album.Artist.Name), + Images: qobuzAlbumImage(album), + ReleaseDate: qobuzNormalizeReleaseDate(album.ReleaseDateOriginal), + TotalTracks: album.TracksCount, + AlbumType: qobuzNormalizeAlbumType(album.ReleaseType, album.ProductType, album.TracksCount), + }) + } + } else { + GoLog("[Qobuz] Album search decode failed: %v\n", decErr) + } + } + } else { + GoLog("[Qobuz] Album search request failed: %v\n", reqErr) + } + } + } + + GoLog("[Qobuz] SearchAll complete: %d tracks, %d artists, %d albums\n", len(result.Tracks), len(result.Artists), len(result.Albums)) + return result, nil +} + func (q *QobuzDownloader) SearchTrackByMetadataWithDuration(trackName, artistName string, expectedDurationSec int) (*QobuzTrack, error) { queries := []string{} diff --git a/go_backend/tidal.go b/go_backend/tidal.go index de8b1039..34de563e 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -874,6 +874,121 @@ func (t *TidalDownloader) SearchTracks(query string, limit int) ([]ExtTrackMetad return results, nil } +// SearchAll searches Tidal for tracks, artists, and albums matching the query. +// Returns results in the same SearchAllResult format as Deezer's SearchAll. +func (t *TidalDownloader) SearchAll(query string, trackLimit, artistLimit int, filter string) (*SearchAllResult, error) { + GoLog("[Tidal] SearchAll: query=%q, trackLimit=%d, artistLimit=%d, filter=%q\n", query, trackLimit, artistLimit, filter) + + cleanQuery := strings.TrimSpace(query) + if cleanQuery == "" { + return nil, fmt.Errorf("empty tidal search query") + } + + albumLimit := 5 + + if filter != "" { + switch filter { + case "track": + trackLimit = 50 + artistLimit = 0 + albumLimit = 0 + case "artist": + trackLimit = 0 + artistLimit = 20 + albumLimit = 0 + case "album": + trackLimit = 0 + artistLimit = 0 + albumLimit = 20 + } + } + + result := &SearchAllResult{ + Tracks: make([]TrackMetadata, 0, trackLimit), + Artists: make([]SearchArtistResult, 0, artistLimit), + Albums: make([]SearchAlbumResult, 0, albumLimit), + Playlists: make([]SearchPlaylistResult, 0), + } + + if trackLimit > 0 { + page, err := t.getTrackSearchPage(cleanQuery, trackLimit) + if err != nil { + GoLog("[Tidal] Track search failed: %v\n", err) + return nil, fmt.Errorf("tidal track search failed: %w", err) + } + GoLog("[Tidal] Got %d tracks from API\n", len(page.Items)) + for i := range page.Items { + result.Tracks = append(result.Tracks, tidalTrackToTrackMetadata(&page.Items[i])) + } + } + + if artistLimit > 0 { + requestURL := tidalBuildMetadataURL("search/artists", url.Values{ + "query": {cleanQuery}, + "limit": {strconv.Itoa(artistLimit)}, + "offset": {"0"}, + }) + var artistResp struct { + Items []struct { + ID int64 `json:"id"` + Name string `json:"name"` + Picture string `json:"picture"` + Popularity int `json:"popularity"` + URL string `json:"url"` + } `json:"items"` + } + if err := t.getTidalMetadataJSON(requestURL, &artistResp); err == nil { + GoLog("[Tidal] Got %d artists from API\n", len(artistResp.Items)) + for _, artist := range artistResp.Items { + result.Artists = append(result.Artists, SearchArtistResult{ + ID: tidalPrefixedNumericID(artist.ID), + Name: strings.TrimSpace(artist.Name), + Images: tidalImageURL(artist.Picture, "750x750"), + Followers: 0, + Popularity: artist.Popularity, + }) + } + } else { + GoLog("[Tidal] Artist search failed: %v\n", err) + } + } + + if albumLimit > 0 { + requestURL := tidalBuildMetadataURL("search/albums", url.Values{ + "query": {cleanQuery}, + "limit": {strconv.Itoa(albumLimit)}, + "offset": {"0"}, + }) + var albumResp struct { + Items []tidalPublicAlbum `json:"items"` + } + if err := t.getTidalMetadataJSON(requestURL, &albumResp); err == nil { + GoLog("[Tidal] Got %d albums from API\n", len(albumResp.Items)) + for i := range albumResp.Items { + album := &albumResp.Items[i] + albumType := strings.ToLower(strings.TrimSpace(album.Type)) + if albumType == "" { + albumType = "album" + } + result.Albums = append(result.Albums, SearchAlbumResult{ + ID: tidalPrefixedNumericID(album.ID), + Name: strings.TrimSpace(album.Title), + Artists: tidalAlbumArtistsDisplay(album), + Images: tidalImageURL(album.Cover, "1280x1280"), + ReleaseDate: strings.TrimSpace(album.ReleaseDate), + TotalTracks: album.NumberOfTracks, + AlbumType: albumType, + }) + } + } else { + GoLog("[Tidal] Album search failed: %v\n", err) + } + } + + GoLog("[Tidal] SearchAll complete: %d tracks, %d artists, %d albums\n", len(result.Tracks), len(result.Artists), len(result.Albums)) + return result, nil +} + func (t *TidalDownloader) GetTrackMetadata(resourceID string) (*TrackResponse, error) { track, err := t.getPublicTrack(resourceID) if err != nil { diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 5bdc9792..740a9fd3 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -367,6 +367,26 @@ import Gobackend // Import Go framework if let error = error { throw error } return response + case "searchTidalAll": + let args = call.arguments as! [String: Any] + let query = args["query"] as! String + let trackLimit = args["track_limit"] as? Int ?? 15 + let artistLimit = args["artist_limit"] as? Int ?? 3 + let filter = args["filter"] as? String ?? "" + let response = GobackendSearchTidalAll(query, Int(trackLimit), Int(artistLimit), filter, &error) + if let error = error { throw error } + return response + + case "searchQobuzAll": + let args = call.arguments as! [String: Any] + let query = args["query"] as! String + let trackLimit = args["track_limit"] as? Int ?? 15 + let artistLimit = args["artist_limit"] as? Int ?? 3 + let filter = args["filter"] as? String ?? "" + let response = GobackendSearchQobuzAll(query, Int(trackLimit), Int(artistLimit), filter, &error) + if let error = error { throw error } + return response + case "getDeezerRelatedArtists": let args = call.arguments as! [String: Any] let artistId = args["artist_id"] as! String diff --git a/lib/main.dart b/lib/main.dart index 5ac51db6..89d5d93c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -222,10 +222,12 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> // All checks passed -- start an incremental scan. final iosBookmark = settings.localLibraryBookmark; - ref.read(localLibraryProvider.notifier).startScan( - settings.localLibraryPath, - iosBookmark: iosBookmark.isNotEmpty ? iosBookmark : null, - ); + ref + .read(localLibraryProvider.notifier) + .startScan( + settings.localLibraryPath, + iosBookmark: iosBookmark.isNotEmpty ? iosBookmark : null, + ); } Future _initializeAppServices() async { diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index c07f50a6..22c834bd 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -3642,6 +3642,15 @@ class DownloadQueueNotifier extends Notifier { e.hasDownloadProvider && e.id.toLowerCase() == item.service.toLowerCase(), ); + final trackSource = (trackToDownload.source ?? '').trim().toLowerCase(); + final shouldSkipExtensionSongLinkPrelookup = + trackSource.isNotEmpty && + extensionState.extensions.any( + (e) => + e.enabled && + e.hasMetadataProvider && + e.id.toLowerCase() == trackSource, + ); String? deezerTrackId = trackToDownload.deezerId; if (deezerTrackId == null && trackToDownload.id.startsWith('deezer:')) { @@ -3678,6 +3687,7 @@ class DownloadQueueNotifier extends Notifier { // Fallback: Use SongLink to convert Spotify ID to Deezer ID if (!selectedExtensionDownloadProvider && deezerTrackId == null && + !shouldSkipExtensionSongLinkPrelookup && trackToDownload.id.isNotEmpty && !trackToDownload.id.startsWith('deezer:') && !trackToDownload.id.startsWith('extension:')) { @@ -3782,6 +3792,11 @@ class DownloadQueueNotifier extends Notifier { _log.d( 'Skipping Flutter SongLink Deezer prelookup for extension provider: ${item.service}', ); + } else if (shouldSkipExtensionSongLinkPrelookup && + deezerTrackId == null) { + _log.d( + 'Skipping Flutter SongLink Deezer prelookup for extension-sourced track; backend metadata enrichment will resolve identifiers first', + ); } if (deezerTrackId != null && deezerTrackId.isNotEmpty) { @@ -4179,8 +4194,9 @@ class DownloadQueueNotifier extends Notifier { progress: 0.95, ); - final format = - tidalHighFormat.startsWith('opus') ? 'opus' : 'mp3'; + final format = tidalHighFormat.startsWith('opus') + ? 'opus' + : 'mp3'; convertedPath = await FFmpegService.convertM4aToLossy( tempPath, format: format, @@ -4359,8 +4375,9 @@ class DownloadQueueNotifier extends Notifier { progress: 0.95, ); - final format = - tidalHighFormat.startsWith('opus') ? 'opus' : 'mp3'; + final format = tidalHighFormat.startsWith('opus') + ? 'opus' + : 'mp3'; final convertedPath = await FFmpegService.convertM4aToLossy( currentFilePath, format: format, diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index 19eafa75..43f25997 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -504,6 +504,11 @@ class ExtensionNotifier extends Notifier { } Future _cleanupExtensions({required String reason}) async { + if (!PlatformBridge.supportsExtensionSystem) { + _cleanupInFlight = false; + return; + } + try { await PlatformBridge.cleanupExtensions(); _log.d('Extensions cleaned up ($reason)'); @@ -519,6 +524,17 @@ class ExtensionNotifier extends Notifier { state = state.copyWith(isLoading: true, error: null); + if (!PlatformBridge.supportsExtensionSystem) { + state = state.copyWith( + isInitialized: true, + isLoading: false, + extensions: const [], + error: null, + ); + _log.i('Extension system disabled on this platform'); + return; + } + try { await PlatformBridge.initExtensionSystem(extensionsDir, dataDir); await loadExtensions(extensionsDir); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index a32277e7..7b89df1a 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -53,6 +53,8 @@ class SettingsNotifier extends Notifier { } void _syncLyricsSettingsToBackend() { + if (!PlatformBridge.supportsCoreBackend) return; + PlatformBridge.setLyricsProviders(state.lyricsProviders).catchError((e) { _log.w('Failed to sync lyrics providers to backend: $e'); }); @@ -68,6 +70,8 @@ class SettingsNotifier extends Notifier { } void _syncNetworkCompatibilitySettingsToBackend() { + if (!PlatformBridge.supportsCoreBackend) return; + final compatibilityMode = state.networkCompatibilityMode; PlatformBridge.setNetworkCompatibilityOptions( allowHttp: compatibilityMode, diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 8947dba0..eee4fc4b 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -30,6 +30,8 @@ class TrackState { searchExtensionId; // Extension ID used for current search results final String? selectedSearchFilter; // Currently selected search filter (e.g., "track", "album", "artist", "playlist") + final String? + searchSource; // Built-in search provider used for current results (e.g., "deezer", "tidal", "qobuz") const TrackState({ this.tracks = const [], @@ -52,6 +54,7 @@ class TrackState { this.isShowingRecentAccess = false, this.searchExtensionId, this.selectedSearchFilter, + this.searchSource, }); bool get hasContent => @@ -83,6 +86,8 @@ class TrackState { String? searchExtensionId, String? selectedSearchFilter, bool clearSelectedSearchFilter = false, + String? searchSource, + bool clearSearchSource = false, }) { return TrackState( tracks: tracks ?? this.tracks, @@ -108,6 +113,9 @@ class TrackState { selectedSearchFilter: clearSelectedSearchFilter ? null : (selectedSearchFilter ?? this.selectedSearchFilter), + searchSource: clearSearchSource + ? null + : (searchSource ?? this.searchSource), ); } } @@ -618,7 +626,11 @@ class TrackNotifier extends Notifier { } } - Future search(String query, {String? filterOverride}) async { + Future search( + String query, { + String? filterOverride, + String? builtInSearchProvider, + }) async { final requestId = ++_currentRequestId; // Preserve selected filter during loading @@ -640,39 +652,68 @@ class TrackNotifier extends Notifier { final includeExtensions = settings.useExtensionProviders && hasActiveMetadataExtensions; + // Determine the effective search provider + final effectiveProvider = builtInSearchProvider ?? 'deezer'; + _log.i( - 'Search started: metadataProviders, query="$query", includeExtensions=$includeExtensions, filter=$currentFilter', + 'Search started: provider=$effectiveProvider, query="$query", includeExtensions=$includeExtensions, filter=$currentFilter', ); Map results; List> metadataTrackResults = []; - try { - _log.d('Calling metadata provider search API...'); - metadataTrackResults = - await PlatformBridge.searchTracksWithMetadataProviders( - query, - limit: 20, - includeExtensions: includeExtensions, - ); - _log.i( - 'Metadata providers returned ${metadataTrackResults.length} tracks', - ); - } catch (e) { - _log.w( - 'Metadata provider search failed, falling back to Deezer tracks: $e', - ); + // Only use metadata providers for Deezer search (default behavior) + if (effectiveProvider == 'deezer') { + try { + _log.d('Calling metadata provider search API...'); + metadataTrackResults = + await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 20, + includeExtensions: includeExtensions, + ); + _log.i( + 'Metadata providers returned ${metadataTrackResults.length} tracks', + ); + } catch (e) { + _log.w( + 'Metadata provider search failed, falling back to Deezer tracks: $e', + ); + } } - _log.d('Calling Deezer search API...'); - results = await PlatformBridge.searchDeezerAll( - query, - trackLimit: 20, - artistLimit: 2, - filter: currentFilter, - ); + // Call the appropriate search API + switch (effectiveProvider) { + case 'tidal': + _log.d('Calling Tidal search API...'); + results = await PlatformBridge.searchTidalAll( + query, + trackLimit: 20, + artistLimit: 2, + filter: currentFilter, + ); + break; + case 'qobuz': + _log.d('Calling Qobuz search API...'); + results = await PlatformBridge.searchQobuzAll( + query, + trackLimit: 20, + artistLimit: 2, + filter: currentFilter, + ); + break; + default: + _log.d('Calling Deezer search API...'); + results = await PlatformBridge.searchDeezerAll( + query, + trackLimit: 20, + artistLimit: 2, + filter: currentFilter, + ); + break; + } _log.i( - 'Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums', + '$effectiveProvider returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums', ); if (!_isRequestValid(requestId)) { @@ -758,6 +799,8 @@ class TrackNotifier extends Notifier { hasSearchText: state.hasSearchText, isShowingRecentAccess: state.isShowingRecentAccess, selectedSearchFilter: currentFilter, // Preserve filter in results + searchSource: + effectiveProvider, // Track which service was used for search ); } catch (e, stackTrace) { if (!_isRequestValid(requestId)) return; diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 1cffcf2d..eeef064e 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -489,6 +489,9 @@ class _HomeTabState extends ConsumerState if (searchProvider == null || searchProvider.isEmpty) return false; + // Built-in providers (tidal, qobuz) also support live search + if (_builtInSearchProviders.contains(searchProvider)) return true; + final extension = extState.extensions .where((e) => e.id == searchProvider && e.enabled) .firstOrNull; @@ -546,6 +549,9 @@ class _HomeTabState extends ConsumerState } } + /// Built-in search providers that are not extensions + static const _builtInSearchProviders = {'tidal', 'qobuz'}; + Future _performSearch(String query, {String? filterOverride}) async { final settings = ref.read(settingsProvider); final extState = ref.read(extensionProvider); @@ -558,9 +564,14 @@ class _HomeTabState extends ConsumerState if (_lastSearchQuery == searchKey) return; _lastSearchQuery = searchKey; + final isBuiltInProvider = + searchProvider != null && + _builtInSearchProviders.contains(searchProvider); + final isExtensionEnabled = searchProvider != null && searchProvider.isNotEmpty && + !isBuiltInProvider && extState.extensions.any((e) => e.id == searchProvider && e.enabled); if (isExtensionEnabled) { @@ -571,10 +582,20 @@ class _HomeTabState extends ConsumerState await ref .read(trackProvider.notifier) .customSearch(searchProvider, query, options: options); + } else if (isBuiltInProvider) { + // Use built-in Tidal or Qobuz search + await ref + .read(trackProvider.notifier) + .search( + query, + filterOverride: selectedFilter, + builtInSearchProvider: searchProvider, + ); } else { if (searchProvider != null && searchProvider.isNotEmpty && - !isExtensionEnabled) { + !isExtensionEnabled && + !isBuiltInProvider) { ref.read(settingsProvider.notifier).setSearchProvider(null); } await ref @@ -718,6 +739,7 @@ class _HomeTabState extends ConsumerState trackName: track.name, artistName: track.artistName, coverUrl: track.coverUrl, + recommendedService: trackState.searchSource, onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) @@ -2770,6 +2792,14 @@ class _HomeTabState extends ConsumerState } if (searchProvider != null && searchProvider.isNotEmpty) { + // Check built-in providers first + if (searchProvider == 'tidal') { + return 'Search with Tidal...'; + } + if (searchProvider == 'qobuz') { + return 'Search with Qobuz...'; + } + final ext = extState.extensions .where((e) => e.id == searchProvider) .firstOrNull; @@ -3004,6 +3034,11 @@ class _SearchProviderDropdown extends ConsumerWidget { .firstOrNull; } + // Check if current provider is a built-in provider (tidal/qobuz) + const builtInProviders = {'tidal', 'qobuz'}; + final isBuiltInProvider = + currentProvider != null && builtInProviders.contains(currentProvider); + IconData displayIcon = Icons.search; String? iconPath; if (currentExt != null) { @@ -3011,10 +3046,8 @@ class _SearchProviderDropdown extends ConsumerWidget { if (currentExt.searchBehavior?.icon != null) { displayIcon = _getIconFromName(currentExt.searchBehavior!.icon!); } - } - - if (searchProviders.isEmpty) { - return const Icon(Icons.search); + } else if (isBuiltInProvider) { + displayIcon = Icons.music_note; } return Padding( @@ -3081,6 +3114,62 @@ class _SearchProviderDropdown extends ConsumerWidget { ], ), ), + // Built-in Tidal search option + PopupMenuItem( + value: 'tidal', + child: Row( + children: [ + Icon( + Icons.music_note, + size: 20, + color: currentProvider == 'tidal' + ? colorScheme.primary + : colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Tidal', + style: TextStyle( + fontWeight: currentProvider == 'tidal' + ? FontWeight.w600 + : FontWeight.normal, + ), + ), + ), + if (currentProvider == 'tidal') + Icon(Icons.check, size: 18, color: colorScheme.primary), + ], + ), + ), + // Built-in Qobuz search option + PopupMenuItem( + value: 'qobuz', + child: Row( + children: [ + Icon( + Icons.music_note, + size: 20, + color: currentProvider == 'qobuz' + ? colorScheme.primary + : colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Qobuz', + style: TextStyle( + fontWeight: currentProvider == 'qobuz' + ? FontWeight.w600 + : FontWeight.normal, + ), + ), + ), + if (currentProvider == 'qobuz') + Icon(Icons.check, size: 18, color: colorScheme.primary), + ], + ), + ), if (searchProviders.isNotEmpty) const PopupMenuDivider(), ...searchProviders.map( (ext) => PopupMenuItem( diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index d10851e2..85470e99 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -1191,7 +1191,7 @@ class _DownloadSettingsPageState extends ConsumerState { Future _pickDirectory(BuildContext context, WidgetRef ref) async { if (Platform.isIOS) { _showIOSDirectoryOptions(context, ref); - } else { + } else if (Platform.isAndroid) { _showAndroidDirectoryOptions(context, ref); } } @@ -1626,9 +1626,9 @@ class _DownloadSettingsPageState extends ConsumerState { padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( context.l10n.downloadLossy320Format, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), ), Padding( diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index 608eb4bf..86c461a3 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -73,11 +73,13 @@ class _LibrarySettingsPageState extends ConsumerState { } else if (Platform.isIOS) { // iOS doesn't need explicit storage permission for app documents setState(() => _hasStoragePermission = true); + } else { + setState(() => _hasStoragePermission = true); } } Future _requestStoragePermission() async { - if (Platform.isIOS) return true; + if (!Platform.isAndroid) return true; // SAF on Android 10+ doesn't need MANAGE_EXTERNAL_STORAGE if (_androidSdkVersion >= 29) return true; @@ -135,8 +137,9 @@ class _LibrarySettingsPageState extends ConsumerState { if (Platform.isIOS) { // On iOS, create a security-scoped bookmark so we can access // this folder across app restarts and from the Go backend. - final bookmark = - await PlatformBridge.createIosBookmarkFromPath(result); + final bookmark = await PlatformBridge.createIosBookmarkFromPath( + result, + ); if (bookmark != null && bookmark.isNotEmpty) { ref .read(settingsProvider.notifier) @@ -182,11 +185,13 @@ class _LibrarySettingsPageState extends ConsumerState { return; } - await ref.read(localLibraryProvider.notifier).startScan( - libraryPath, - forceFullScan: forceFullScan, - iosBookmark: iosBookmark.isNotEmpty ? iosBookmark : null, - ); + await ref + .read(localLibraryProvider.notifier) + .startScan( + libraryPath, + forceFullScan: forceFullScan, + iosBookmark: iosBookmark.isNotEmpty ? iosBookmark : null, + ); } Future _cancelScan() async { @@ -272,10 +277,9 @@ class _LibrarySettingsPageState extends ConsumerState { padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( context.l10n.libraryAutoScan, - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), ), Padding( @@ -293,7 +297,9 @@ class _LibrarySettingsPageState extends ConsumerState { selected: current == 'off', colorScheme: colorScheme, onTap: () { - ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('off'); + ref + .read(settingsProvider.notifier) + .setLocalLibraryAutoScan('off'); Navigator.pop(context); }, ), @@ -303,7 +309,9 @@ class _LibrarySettingsPageState extends ConsumerState { selected: current == 'on_open', colorScheme: colorScheme, onTap: () { - ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('on_open'); + ref + .read(settingsProvider.notifier) + .setLocalLibraryAutoScan('on_open'); Navigator.pop(context); }, ), @@ -313,7 +321,9 @@ class _LibrarySettingsPageState extends ConsumerState { selected: current == 'daily', colorScheme: colorScheme, onTap: () { - ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('daily'); + ref + .read(settingsProvider.notifier) + .setLocalLibraryAutoScan('daily'); Navigator.pop(context); }, ), @@ -323,7 +333,9 @@ class _LibrarySettingsPageState extends ConsumerState { selected: current == 'weekly', colorScheme: colorScheme, onTap: () { - ref.read(settingsProvider.notifier).setLocalLibraryAutoScan('weekly'); + ref + .read(settingsProvider.notifier) + .setLocalLibraryAutoScan('weekly'); Navigator.pop(context); }, ), @@ -443,9 +455,15 @@ class _LibrarySettingsPageState extends ConsumerState { child: SettingsItem( icon: Icons.autorenew_rounded, title: context.l10n.libraryAutoScan, - subtitle: _getAutoScanLabel(context, settings.localLibraryAutoScan), + subtitle: _getAutoScanLabel( + context, + settings.localLibraryAutoScan, + ), onTap: settings.localLibraryEnabled - ? () => _showAutoScanPicker(context, settings.localLibraryAutoScan) + ? () => _showAutoScanPicker( + context, + settings.localLibraryAutoScan, + ) : null, showDivider: false, ), @@ -950,9 +968,7 @@ class _AutoScanOption extends StatelessWidget { return ListTile( leading: Icon(icon), title: Text(title), - trailing: selected - ? Icon(Icons.check, color: colorScheme.primary) - : null, + trailing: selected ? Icon(Icons.check, color: colorScheme.primary) : null, onTap: onTap, ); } diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index 6d12825d..5f137482 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -91,6 +91,11 @@ class _SetupScreenState extends ConsumerState { _notificationPermissionGranted = notificationStatus.isGranted; }); } + } else { + setState(() { + _storagePermissionGranted = true; + _notificationPermissionGranted = true; + }); } } @@ -139,6 +144,8 @@ class _SetupScreenState extends ConsumerState { SnackBar(content: Text(context.l10n.setupPermissionDeniedMessage)), ); } + } else { + setState(() => _storagePermissionGranted = true); } } catch (e) { debugPrint('Permission error: $e'); @@ -225,7 +232,7 @@ class _SetupScreenState extends ConsumerState { try { if (Platform.isIOS) { await _showIOSDirectoryOptions(); - } else { + } else if (Platform.isAndroid) { final result = await PlatformBridge.pickSafTree(); if (result != null) { final treeUri = result['tree_uri'] as String? ?? ''; diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 99696825..e2c1df83 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/services.dart'; import 'package:spotiflac_android/services/download_request_payload.dart'; import 'package:spotiflac_android/utils/logger.dart'; @@ -14,6 +15,11 @@ class PlatformBridge { 'com.zarz.spotiflac/library_scan_progress_stream', ); + static bool get supportsCoreBackend => Platform.isAndroid || Platform.isIOS; + + static bool get supportsExtensionSystem => + Platform.isAndroid || Platform.isIOS; + static Future> parseSpotifyUrl(String url) async { _log.d('parseSpotifyUrl: $url'); final result = await _channel.invokeMethod('parseSpotifyUrl', {'url': url}); @@ -503,6 +509,36 @@ class PlatformBridge { return jsonDecode(result as String) as Map; } + static Future> searchTidalAll( + String query, { + int trackLimit = 15, + int artistLimit = 2, + String? filter, + }) async { + final result = await _channel.invokeMethod('searchTidalAll', { + 'query': query, + 'track_limit': trackLimit, + 'artist_limit': artistLimit, + 'filter': filter ?? '', + }); + return jsonDecode(result as String) as Map; + } + + static Future> searchQobuzAll( + String query, { + int trackLimit = 15, + int artistLimit = 2, + String? filter, + }) async { + final result = await _channel.invokeMethod('searchQobuzAll', { + 'query': query, + 'track_limit': trackLimit, + 'artist_limit': artistLimit, + 'filter': filter ?? '', + }); + return jsonDecode(result as String) as Map; + } + static Future> getDeezerRelatedArtists( String artistId, { int limit = 12, diff --git a/lib/services/share_intent_service.dart b/lib/services/share_intent_service.dart index 4f8867b1..e958394f 100644 --- a/lib/services/share_intent_service.dart +++ b/lib/services/share_intent_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:spotiflac_android/utils/logger.dart'; @@ -10,8 +11,9 @@ class ShareIntentService { ShareIntentService._internal(); // Spotify patterns - static final RegExp _spotifyUriPattern = - RegExp(r'spotify:(track|album|playlist|artist):[a-zA-Z0-9]+'); + static final RegExp _spotifyUriPattern = RegExp( + r'spotify:(track|album|playlist|artist):[a-zA-Z0-9]+', + ); static final RegExp _spotifyUrlPattern = RegExp( r'https?://open\.spotify\.com/(track|album|playlist|artist)/[a-zA-Z0-9]+(\?[^\s]*)?', ); @@ -56,6 +58,11 @@ class ShareIntentService { if (_initialized) return; _initialized = true; + if (!Platform.isAndroid && !Platform.isIOS) { + _log.i('Share intent is not supported on this platform'); + return; + } + _mediaSubscription = ReceiveSharingIntent.instance.getMediaStream().listen( _handleSharedMedia, onError: (err) => _log.e('Error: $err'), @@ -68,14 +75,14 @@ class ShareIntentService { } } - void _handleSharedMedia(List files, {bool isInitial = false}) { + void _handleSharedMedia( + List files, { + bool isInitial = false, + }) { for (final file in files) { // Check both path and message - apps may share URL in either field - final textsToCheck = [ - file.path, - if (file.message != null) file.message!, - ]; - + final textsToCheck = [file.path, if (file.message != null) file.message!]; + for (final textToCheck in textsToCheck) { final url = _extractMusicUrl(textToCheck); if (url != null) { diff --git a/lib/services/update_checker.dart b/lib/services/update_checker.dart index 3a2fb50f..c24b47f0 100644 --- a/lib/services/update_checker.dart +++ b/lib/services/update_checker.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart' as http; import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/utils/logger.dart'; @@ -24,20 +25,28 @@ class UpdateInfo { } class UpdateChecker { - static const String _latestApiUrl = 'https://api.github.com/repos/${AppInfo.githubRepo}/releases/latest'; - static const String _allReleasesApiUrl = 'https://api.github.com/repos/${AppInfo.githubRepo}/releases'; + static const String _latestApiUrl = + 'https://api.github.com/repos/${AppInfo.githubRepo}/releases/latest'; + static const String _allReleasesApiUrl = + 'https://api.github.com/repos/${AppInfo.githubRepo}/releases'; /// Check for updates based on channel preference /// [channel] can be 'stable' or 'preview' static Future checkForUpdate({String channel = 'stable'}) async { + if (!Platform.isAndroid) { + return null; + } + try { Map? releaseData; - + if (channel == 'preview') { - final response = await http.get( - Uri.parse('$_allReleasesApiUrl?per_page=10'), - headers: {'Accept': 'application/vnd.github.v3+json'}, - ).timeout(const Duration(seconds: 10)); + final response = await http + .get( + Uri.parse('$_allReleasesApiUrl?per_page=10'), + headers: {'Accept': 'application/vnd.github.v3+json'}, + ) + .timeout(const Duration(seconds: 10)); if (response.statusCode != 200) { _log.w('GitHub API returned ${response.statusCode}'); @@ -49,13 +58,15 @@ class UpdateChecker { _log.i('No releases found'); return null; } - + releaseData = releases.first as Map; } else { - final response = await http.get( - Uri.parse(_latestApiUrl), - headers: {'Accept': 'application/vnd.github.v3+json'}, - ).timeout(const Duration(seconds: 10)); + final response = await http + .get( + Uri.parse(_latestApiUrl), + headers: {'Accept': 'application/vnd.github.v3+json'}, + ) + .timeout(const Duration(seconds: 10)); if (response.statusCode != 200) { _log.w('GitHub API returned ${response.statusCode}'); @@ -68,19 +79,24 @@ class UpdateChecker { final tagName = releaseData['tag_name'] as String? ?? ''; final latestVersion = tagName.replaceFirst('v', ''); final isPrerelease = releaseData['prerelease'] as bool? ?? false; - + if (!_isNewerVersion(latestVersion, AppInfo.version)) { - _log.i('No update available (current: ${AppInfo.version}, latest: $latestVersion, channel: $channel)'); + _log.i( + 'No update available (current: ${AppInfo.version}, latest: $latestVersion, channel: $channel)', + ); return null; } final body = releaseData['body'] as String? ?? 'No changelog available'; - final htmlUrl = releaseData['html_url'] as String? ?? '${AppInfo.githubUrl}/releases'; - final publishedAt = DateTime.tryParse(releaseData['published_at'] as String? ?? '') ?? DateTime.now(); + final htmlUrl = + releaseData['html_url'] as String? ?? '${AppInfo.githubUrl}/releases'; + final publishedAt = + DateTime.tryParse(releaseData['published_at'] as String? ?? '') ?? + DateTime.now(); String? arm64Url; String? universalUrl; - + final assets = releaseData['assets'] as List? ?? []; for (final asset in assets) { final name = (asset['name'] as String? ?? '').toLowerCase(); @@ -98,12 +114,14 @@ class UpdateChecker { } } } - + // Only arm64 is supported; fall back to universal if available final apkUrl = arm64Url ?? universalUrl; - _log.i('Update available: $latestVersion (prerelease: $isPrerelease), APK URL: $apkUrl'); - + _log.i( + 'Update available: $latestVersion (prerelease: $isPrerelease), APK URL: $apkUrl', + ); + return UpdateInfo( version: latestVersion, changelog: body, @@ -122,7 +140,7 @@ class UpdateChecker { try { final latestBase = latest.split('-').first; final currentBase = current.split('-').first; - + final latestParts = latestBase.split('.').map(int.parse).toList(); final currentParts = currentBase.split('.').map(int.parse).toList(); @@ -137,12 +155,12 @@ class UpdateChecker { if (latestParts[i] > currentParts[i]) return true; if (latestParts[i] < currentParts[i]) return false; } - + final latestHasSuffix = latest.contains('-'); final currentHasSuffix = current.contains('-'); - + if (!latestHasSuffix && currentHasSuffix) return true; - + return false; } catch (e) { _log.e('Error comparing versions: $e'); diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index 3b8bac12..523970b9 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -102,6 +102,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget { final String? artistName; final String? coverUrl; final void Function(String quality, String service) onSelect; + final String? recommendedService; // Service to show as "(Recommended)" const DownloadServicePicker({ super.key, @@ -109,6 +110,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget { this.artistName, this.coverUrl, required this.onSelect, + this.recommendedService, }); @override @@ -121,6 +123,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget { String? trackName, String? artistName, String? coverUrl, + String? recommendedService, required void Function(String quality, String service) onSelect, }) { final colorScheme = Theme.of(context).colorScheme; @@ -138,6 +141,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget { artistName: artistName, coverUrl: coverUrl, onSelect: onSelect, + recommendedService: recommendedService, ), ); } @@ -152,7 +156,13 @@ class _DownloadServicePickerState extends ConsumerState { @override void initState() { super.initState(); - _selectedService = ref.read(settingsProvider).defaultService; + // Default to recommended service if available, otherwise use default + final recommended = widget.recommendedService; + if (recommended != null && recommended.isNotEmpty) { + _selectedService = recommended; + } else { + _selectedService = ref.read(settingsProvider).defaultService; + } } /// Get quality options for the selected service @@ -282,6 +292,8 @@ class _DownloadServicePickerState extends ConsumerState { _ServiceChip( label: service.isDisabled ? '${service.label} (${service.disabledReason})' + : widget.recommendedService == service.id + ? '${service.label} (Recommended)' : service.label, isSelected: _selectedService == service.id, isDisabled: service.isDisabled, diff --git a/pubspec.lock b/pubspec.lock index 0370159a..5f81e3da 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -169,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: @@ -509,6 +517,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" http: dependency: "direct main" description: @@ -661,6 +677,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.6.1" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" nm: dependency: transitive description: @@ -1082,6 +1106,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.6" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + sha256: c59fcdc143839a77581f7a7c4de018e53682408903a0a0800b95ef2dc4033eff + url: "https://pub.dev" + source: hosted + version: "2.4.0+2" sqflite_darwin: dependency: transitive description: @@ -1098,6 +1130,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: caa693ad15a587a2b4fde093b728131a1827903872171089dedb16f7665d3a91 + url: "https://pub.dev" + source: hosted + version: "3.2.0" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1bf2b50f..0d962edc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: path_provider: ^2.1.5 path: ^1.9.0 sqflite: ^2.4.1 + sqflite_common_ffi: ^2.3.6 # HTTP & Network http: ^1.6.0 From c91154ea3ead62bf373d6f72b51975f96d6da0c2 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 25 Mar 2026 15:46:12 +0700 Subject: [PATCH 143/145] feat: add built-in search provider in settings, fix bottom sheet overflow --- lib/l10n/app_localizations.dart | 2 +- lib/l10n/app_localizations_en.dart | 2 +- lib/l10n/arb/app_en.arb | 2 +- lib/screens/settings/donate_page.dart | 170 +++++++++- lib/screens/settings/extensions_page.dart | 309 ++++++++++++++---- .../settings/options_settings_page.dart | 60 +++- lib/services/ffmpeg_service.dart | 56 +++- 7 files changed, 498 insertions(+), 103 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d38202cc..2fd44b83 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2323,7 +2323,7 @@ abstract class AppLocalizations { /// Default search provider option /// /// In en, this message translates to: - /// **'Default (Deezer/Spotify)'** + /// **'Default (Deezer)'** String get extensionDefaultProvider; /// Subtitle for default provider diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index cc37fb89..594df94e 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1240,7 +1240,7 @@ class AppLocalizationsEn extends AppLocalizations { String get storeEmptyNoResults => 'No extensions found'; @override - String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + String get extensionDefaultProvider => 'Default (Deezer)'; @override String get extensionDefaultProviderSubtitle => 'Use built-in search'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7797e048..7e882ab2 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1626,7 +1626,7 @@ "@storeEmptyNoResults": { "description": "Message when search/filter returns no results" }, - "extensionDefaultProvider": "Default (Deezer/Spotify)", + "extensionDefaultProvider": "Default (Deezer)", "@extensionDefaultProvider": { "description": "Default search provider option" }, diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index 97f515ca..5ba2fcce 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -164,7 +164,13 @@ class _RecentDonorsCard extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - const donorNames = ['McNuggets Jimmy', 'zcc09', 'micahRichie', 'a fan', 'CJBGR']; + const donorNames = [ + 'McNuggets Jimmy', + 'zcc09', + 'micahRichie', + 'a fan', + 'CJBGR', + ]; // Match SettingsGroup color logic final cardColor = isDark @@ -480,31 +486,77 @@ int _cr(String v) { } // Highlighted supporters (hashes of names). -const _cv = {1211573191, 1003219236, 560908930}; +const _cv = {1211573191, 1003219236}; -class _SupporterChip extends StatelessWidget { +// Diamond tier supporters ($50+ donors). +const _dv = {560908930}; + +enum _SupporterTier { normal, gold, diamond } + +_SupporterTier _tierOf(String name) { + final h = _cr(name); + if (_dv.contains(h)) return _SupporterTier.diamond; + if (_cv.contains(h)) return _SupporterTier.gold; + return _SupporterTier.normal; +} + +class _SupporterChip extends StatefulWidget { final String name; final ColorScheme colorScheme; const _SupporterChip({required this.name, required this.colorScheme}); + @override + State<_SupporterChip> createState() => _SupporterChipState(); +} + +class _SupporterChipState extends State<_SupporterChip> + with SingleTickerProviderStateMixin { + late final _SupporterTier _tier; + AnimationController? _shimmerController; + + @override + void initState() { + super.initState(); + _tier = _tierOf(widget.name); + if (_tier == _SupporterTier.diamond) { + _shimmerController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2400), + )..repeat(); + } + } + + @override + void dispose() { + _shimmerController?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - final e = _cv.contains(_cr(name)); + final isDark = Theme.of(context).brightness == Brightness.dark; + + if (_tier == _SupporterTier.diamond) { + return _buildDiamondChip(isDark); + } + + final isGold = _tier == _SupporterTier.gold; const goldChipColor = Color(0xFFFFF8DC); const goldAccentColor = Color(0xFFB8860B); const goldDarkChipColor = Color(0xFF3A3000); - final chipColor = e ? goldChipColor : colorScheme.secondaryContainer; - final accentColor = e ? goldAccentColor : colorScheme.primary; - final isDark = Theme.of(context).brightness == Brightness.dark; - final effectiveChipColor = e && isDark ? goldDarkChipColor : chipColor; + final chipColor = isGold + ? goldChipColor + : widget.colorScheme.secondaryContainer; + final accentColor = isGold ? goldAccentColor : widget.colorScheme.primary; + final effectiveChipColor = isGold && isDark ? goldDarkChipColor : chipColor; return Material( color: effectiveChipColor, borderRadius: BorderRadius.circular(20), child: Container( - decoration: e + decoration: isGold ? BoxDecoration( borderRadius: BorderRadius.circular(20), border: Border.all( @@ -520,10 +572,12 @@ class _SupporterChip extends StatelessWidget { CircleAvatar( radius: 10, backgroundColor: accentColor.withValues(alpha: 0.2), - child: e + child: isGold ? Icon(Icons.star_rounded, size: 12, color: accentColor) : Text( - name.isNotEmpty ? name[0].toUpperCase() : '?', + widget.name.isNotEmpty + ? widget.name[0].toUpperCase() + : '?', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, @@ -533,10 +587,12 @@ class _SupporterChip extends StatelessWidget { ), const SizedBox(width: 8), Text( - name, + widget.name, style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: e ? accentColor : colorScheme.onSecondaryContainer, - fontWeight: e ? FontWeight.w600 : FontWeight.w500, + color: isGold + ? accentColor + : widget.colorScheme.onSecondaryContainer, + fontWeight: isGold ? FontWeight.w600 : FontWeight.w500, ), ), ], @@ -544,6 +600,92 @@ class _SupporterChip extends StatelessWidget { ), ); } + + Widget _buildDiamondChip(bool isDark) { + const diamondLight = Color(0xFFE8F4FD); + const diamondDark = Color(0xFF0D2B3E); + const diamondAccent = Color(0xFF4FC3F7); + const diamondHighlight = Color(0xFFB3E5FC); + + final chipBg = isDark ? diamondDark : diamondLight; + + return AnimatedBuilder( + animation: _shimmerController!, + builder: (context, child) { + final t = _shimmerController!.value; + return Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(20), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + begin: Alignment(-2.0 + 4.0 * t, 0.0), + end: Alignment(-1.0 + 4.0 * t, 0.0), + colors: [ + chipBg, + isDark + ? diamondAccent.withValues(alpha: 0.18) + : diamondHighlight.withValues(alpha: 0.7), + chipBg, + ], + stops: const [0.0, 0.5, 1.0], + ), + border: Border.all( + color: diamondAccent.withValues( + alpha: 0.5 + 0.3 * (0.5 - (t - 0.5).abs()), + ), + width: 1.2, + ), + boxShadow: [ + BoxShadow( + color: diamondAccent.withValues( + alpha: 0.15 + 0.1 * (0.5 - (t - 0.5).abs()), + ), + blurRadius: 8, + spreadRadius: 0, + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + diamondAccent.withValues(alpha: 0.3), + diamondAccent.withValues(alpha: 0.15), + ], + ), + ), + child: const Icon( + Icons.diamond_rounded, + size: 12, + color: diamondAccent, + ), + ), + const SizedBox(width: 8), + Text( + widget.name, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: isDark ? diamondHighlight : diamondAccent, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ); + }, + ); + } } class _NoticeLine extends StatelessWidget { diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index 196dd0aa..57979689 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; +import 'package:spotiflac_android/providers/explore_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/screens/settings/extension_detail_page.dart'; import 'package:spotiflac_android/screens/settings/provider_priority_page.dart'; @@ -151,6 +152,7 @@ class _ExtensionsPageState extends ConsumerState { _DownloadPriorityItem(), _MetadataPriorityItem(), _SearchProviderSelector(), + _HomeFeedProviderSelector(), ], ), ), @@ -586,6 +588,8 @@ class _MetadataPriorityItem extends ConsumerWidget { class _SearchProviderSelector extends ConsumerWidget { const _SearchProviderSelector(); + static const _builtInProviders = {'tidal': 'Tidal', 'qobuz': 'Qobuz'}; + @override Widget build(BuildContext context, WidgetRef ref) { final settings = ref.watch(settingsProvider); @@ -596,20 +600,29 @@ class _SearchProviderSelector extends ConsumerWidget { .where((e) => e.enabled && e.hasCustomSearch) .toList(); + // Always allow tapping: built-in providers are always available + final hasAnyProvider = + searchProviders.isNotEmpty || _builtInProviders.isNotEmpty; + String currentProviderName = context.l10n.extensionDefaultProvider; if (settings.searchProvider != null && settings.searchProvider!.isNotEmpty) { - final ext = searchProviders - .where((e) => e.id == settings.searchProvider) - .firstOrNull; - currentProviderName = ext?.displayName ?? settings.searchProvider!; + // Check built-in first + if (_builtInProviders.containsKey(settings.searchProvider)) { + currentProviderName = _builtInProviders[settings.searchProvider]!; + } else { + final ext = searchProviders + .where((e) => e.id == settings.searchProvider) + .firstOrNull; + currentProviderName = ext?.displayName ?? settings.searchProvider!; + } } return Column( mainAxisSize: MainAxisSize.min, children: [ InkWell( - onTap: searchProviders.isEmpty + onTap: !hasAnyProvider ? null : () => _showSearchProviderPicker( context, @@ -623,7 +636,7 @@ class _SearchProviderSelector extends ConsumerWidget { children: [ Icon( Icons.manage_search, - color: searchProviders.isEmpty + color: !hasAnyProvider ? colorScheme.outline : colorScheme.onSurfaceVariant, ), @@ -635,14 +648,12 @@ class _SearchProviderSelector extends ConsumerWidget { Text( context.l10n.extensionsSearchProvider, style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: searchProviders.isEmpty - ? colorScheme.outline - : null, + color: !hasAnyProvider ? colorScheme.outline : null, ), ), const SizedBox(height: 2), Text( - searchProviders.isEmpty + !hasAnyProvider ? context.l10n.extensionsNoCustomSearch : currentProviderName, style: Theme.of(context).textTheme.bodySmall?.copyWith( @@ -654,7 +665,7 @@ class _SearchProviderSelector extends ConsumerWidget { ), Icon( Icons.chevron_right, - color: searchProviders.isEmpty + color: !hasAnyProvider ? colorScheme.outline : colorScheme.onSurfaceVariant, ), @@ -682,61 +693,245 @@ class _SearchProviderSelector extends ConsumerWidget { borderRadius: BorderRadius.vertical(top: Radius.circular(28)), ), builder: (ctx) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - ctx.l10n.extensionsSearchProvider, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - ctx.l10n.extensionsSearchProviderDescription, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + ctx.l10n.extensionsSearchProvider, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), ), - ), - ListTile( - leading: Icon(Icons.music_note, color: colorScheme.primary), - title: Text(ctx.l10n.extensionDefaultProvider), - subtitle: Text(ctx.l10n.extensionDefaultProviderSubtitle), - trailing: - (settings.searchProvider == null || - settings.searchProvider!.isEmpty) - ? Icon(Icons.check_circle, color: colorScheme.primary) - : Icon(Icons.circle_outlined, color: colorScheme.outline), - onTap: () { - ref.read(settingsProvider.notifier).setSearchProvider(null); - Navigator.pop(ctx); - }, - ), - ...searchProviders.map( - (ext) => ListTile( - leading: Icon(Icons.extension, color: colorScheme.secondary), - title: Text(ext.displayName), - subtitle: Text( - ext.searchBehavior?.placeholder ?? - ctx.l10n.extensionsCustomSearch, + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + ctx.l10n.extensionsSearchProviderDescription, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), ), - trailing: settings.searchProvider == ext.id + ), + ListTile( + leading: Icon(Icons.music_note, color: colorScheme.primary), + title: Text(ctx.l10n.extensionDefaultProvider), + subtitle: Text(ctx.l10n.extensionDefaultProviderSubtitle), + trailing: + (settings.searchProvider == null || + settings.searchProvider!.isEmpty) ? Icon(Icons.check_circle, color: colorScheme.primary) : Icon(Icons.circle_outlined, color: colorScheme.outline), onTap: () { - ref.read(settingsProvider.notifier).setSearchProvider(ext.id); + ref.read(settingsProvider.notifier).setSearchProvider(null); Navigator.pop(ctx); }, ), - ), - const SizedBox(height: 16), - ], + ..._builtInProviders.entries.map( + (entry) => ListTile( + leading: Icon(Icons.search, color: colorScheme.tertiary), + title: Text(entry.value), + subtitle: Text('Search with ${entry.value}'), + trailing: settings.searchProvider == entry.key + ? Icon(Icons.check_circle, color: colorScheme.primary) + : Icon(Icons.circle_outlined, color: colorScheme.outline), + onTap: () { + ref + .read(settingsProvider.notifier) + .setSearchProvider(entry.key); + Navigator.pop(ctx); + }, + ), + ), + if (searchProviders.isNotEmpty) const Divider(height: 1), + ...searchProviders.map( + (ext) => ListTile( + leading: Icon(Icons.extension, color: colorScheme.secondary), + title: Text(ext.displayName), + subtitle: Text( + ext.searchBehavior?.placeholder ?? + ctx.l10n.extensionsCustomSearch, + ), + trailing: settings.searchProvider == ext.id + ? Icon(Icons.check_circle, color: colorScheme.primary) + : Icon(Icons.circle_outlined, color: colorScheme.outline), + onTap: () { + ref + .read(settingsProvider.notifier) + .setSearchProvider(ext.id); + Navigator.pop(ctx); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ); + } +} + +class _HomeFeedProviderSelector extends ConsumerWidget { + const _HomeFeedProviderSelector(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final extState = ref.watch(extensionProvider); + final colorScheme = Theme.of(context).colorScheme; + + final homeFeedProviders = extState.extensions + .where((e) => e.enabled && e.hasHomeFeed) + .toList(); + + final hasAnyProvider = homeFeedProviders.isNotEmpty; + + String currentProviderName = 'Auto'; + if (settings.homeFeedProvider != null && + settings.homeFeedProvider!.isNotEmpty) { + final ext = homeFeedProviders + .where((e) => e.id == settings.homeFeedProvider) + .firstOrNull; + currentProviderName = ext?.displayName ?? settings.homeFeedProvider!; + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: !hasAnyProvider + ? null + : () => _showHomeFeedProviderPicker( + context, + ref, + settings, + homeFeedProviders, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.explore_outlined, + color: !hasAnyProvider + ? colorScheme.outline + : colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Home Feed Provider', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: !hasAnyProvider ? colorScheme.outline : null, + ), + ), + const SizedBox(height: 2), + Text( + !hasAnyProvider + ? 'No extensions with home feed' + : currentProviderName, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: !hasAnyProvider + ? colorScheme.outline + : colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ], + ); + } + + void _showHomeFeedProviderPicker( + BuildContext context, + WidgetRef ref, + dynamic settings, + List homeFeedProviders, + ) { + final colorScheme = Theme.of(context).colorScheme; + + showModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (ctx) => SafeArea( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + 'Home Feed Provider', + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + 'Choose which extension provides the home feed on the main screen', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ListTile( + leading: Icon(Icons.auto_awesome, color: colorScheme.primary), + title: const Text('Auto'), + subtitle: const Text('Automatically select the best available'), + trailing: + (settings.homeFeedProvider == null || + settings.homeFeedProvider!.isEmpty) + ? Icon(Icons.check_circle, color: colorScheme.primary) + : Icon(Icons.circle_outlined, color: colorScheme.outline), + onTap: () { + ref.read(settingsProvider.notifier).setHomeFeedProvider(null); + ref.read(exploreProvider.notifier).refresh(); + Navigator.pop(ctx); + }, + ), + ...homeFeedProviders.map( + (ext) => ListTile( + leading: Icon(Icons.extension, color: colorScheme.secondary), + title: Text(ext.displayName), + subtitle: Text('Use ${ext.displayName} home feed'), + trailing: settings.homeFeedProvider == ext.id + ? Icon(Icons.check_circle, color: colorScheme.primary) + : Icon(Icons.circle_outlined, color: colorScheme.outline), + onTap: () { + ref + .read(settingsProvider.notifier) + .setHomeFeedProvider(ext.id); + ref.read(exploreProvider.notifier).refresh(); + Navigator.pop(ctx); + }, + ), + ), + const SizedBox(height: 16), + ], + ), ), ), ); diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index 4a7618d0..3f5a2e7c 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -611,24 +611,34 @@ class _MetadataSourceSelector extends ConsumerWidget { final ValueChanged onChanged; const _MetadataSourceSelector({required this.onChanged}); + static const _builtInProviders = {'tidal': 'Tidal', 'qobuz': 'Qobuz'}; + @override Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; final settings = ref.watch(settingsProvider); final extState = ref.watch(extensionProvider); + final searchProvider = settings.searchProvider ?? ''; + final isBuiltIn = _builtInProviders.containsKey(searchProvider); + Extension? activeExtension; - if (settings.searchProvider != null && - settings.searchProvider!.isNotEmpty) { + if (searchProvider.isNotEmpty && !isBuiltIn) { activeExtension = extState.extensions - .where((e) => e.id == settings.searchProvider && e.enabled) + .where((e) => e.id == searchProvider && e.enabled) .firstOrNull; } - final hasExtensionSearch = activeExtension != null; + final hasNonDefaultProvider = isBuiltIn || activeExtension != null; - String? extensionName; - if (hasExtensionSearch) { - extensionName = activeExtension.displayName; + String subtitle; + if (isBuiltIn) { + subtitle = 'Using ${_builtInProviders[searchProvider]}'; + } else if (activeExtension != null) { + subtitle = context.l10n.optionsUsingExtension( + activeExtension.displayName, + ); + } else { + subtitle = context.l10n.optionsPrimaryProviderSubtitle; } return Padding( @@ -644,11 +654,9 @@ class _MetadataSourceSelector extends ConsumerWidget { ), const SizedBox(height: 4), Text( - hasExtensionSearch - ? context.l10n.optionsUsingExtension(extensionName!) - : context.l10n.optionsPrimaryProviderSubtitle, + subtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: hasExtensionSearch + color: hasNonDefaultProvider ? colorScheme.primary : colorScheme.onSurfaceVariant, ), @@ -659,17 +667,41 @@ class _MetadataSourceSelector extends ConsumerWidget { _SourceChip( icon: Icons.graphic_eq, label: 'Deezer', - isSelected: !hasExtensionSearch, + isSelected: searchProvider.isEmpty, onTap: () { - if (hasExtensionSearch) { + if (hasNonDefaultProvider) { ref.read(settingsProvider.notifier).setSearchProvider(null); } onChanged('deezer'); }, ), + const SizedBox(width: 8), + _SourceChip( + icon: Icons.waves, + label: 'Tidal', + isSelected: searchProvider == 'tidal', + onTap: () { + ref + .read(settingsProvider.notifier) + .setSearchProvider('tidal'); + onChanged('tidal'); + }, + ), + const SizedBox(width: 8), + _SourceChip( + icon: Icons.album, + label: 'Qobuz', + isSelected: searchProvider == 'qobuz', + onTap: () { + ref + .read(settingsProvider.notifier) + .setSearchProvider('qobuz'); + onChanged('qobuz'); + }, + ), ], ), - if (hasExtensionSearch) ...[ + if (activeExtension != null) ...[ const SizedBox(height: 12), Row( children: [ diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index bf43717c..53efe84d 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -130,6 +130,25 @@ class FFmpegService { } } + static Future _executeWithArguments( + List arguments, + ) async { + try { + final session = await FFmpegKit.executeWithArguments(arguments); + final returnCode = await session.getReturnCode(); + final output = await session.getOutput() ?? ''; + + return FFmpegResult( + success: ReturnCode.isSuccess(returnCode), + returnCode: returnCode?.getValue() ?? -1, + output: output, + ); + } catch (e) { + _log.e('FFmpeg executeWithArguments error: $e'); + return FFmpegResult(success: false, returnCode: -1, output: e.toString()); + } + } + static Future convertM4aToFlac(String inputPath) async { final outputPath = _buildOutputPath(inputPath, '.flac'); @@ -1030,18 +1049,24 @@ class FFmpegService { }) async { final tempDir = await getTemporaryDirectory(); final tempOutput = _nextTempEmbedPath(tempDir.path, '.opus'); - - final StringBuffer cmdBuffer = StringBuffer(); - cmdBuffer.write('-i "$opusPath" '); - cmdBuffer.write('-map 0:a '); - cmdBuffer.write('-map_metadata -1 '); - cmdBuffer.write('-map_metadata:s:a -1 '); - cmdBuffer.write('-c:a copy '); + final arguments = [ + '-i', + opusPath, + '-map', + '0:a', + '-map_metadata', + '-1', + '-map_metadata:s:a', + '-1', + '-c:a', + 'copy', + ]; if (metadata != null) { metadata.forEach((key, value) { - final sanitizedValue = value.replaceAll('"', '\\"'); - cmdBuffer.write('-metadata $key="$sanitizedValue" '); + arguments + ..add('-metadata') + ..add('$key=$value'); }); } @@ -1049,8 +1074,9 @@ class FFmpegService { try { final pictureBlock = await _createMetadataBlockPicture(coverPath); if (pictureBlock != null) { - final escapedBlock = pictureBlock.replaceAll('"', '\\"'); - cmdBuffer.write('-metadata METADATA_BLOCK_PICTURE="$escapedBlock" '); + arguments + ..add('-metadata') + ..add('METADATA_BLOCK_PICTURE=$pictureBlock'); _log.d( 'Created METADATA_BLOCK_PICTURE for Opus (${pictureBlock.length} chars)', ); @@ -1062,12 +1088,12 @@ class FFmpegService { } } - cmdBuffer.write('"$tempOutput" -y'); - - final command = cmdBuffer.toString(); + arguments + ..add(tempOutput) + ..add('-y'); _log.d('Executing FFmpeg Opus embed command'); - final result = await _execute(command); + final result = await _executeWithArguments(arguments); if (result.success) { try { From 3a73aee1b7fc2c68374d7a2ed2048aef31afc9d1 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 25 Mar 2026 15:46:22 +0700 Subject: [PATCH 144/145] feat: add home feed provider setting, fix Qobuz cover URL propagation - Add homeFeedProvider field to AppSettings with picker UI in extensions page - Update explore_provider to respect user's home feed provider preference - Add normalizeCoverReference() and normalizeRemoteHttpUrl() to filter invalid cover URLs (no scheme, no host, protocol-relative) - Apply cover URL normalization across all screens and providers to prevent 'no host specified in URI' errors from Qobuz - Propagate CoverURL from QobuzDownloadResult through Go backend so cover art is available even when request metadata is incomplete --- go_backend/exports.go | 10 +++- go_backend/exports_test.go | 29 ++++++++++ go_backend/extension_providers.go | 2 + go_backend/qobuz.go | 7 ++- lib/models/settings.dart | 7 +++ lib/models/settings.g.dart | 2 + lib/providers/download_queue_provider.dart | 26 ++++++--- lib/providers/explore_provider.dart | 52 +++++++++-------- lib/providers/settings_provider.dart | 9 +++ lib/providers/track_provider.dart | 66 +++++++++++++--------- lib/screens/album_screen.dart | 35 ++++++++---- lib/screens/artist_screen.dart | 27 +++++---- lib/screens/home_tab.dart | 7 ++- lib/screens/playlist_screen.dart | 30 +++++++--- lib/screens/track_metadata_screen.dart | 3 +- lib/utils/string_utils.dart | 35 ++++++++++++ 16 files changed, 252 insertions(+), 95 deletions(-) diff --git a/go_backend/exports.go b/go_backend/exports.go index 457a3e6e..39184350 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -128,6 +128,7 @@ type DownloadResult struct { TrackNumber int DiscNumber int ISRC string + CoverURL string Genre string Label string Copyright string @@ -214,6 +215,11 @@ func buildDownloadSuccessResponse( copyright = req.Copyright } + coverURL := strings.TrimSpace(result.CoverURL) + if coverURL == "" { + coverURL = strings.TrimSpace(req.CoverURL) + } + return DownloadResponse{ Success: true, Message: message, @@ -230,7 +236,7 @@ func buildDownloadSuccessResponse( TrackNumber: trackNumber, DiscNumber: discNumber, ISRC: isrc, - CoverURL: req.CoverURL, + CoverURL: coverURL, Genre: genre, Label: label, Copyright: copyright, @@ -378,6 +384,7 @@ func DownloadTrack(requestJSON string) (string, error) { TrackNumber: qobuzResult.TrackNumber, DiscNumber: qobuzResult.DiscNumber, ISRC: qobuzResult.ISRC, + CoverURL: qobuzResult.CoverURL, LyricsLRC: qobuzResult.LyricsLRC, } } @@ -586,6 +593,7 @@ func DownloadWithFallback(requestJSON string) (string, error) { TrackNumber: qobuzResult.TrackNumber, DiscNumber: qobuzResult.DiscNumber, ISRC: qobuzResult.ISRC, + CoverURL: qobuzResult.CoverURL, LyricsLRC: qobuzResult.LyricsLRC, } } else if !errors.Is(qobuzErr, ErrDownloadCancelled) { diff --git a/go_backend/exports_test.go b/go_backend/exports_test.go index dbce4278..44faf294 100644 --- a/go_backend/exports_test.go +++ b/go_backend/exports_test.go @@ -84,3 +84,32 @@ func TestPreferredReleaseMetadataPrefersRequestValues(t *testing.T) { t.Fatalf("disc number = %d", discNumber) } } + +func TestBuildDownloadSuccessResponsePrefersProviderCoverURL(t *testing.T) { + req := DownloadRequest{ + TrackName: "Track", + ArtistName: "Artist", + AlbumName: "Album", + AlbumArtist: "Artist", + } + + result := DownloadResult{ + Title: "Track", + Artist: "Artist", + Album: "Album", + CoverURL: "https://cdn.qobuz.test/cover.jpg", + } + + resp := buildDownloadSuccessResponse( + req, + result, + "qobuz", + "ok", + "/tmp/test.flac", + false, + ) + + if resp.CoverURL != result.CoverURL { + t.Fatalf("cover url = %q, want %q", resp.CoverURL, result.CoverURL) + } +} diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index b9f79bc4..a3e46450 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -1480,6 +1480,7 @@ func tryBuiltInProvider(providerID string, req DownloadRequest) (*DownloadRespon TrackNumber: qobuzResult.TrackNumber, DiscNumber: qobuzResult.DiscNumber, ISRC: qobuzResult.ISRC, + CoverURL: qobuzResult.CoverURL, } } err = qobuzErr @@ -1522,6 +1523,7 @@ func tryBuiltInProvider(providerID string, req DownloadRequest) (*DownloadRespon TrackNumber: result.TrackNumber, DiscNumber: result.DiscNumber, ISRC: result.ISRC, + CoverURL: result.CoverURL, Genre: req.Genre, Label: req.Label, Copyright: req.Copyright, diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index dc7d160a..cba711b0 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -2067,6 +2067,7 @@ type QobuzDownloadResult struct { TrackNumber int DiscNumber int ISRC string + CoverURL string LyricsLRC string } @@ -2260,7 +2261,10 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { parallelDone := make(chan struct{}) go func() { defer close(parallelDone) - coverURL := req.CoverURL + coverURL := strings.TrimSpace(req.CoverURL) + if coverURL == "" { + coverURL = strings.TrimSpace(qobuzTrackAlbumImage(track)) + } embedLyrics := req.EmbedLyrics if !req.EmbedMetadata { coverURL = "" @@ -2393,6 +2397,7 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { TrackNumber: resultTrackNumber, DiscNumber: resultDiscNumber, ISRC: track.ISRC, + CoverURL: strings.TrimSpace(qobuzTrackAlbumImage(track)), LyricsLRC: lyricsLRC, }, nil } diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 32b3b76c..c2f2eed6 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -34,6 +34,7 @@ class AppSettings { final bool enableLogging; final bool useExtensionProviders; final String? searchProvider; + final String? homeFeedProvider; final bool separateSingles; final String albumFolderStructure; final bool showExtensionStore; @@ -113,6 +114,7 @@ class AppSettings { this.enableLogging = false, this.useExtensionProviders = true, this.searchProvider, + this.homeFeedProvider, this.separateSingles = false, this.albumFolderStructure = 'artist_album', this.showExtensionStore = true, @@ -179,6 +181,8 @@ class AppSettings { bool? useExtensionProviders, String? searchProvider, bool clearSearchProvider = false, + String? homeFeedProvider, + bool clearHomeFeedProvider = false, bool? separateSingles, String? albumFolderStructure, bool? showExtensionStore, @@ -244,6 +248,9 @@ class AppSettings { searchProvider: clearSearchProvider ? null : (searchProvider ?? this.searchProvider), + homeFeedProvider: clearHomeFeedProvider + ? null + : (homeFeedProvider ?? this.homeFeedProvider), separateSingles: separateSingles ?? this.separateSingles, albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure, showExtensionStore: showExtensionStore ?? this.showExtensionStore, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 85244329..914e224f 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -39,6 +39,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( enableLogging: json['enableLogging'] as bool? ?? false, useExtensionProviders: json['useExtensionProviders'] as bool? ?? true, searchProvider: json['searchProvider'] as String?, + homeFeedProvider: json['homeFeedProvider'] as String?, separateSingles: json['separateSingles'] as bool? ?? false, albumFolderStructure: json['albumFolderStructure'] as String? ?? 'artist_album', @@ -117,6 +118,7 @@ Map _$AppSettingsToJson( 'enableLogging': instance.enableLogging, 'useExtensionProviders': instance.useExtensionProviders, 'searchProvider': instance.searchProvider, + 'homeFeedProvider': instance.homeFeedProvider, 'separateSingles': instance.separateSingles, 'albumFolderStructure': instance.albumFolderStructure, 'showExtensionStore': instance.showExtensionStore, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 22c834bd..a457b3d2 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -122,7 +122,7 @@ class DownloadHistoryItem { artistName: json['artistName'] as String, albumName: json['albumName'] as String, albumArtist: normalizeOptionalString(json['albumArtist'] as String?), - coverUrl: json['coverUrl'] as String?, + coverUrl: normalizeCoverReference(json['coverUrl']?.toString()), filePath: json['filePath'] as String, storageMode: json['storageMode'] as String?, downloadTreeUri: json['downloadTreeUri'] as String?, @@ -176,7 +176,7 @@ class DownloadHistoryItem { artistName: artistName ?? this.artistName, albumName: albumName ?? this.albumName, albumArtist: albumArtist ?? this.albumArtist, - coverUrl: coverUrl ?? this.coverUrl, + coverUrl: normalizeCoverReference(coverUrl ?? this.coverUrl), filePath: filePath ?? this.filePath, storageMode: storageMode ?? this.storageMode, downloadTreeUri: downloadTreeUri ?? this.downloadTreeUri, @@ -2534,8 +2534,8 @@ class DownloadQueueNotifier extends Notifier { final backendIsrc = normalizeOptionalString( backendResult['isrc'] as String?, ); - final backendCoverUrl = normalizeOptionalString( - backendResult['cover_url'] as String?, + final backendCoverUrl = normalizeCoverReference( + backendResult['cover_url']?.toString(), ); final backendAlbumArtist = normalizeOptionalString( backendResult['album_artist'] as String?, @@ -2591,7 +2591,7 @@ class DownloadQueueNotifier extends Notifier { } String? coverPath; - var coverUrl = track.coverUrl; + var coverUrl = normalizeRemoteHttpUrl(track.coverUrl); if (coverUrl != null && coverUrl.isNotEmpty) { try { if (settings.maxQualityCover) { @@ -2777,7 +2777,7 @@ class DownloadQueueNotifier extends Notifier { } String? coverPath; - var coverUrl = track.coverUrl; + var coverUrl = normalizeRemoteHttpUrl(track.coverUrl); if (coverUrl != null && coverUrl.isNotEmpty) { try { if (settings.maxQualityCover) { @@ -2945,7 +2945,7 @@ class DownloadQueueNotifier extends Notifier { } String? coverPath; - var coverUrl = track.coverUrl; + var coverUrl = normalizeRemoteHttpUrl(track.coverUrl); if (coverUrl != null && coverUrl.isNotEmpty) { try { if (settings.maxQualityCover) { @@ -3751,7 +3751,7 @@ class DownloadQueueNotifier extends Notifier { albumArtist: trackToDownload.albumArtist, artistId: trackToDownload.artistId, albumId: trackToDownload.albumId, - coverUrl: trackToDownload.coverUrl, + coverUrl: normalizeCoverReference(trackToDownload.coverUrl), duration: trackToDownload.duration, isrc: (deezerIsrc != null && _isValidISRC(deezerIsrc)) ? deezerIsrc @@ -4041,6 +4041,14 @@ class DownloadQueueNotifier extends Notifier { item.service.toLowerCase(); final decryptionKey = (result['decryption_key'] as String?)?.trim() ?? ''; + trackToDownload = _buildTrackForMetadataEmbedding( + trackToDownload, + result, + resolvedAlbumArtist, + ); + _log.d( + 'Track coverUrl after download result: ${trackToDownload.coverUrl}', + ); if (!wasExisting && decryptionKey.isNotEmpty && filePath != null) { _log.i('Encrypted stream detected, decrypting via FFmpeg...'); @@ -4959,7 +4967,7 @@ class DownloadQueueNotifier extends Notifier { ? backendAlbum : trackToDownload.albumName, albumArtist: historyAlbumArtist, - coverUrl: trackToDownload.coverUrl, + coverUrl: normalizeCoverReference(trackToDownload.coverUrl), filePath: filePath, storageMode: effectiveSafMode ? 'saf' : 'app', downloadTreeUri: effectiveSafMode diff --git a/lib/providers/explore_provider.dart b/lib/providers/explore_provider.dart index cd2b01a7..e4ffa5f7 100644 --- a/lib/providers/explore_provider.dart +++ b/lib/providers/explore_provider.dart @@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/logger.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; +import 'package:spotiflac_android/providers/settings_provider.dart'; final _log = AppLogger('ExploreProvider'); @@ -202,9 +203,7 @@ class ExploreNotifier extends Notifier { Future _saveToCache(List sections) async { try { final prefs = await SharedPreferences.getInstance(); - final data = { - 'sections': sections.map((s) => s.toJson()).toList(), - }; + final data = {'sections': sections.map((s) => s.toJson()).toList()}; await prefs.setString(_cacheKey, jsonEncode(data)); await prefs.setInt(_cacheTsKey, DateTime.now().millisecondsSinceEpoch); _log.d('Saved ${sections.length} explore sections to cache'); @@ -216,16 +215,16 @@ class ExploreNotifier extends Notifier { /// Fetch home feed from spotify-web extension Future fetchHomeFeed({bool forceRefresh = false}) async { _log.i('fetchHomeFeed called, forceRefresh=$forceRefresh'); - + // If we have cached content and it's fresh enough, skip network fetch - if (!forceRefresh && - state.hasContent && + if (!forceRefresh && + state.hasContent && state.lastFetched != null && DateTime.now().difference(state.lastFetched!).inMinutes < 5) { _log.d('Using cached home feed (fresh enough)'); return; } - + if (state.isLoading) { _log.d('Home feed fetch already in progress'); return; @@ -237,21 +236,33 @@ class ExploreNotifier extends Notifier { try { final extState = ref.read(extensionProvider); - _log.d('Extensions count: ${extState.extensions.length}'); - + final settings = ref.read(settingsProvider); + final preferredId = settings.homeFeedProvider; + _log.d( + 'Extensions count: ${extState.extensions.length}, preferred home feed: $preferredId', + ); + Extension? targetExt; for (final extension in extState.extensions) { if (!extension.enabled || !extension.hasHomeFeed) { continue; } + // If user has a preference, use that + if (preferredId != null && + preferredId.isNotEmpty && + extension.id == preferredId) { + targetExt = extension; + break; + } + // Otherwise take the first available (fallback to spotify-web if found) if (targetExt == null || extension.id == 'spotify-web') { targetExt = extension; - if (extension.id == 'spotify-web') { + if (preferredId == null && extension.id == 'spotify-web') { break; } } } - + if (targetExt == null) { _log.w('No extension with homeFeed capability found'); state = state.copyWith( @@ -260,7 +271,7 @@ class ExploreNotifier extends Notifier { ); return; } - + _log.i('Fetching home feed from ${targetExt.id}...'); final result = await PlatformBridge.getExtensionHomeFeed(targetExt.id); @@ -276,10 +287,7 @@ class ExploreNotifier extends Notifier { _log.d('getExtensionHomeFeed success=$success'); if (!success) { final error = result['error'] as String? ?? 'Unknown error'; - state = state.copyWith( - isLoading: false, - error: error, - ); + state = state.copyWith(isLoading: false, error: error); return; } @@ -291,10 +299,12 @@ class ExploreNotifier extends Notifier { .toList(); _log.i('Fetched ${sections.length} sections'); - + if (sections.isNotEmpty && sections.first.items.isNotEmpty) { final firstItem = sections.first.items.first; - _log.d('First item: name=${firstItem.name}, artists=${firstItem.artists}, type=${firstItem.type}'); + _log.d( + 'First item: name=${firstItem.name}, artists=${firstItem.artists}, type=${firstItem.type}', + ); } final localGreeting = _getLocalGreeting(); @@ -311,10 +321,7 @@ class ExploreNotifier extends Notifier { _saveToCache(sections); } catch (e, stack) { _log.e('Error fetching home feed: $e', e, stack); - state = state.copyWith( - isLoading: false, - error: e.toString(), - ); + state = state.copyWith(isLoading: false, error: e.toString()); } } @@ -325,7 +332,6 @@ class ExploreNotifier extends Notifier { Future refresh() => fetchHomeFeed(forceRefresh: true); } - final exploreProvider = NotifierProvider(() { return ExploreNotifier(); }); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 7b89df1a..f2309d5d 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -424,6 +424,15 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setHomeFeedProvider(String? provider) { + if (provider == null || provider.isEmpty) { + state = state.copyWith(clearHomeFeedProvider: true); + } else { + state = state.copyWith(homeFeedProvider: provider); + } + _saveSettings(); + } + void setEnableLogging(bool enabled) { state = state.copyWith(enableLogging: enabled); _saveSettings(); diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index eee4fc4b..5de7d16b 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/logger.dart'; +import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; @@ -286,7 +287,9 @@ class TrackNotifier extends Notifier { playlistName: type == 'playlist' ? result['name'] as String? : null, - coverUrl: result['cover_url'] as String?, + coverUrl: normalizeCoverReference( + result['cover_url']?.toString(), + ), searchExtensionId: extensionId, ); return; @@ -313,10 +316,12 @@ class TrackNotifier extends Notifier { isLoading: false, artistId: artistData['id'] as String?, artistName: artistData['name'] as String?, - coverUrl: - artistData['image_url'] as String? ?? - artistData['images'] as String?, - headerImageUrl: artistData['header_image'] as String?, + coverUrl: normalizeRemoteHttpUrl( + (artistData['image_url'] ?? artistData['images'])?.toString(), + ), + headerImageUrl: normalizeRemoteHttpUrl( + artistData['header_image']?.toString(), + ), monthlyListeners: artistData['listeners'] as int?, artistAlbums: albums, artistTopTracks: topTracks.isNotEmpty ? topTracks : null, @@ -357,7 +362,7 @@ class TrackNotifier extends Notifier { isLoading: false, albumId: id, albumName: albumInfo['name'] as String?, - coverUrl: albumInfo['images'] as String?, + coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()), ); _preWarmCacheForTracks(tracks); } else if (type == 'playlist') { @@ -371,7 +376,9 @@ class TrackNotifier extends Notifier { tracks: tracks, isLoading: false, playlistName: playlistInfo['name'] as String?, - coverUrl: playlistInfo['images'] as String?, + coverUrl: normalizeRemoteHttpUrl( + playlistInfo['images']?.toString(), + ), ); _preWarmCacheForTracks(tracks); } else if (type == 'artist') { @@ -385,7 +392,7 @@ class TrackNotifier extends Notifier { isLoading: false, artistId: artistInfo['id'] as String?, artistName: artistInfo['name'] as String?, - coverUrl: artistInfo['images'] as String?, + coverUrl: normalizeRemoteHttpUrl(artistInfo['images']?.toString()), artistAlbums: albums, ); } @@ -422,7 +429,7 @@ class TrackNotifier extends Notifier { isLoading: false, albumId: 'qobuz:$id', albumName: albumInfo['name'] as String?, - coverUrl: albumInfo['images'] as String?, + coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()), ); _preWarmCacheForTracks(tracks); } else if (type == 'playlist') { @@ -435,8 +442,9 @@ class TrackNotifier extends Notifier { final owner = playlistInfo['owner'] as Map?; final playlistName = (playlistInfo['name'] ?? owner?['name']) as String?; - final coverUrl = - (playlistInfo['images'] ?? owner?['images']) as String?; + final coverUrl = normalizeRemoteHttpUrl( + (playlistInfo['images'] ?? owner?['images'])?.toString(), + ); state = TrackState( tracks: tracks, isLoading: false, @@ -455,7 +463,7 @@ class TrackNotifier extends Notifier { isLoading: false, artistId: artistInfo['id'] as String?, artistName: artistInfo['name'] as String?, - coverUrl: artistInfo['images'] as String?, + coverUrl: normalizeRemoteHttpUrl(artistInfo['images']?.toString()), artistAlbums: albums, ); } @@ -492,7 +500,7 @@ class TrackNotifier extends Notifier { isLoading: false, albumId: 'tidal:$id', albumName: albumInfo['name'] as String?, - coverUrl: albumInfo['images'] as String?, + coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()), ); _preWarmCacheForTracks(tracks); } else if (type == 'playlist') { @@ -505,8 +513,9 @@ class TrackNotifier extends Notifier { final owner = playlistInfo['owner'] as Map?; final playlistName = (playlistInfo['name'] ?? owner?['name']) as String?; - final coverUrl = - (playlistInfo['images'] ?? owner?['images']) as String?; + final coverUrl = normalizeRemoteHttpUrl( + (playlistInfo['images'] ?? owner?['images'])?.toString(), + ); state = TrackState( tracks: tracks, isLoading: false, @@ -525,7 +534,7 @@ class TrackNotifier extends Notifier { isLoading: false, artistId: artistInfo['id'] as String?, artistName: artistInfo['name'] as String?, - coverUrl: artistInfo['images'] as String?, + coverUrl: normalizeRemoteHttpUrl(artistInfo['images']?.toString()), artistAlbums: albums, ); } @@ -580,7 +589,7 @@ class TrackNotifier extends Notifier { isLoading: false, albumId: parsed['id'] as String?, albumName: albumInfo['name'] as String?, - coverUrl: albumInfo['images'] as String?, + coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()), ); _preWarmCacheForTracks(tracks); } else if (type == 'playlist') { @@ -592,8 +601,9 @@ class TrackNotifier extends Notifier { final owner = playlistInfo['owner'] as Map?; final playlistName = (playlistInfo['name'] ?? owner?['name']) as String?; - final coverUrl = - (playlistInfo['images'] ?? owner?['images']) as String?; + final coverUrl = normalizeRemoteHttpUrl( + (playlistInfo['images'] ?? owner?['images'])?.toString(), + ); state = TrackState( tracks: tracks, isLoading: false, @@ -612,7 +622,7 @@ class TrackNotifier extends Notifier { isLoading: false, artistId: artistInfo['id'] as String?, artistName: artistInfo['name'] as String?, - coverUrl: artistInfo['images'] as String?, + coverUrl: normalizeRemoteHttpUrl(artistInfo['images']?.toString()), artistAlbums: albums, ); } @@ -986,7 +996,7 @@ class TrackNotifier extends Notifier { albumArtist: data['album_artist'] as String?, artistId: (data['artist_id'] ?? data['artistId'])?.toString(), albumId: data['album_id']?.toString(), - coverUrl: data['images'] as String?, + coverUrl: normalizeCoverReference(data['images']?.toString()), isrc: data['isrc'] as String?, duration: (durationMs / 1000).round(), trackNumber: data['track_number'] as int?, @@ -1017,7 +1027,9 @@ class TrackNotifier extends Notifier { albumArtist: data['album_artist']?.toString(), artistId: (data['artist_id'] ?? data['artistId'])?.toString(), albumId: data['album_id']?.toString(), - coverUrl: (data['cover_url'] ?? data['images'])?.toString(), + coverUrl: normalizeCoverReference( + (data['cover_url'] ?? data['images'])?.toString(), + ), isrc: data['isrc']?.toString(), duration: (durationMs / 1000).round(), trackNumber: data['track_number'] as int?, @@ -1062,7 +1074,9 @@ class TrackNotifier extends Notifier { name: data['name'] as String? ?? '', releaseDate: data['release_date'] as String? ?? '', totalTracks: data['total_tracks'] as int? ?? 0, - coverUrl: (data['cover_url'] ?? data['images'])?.toString(), + coverUrl: normalizeCoverReference( + (data['cover_url'] ?? data['images'])?.toString(), + ), albumType: data['album_type'] as String? ?? 'album', artists: data['artists'] as String? ?? '', providerId: data['provider_id']?.toString(), @@ -1073,7 +1087,7 @@ class TrackNotifier extends Notifier { return SearchArtist( id: data['id'] as String? ?? '', name: data['name'] as String? ?? '', - imageUrl: data['images'] as String?, + imageUrl: normalizeRemoteHttpUrl(data['images']?.toString()), followers: data['followers'] as int? ?? 0, popularity: data['popularity'] as int? ?? 0, ); @@ -1084,7 +1098,7 @@ class TrackNotifier extends Notifier { id: data['id'] as String? ?? '', name: data['name'] as String? ?? '', artists: data['artists'] as String? ?? '', - imageUrl: data['images'] as String?, + imageUrl: normalizeRemoteHttpUrl(data['images']?.toString()), releaseDate: data['release_date'] as String?, totalTracks: data['total_tracks'] as int? ?? 0, albumType: data['album_type'] as String? ?? 'album', @@ -1096,7 +1110,7 @@ class TrackNotifier extends Notifier { id: data['id'] as String? ?? '', name: data['name'] as String? ?? '', owner: data['owner'] as String? ?? '', - imageUrl: data['images'] as String?, + imageUrl: normalizeRemoteHttpUrl(data['images']?.toString()), totalTracks: data['total_tracks'] as int? ?? 0, ); } diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index b54fb92a..3f020953 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -11,6 +11,7 @@ import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/playback_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/file_access.dart'; +import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart'; import 'package:spotiflac_android/widgets/download_service_picker.dart'; import 'package:spotiflac_android/providers/library_collections_provider.dart'; @@ -94,7 +95,10 @@ class _AlbumScreenState extends ConsumerState { .recordAlbumAccess( id: widget.albumId, name: widget.albumName, - artistName: widget.artistName ?? widget.tracks?.firstOrNull?.albumArtist ?? widget.tracks?.firstOrNull?.artistName, + artistName: + widget.artistName ?? + widget.tracks?.firstOrNull?.albumArtist ?? + widget.tracks?.firstOrNull?.artistName, imageUrl: widget.coverUrl, providerId: providerId, ); @@ -226,7 +230,7 @@ class _AlbumScreenState extends ConsumerState { artistId: (data['artist_id'] ?? data['artistId'])?.toString() ?? _artistId, albumId: data['album_id']?.toString() ?? widget.albumId, - coverUrl: data['images'] as String?, + coverUrl: normalizeCoverReference(data['images']?.toString()), isrc: data['isrc'] as String?, duration: ((data['duration_ms'] as int? ?? 0) / 1000).round(), trackNumber: data['track_number'] as int?, @@ -280,7 +284,8 @@ class _AlbumScreenState extends ConsumerState { ) { final expandedHeight = _calculateExpandedHeight(context); final tracks = _tracks ?? []; - final artistName = widget.artistName ?? + final artistName = + widget.artistName ?? (tracks.isNotEmpty ? (tracks.first.albumArtist ?? tracks.first.artistName) : null); @@ -574,17 +579,21 @@ class _AlbumScreenState extends ConsumerState { // Skip already-downloaded tracks final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); - final localLibState = (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) + final localLibState = + (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) ? ref.read(localLibraryProvider) : null; final tracksToQueue = []; int skippedCount = 0; for (final track in tracks) { - final isInHistory = historyState.isDownloaded(track.id) || + final isInHistory = + historyState.isDownloaded(track.id) || (track.isrc != null && historyState.getByIsrc(track.isrc!) != null) || - historyState.findByTrackAndArtist(track.name, track.artistName) != null; - final isInLocal = localLibState?.existsInLibrary( + historyState.findByTrackAndArtist(track.name, track.artistName) != + null; + final isInLocal = + localLibState?.existsInLibrary( isrc: track.isrc, trackName: track.name, artistName: track.artistName, @@ -617,7 +626,11 @@ class _AlbumScreenState extends ConsumerState { onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracksToQueue, service, qualityOverride: quality); + .addMultipleToQueue( + tracksToQueue, + service, + qualityOverride: quality, + ); _showQueuedSnackbar(context, tracksToQueue.length, skippedCount); }, ); @@ -633,9 +646,9 @@ class _AlbumScreenState extends ConsumerState { final message = skipped > 0 ? context.l10n.discographySkippedDownloaded(added, skipped) : context.l10n.snackbarAddedTracksToQueue(added); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); } Widget _buildLoveAllButton() { diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 3efc2d88..7aefcd64 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -14,6 +14,7 @@ import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/playback_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/file_access.dart'; +import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/screens/album_screen.dart'; import 'package:spotiflac_android/screens/home_tab.dart' show ExtensionAlbumScreen; @@ -297,8 +298,7 @@ class _ArtistScreenState extends ConsumerState { .toList(); } - final topTracksList = - artistData['top_tracks'] as List? ?? []; + final topTracksList = artistData['top_tracks'] as List? ?? []; if (topTracksList.isNotEmpty) { topTracks = topTracksList .map((t) => _parseTrack(t as Map)) @@ -399,8 +399,9 @@ class _ArtistScreenState extends ConsumerState { (data['artist_id'] ?? data['artistId'])?.toString() ?? widget.artistId, albumId: data['album_id']?.toString() ?? album?.id, - coverUrl: (data['cover_url'] ?? data['images'] ?? album?.coverUrl) - ?.toString(), + coverUrl: normalizeCoverReference( + (data['cover_url'] ?? data['images'] ?? album?.coverUrl)?.toString(), + ), isrc: data['isrc']?.toString(), duration: (durationMs / 1000).round(), trackNumber: data['track_number'] as int?, @@ -414,18 +415,18 @@ class _ArtistScreenState extends ConsumerState { ArtistAlbum _parseArtistAlbum(Map data) { final totalTracksValue = data['total_tracks']; - final totalTracks = - totalTracksValue is int - ? totalTracksValue - : int.tryParse(totalTracksValue?.toString() ?? '') ?? 0; + final totalTracks = totalTracksValue is int + ? totalTracksValue + : int.tryParse(totalTracksValue?.toString() ?? '') ?? 0; return ArtistAlbum( id: data['id'] as String? ?? '', name: (data['name'] ?? data['title'] ?? '').toString(), releaseDate: (data['release_date'] ?? '').toString(), totalTracks: totalTracks, - coverUrl: (data['cover_url'] ?? data['images'] ?? data['cover_art']) - ?.toString(), + coverUrl: normalizeCoverReference( + (data['cover_url'] ?? data['images'] ?? data['cover_art'])?.toString(), + ), albumType: (data['album_type'] ?? data['type'] ?? 'album').toString(), artists: (data['artists'] ?? data['artist'] ?? widget.artistName) .toString(), @@ -1359,8 +1360,10 @@ class _ArtistScreenState extends ConsumerState { }, itemBuilder: (context, pageIndex) { final startIndex = pageIndex * tracksPerPage; - final endIndex = - (startIndex + tracksPerPage).clamp(0, tracks.length); + final endIndex = (startIndex + tracksPerPage).clamp( + 0, + tracks.length, + ); final pageTracks = tracks.sublist(startIndex, endIndex); return Column( diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index eeef064e..ca022604 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -23,6 +23,7 @@ import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.da import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/app_bar_layout.dart'; import 'package:spotiflac_android/utils/file_access.dart'; +import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/screens/playlist_screen.dart'; import 'package:spotiflac_android/screens/downloaded_album_screen.dart'; import 'package:spotiflac_android/widgets/download_service_picker.dart'; @@ -4306,7 +4307,7 @@ class _ExtensionArtistScreenState extends ConsumerState { artists: (data['artists'] ?? '').toString(), releaseDate: (data['release_date'] ?? '').toString(), totalTracks: data['total_tracks'] as int? ?? 0, - coverUrl: data['cover_url']?.toString(), + coverUrl: normalizeCoverReference(data['cover_url']?.toString()), albumType: (data['album_type'] ?? 'album').toString(), providerId: (data['provider_id'] ?? widget.extensionId).toString(), ); @@ -4331,7 +4332,9 @@ class _ExtensionArtistScreenState extends ConsumerState { (data['artist_id'] ?? data['artistId'])?.toString() ?? widget.artistId, albumId: data['album_id']?.toString(), - coverUrl: (data['cover_url'] ?? data['images'])?.toString(), + coverUrl: normalizeCoverReference( + (data['cover_url'] ?? data['images'])?.toString(), + ), isrc: data['isrc']?.toString(), duration: (durationMs / 1000).round(), trackNumber: data['track_number'] as int?, diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index c2d9bff3..fb4818ea 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -8,6 +8,7 @@ import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/utils/file_access.dart'; +import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/playback_provider.dart'; @@ -128,7 +129,9 @@ class _PlaylistScreenState extends ConsumerState { albumArtist: data['album_artist']?.toString(), artistId: (data['artist_id'] ?? data['artistId'])?.toString(), albumId: data['album_id']?.toString(), - coverUrl: (data['cover_url'] ?? data['images'])?.toString(), + coverUrl: normalizeCoverReference( + (data['cover_url'] ?? data['images'])?.toString(), + ), isrc: data['isrc']?.toString(), duration: (durationMs / 1000).round(), trackNumber: data['track_number'] as int?, @@ -532,7 +535,12 @@ class _PlaylistScreenState extends ConsumerState { tooltip: context.l10n.tooltipAddToPlaylist, onPressed: _tracks.isEmpty ? null - : () => showAddTracksToPlaylistSheet(context, ref, _tracks, playlistNamePrefill: widget.playlistName), + : () => showAddTracksToPlaylistSheet( + context, + ref, + _tracks, + playlistNamePrefill: widget.playlistName, + ), ); } @@ -611,17 +619,21 @@ class _PlaylistScreenState extends ConsumerState { // Skip already-downloaded tracks final historyState = ref.read(downloadHistoryProvider); final settings = ref.read(settingsProvider); - final localLibState = (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) + final localLibState = + (settings.localLibraryEnabled && settings.localLibraryShowDuplicates) ? ref.read(localLibraryProvider) : null; final tracksToQueue = []; int skippedCount = 0; for (final track in tracks) { - final isInHistory = historyState.isDownloaded(track.id) || + final isInHistory = + historyState.isDownloaded(track.id) || (track.isrc != null && historyState.getByIsrc(track.isrc!) != null) || - historyState.findByTrackAndArtist(track.name, track.artistName) != null; - final isInLocal = localLibState?.existsInLibrary( + historyState.findByTrackAndArtist(track.name, track.artistName) != + null; + final isInLocal = + localLibState?.existsInLibrary( isrc: track.isrc, trackName: track.name, artistName: track.artistName, @@ -679,9 +691,9 @@ class _PlaylistScreenState extends ConsumerState { final message = skipped > 0 ? context.l10n.discographySkippedDownloaded(added, skipped) : context.l10n.snackbarAddedTracksToQueue(added); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); } } diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 81102a3e..09f0175c 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -519,7 +519,8 @@ class _TrackMetadataScreenState extends ConsumerState { String get _filePath => _isLocalItem ? _localLibraryItem!.filePath : _downloadItem!.filePath; - String? get _coverUrl => _isLocalItem ? null : _downloadItem!.coverUrl; + String? get _coverUrl => + _isLocalItem ? null : normalizeRemoteHttpUrl(_downloadItem!.coverUrl); String? get _localCoverPath => _isLocalItem ? _localLibraryItem!.coverPath : null; String? get _spotifyId => _isLocalItem ? null : _downloadItem!.spotifyId; diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart index 9f4cc164..2e2b704c 100644 --- a/lib/utils/string_utils.dart +++ b/lib/utils/string_utils.dart @@ -6,6 +6,41 @@ String? normalizeOptionalString(String? value) { return trimmed; } +final RegExp _windowsAbsolutePathPattern = RegExp(r'^[A-Za-z]:[\\/]'); + +bool _looksLikeLocalReference(String value) { + return value.startsWith('/') || + value.startsWith('content://') || + value.startsWith('file://') || + _windowsAbsolutePathPattern.hasMatch(value); +} + +String? normalizeCoverReference(String? value) { + final normalized = normalizeOptionalString(value); + if (normalized == null) return null; + + if (normalized.startsWith('//')) { + return 'https:$normalized'; + } + + if (normalized.startsWith('http://') || + normalized.startsWith('https://') || + _looksLikeLocalReference(normalized)) { + return normalized; + } + + return null; +} + +String? normalizeRemoteHttpUrl(String? value) { + final normalized = normalizeCoverReference(value); + if (normalized == null) return null; + if (normalized.startsWith('http://') || normalized.startsWith('https://')) { + return normalized; + } + return null; +} + String formatSampleRateKHz(int sampleRate) { final khz = sampleRate / 1000; final precision = sampleRate % 1000 == 0 ? 0 : 1; From be94a594411566e561e8ffdb708d0b49075da507 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 25 Mar 2026 15:47:08 +0700 Subject: [PATCH 145/145] chore: bump version to 3.9.0+115, add new translators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump app version from 3.8.8 to 3.9.0 (build 115) - Add 4 new Crowdin translators: unkn0wn (Indonesian), lunching1272 (Chinese Simplified), Сергей Ильченко (Russian), Girl-lass (Chinese Simplified) --- lib/constants/app_info.dart | 16 ++++++++-------- lib/screens/settings/about_page.dart | 24 ++++++++++++++++++++++++ pubspec.yaml | 2 +- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index b57c894b..94e71f92 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -3,24 +3,24 @@ import 'package:flutter/foundation.dart'; /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.8.8'; - static const String buildNumber = '114'; + static const String version = '3.9.0'; + static const String buildNumber = '115'; static const String fullVersion = '$version+$buildNumber'; /// Shows "Internal" in debug builds, actual version in release. static String get displayVersion => kDebugMode ? 'Internal' : version; - - + static const String appName = 'SpotiFLAC'; static const String copyright = '© 2026 SpotiFLAC'; - + static const String mobileAuthor = 'zarzet'; static const String originalAuthor = 'afkarxyz'; - + static const String githubRepo = 'zarzet/SpotiFLAC-Mobile'; static const String githubUrl = 'https://github.com/$githubRepo'; - static const String originalGithubUrl = 'https://github.com/afkarxyz/SpotiFLAC'; - + static const String originalGithubUrl = + 'https://github.com/afkarxyz/SpotiFLAC'; + static const String kofiUrl = 'https://ko-fi.com/zarzet'; static const String githubSponsorsUrl = 'https://github.com/sponsors/zarzet/'; } diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 1ed5ce43..14cdef2b 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -511,6 +511,30 @@ class _TranslatorsSection extends StatelessWidget { language: 'Japanese', flag: '🇯🇵', ), + _Translator( + name: 'unkn0wn', + crowdinUsername: 'rdclvi', + language: 'Indonesian', + flag: '🇮🇩', + ), + _Translator( + name: 'lunching1272', + crowdinUsername: 'lunching1272', + language: 'Chinese Simplified', + flag: '🇨🇳', + ), + _Translator( + name: 'Сергей Ильченко', + crowdinUsername: 'Sega_Mostky', + language: 'Russian', + flag: '🇷🇺', + ), + _Translator( + name: 'Girl-lass', + crowdinUsername: 'Girl-lass', + language: 'Chinese Simplified', + flag: '🇨🇳', + ), _Translator( name: 'Kaan', crowdinUsername: 'glai', diff --git a/pubspec.yaml b/pubspec.yaml index 0d962edc..2bbf902a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Deezer publish_to: "none" -version: 3.8.8+114 +version: 3.9.0+115 environment: sdk: ^3.10.0

    QW2K4mg3V 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 ZAAp53f(-qe!m0nK=JlUH9a$Ys{1@Nl0q+0+ literal 0 HcmV?d00001 diff --git a/assets/images/3.jpg b/assets/images/3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f4cc9de5853e61c8e0dec08e9b26823ad8bf14ff GIT binary patch literal 106344 zcmeFZcT|&EyEh!iHloNNL=*%aia=;15ReiXmC##ALgE*zH?^IS?9dpI?wyA_m6MQ&dR>8z4x`t{oC!jcDeV5 z_9p?h(A(ZO0RVGz00;m8{0KO70020|qrN{bA2{%D^s(O_d;c2^xN_jsztcR{@7&3} znBSlOaPh4D4}hOc42%r`yd-$OJZpdIz>x9v>n>K-mIlUV(0>NJ{my^r7XZM|KNw?e zcpa?>N6UHsg?9Z5?G}jn&i{nRqwD7z z@;$Hb<~vR&Jp7Sx-t$M^{~RC)U=1(?T>Jk1JU7n-kN|+o8vww;-~aS;%>)3dqX2+2 zBY*lyKL-E=9svMVU4Q!hiAmrM%#D8}_q}$0xXG*OH8KFeZwCMfbprr=4*v+_S^rJh zF7SfR@yZpz`*{L<0Um%00AqkZzzv|xqf`OE04@PE_D2EN0Y?rUK63cbkt2tX96fsE zN4}r<_>LXp6Fw<$;wKT|GiOAEPoEYQmpLygCUx%g=?jV%q%O+JgXGViSNcUs?iU$3 zdAaY295{OPDBq8KLVSEea-ye2<^IQJ{||uRkH>yHbot-`Nx%<+2M!7z*na~M=fVBM z4+p=u#D6n~j~qJs!@(a99ODIh{scI1@Zf<%hYueS;6HNk$PdQ>2Yxts=&<0Cqo;np zBqV$lmLzF?iyR5U&JRVTXlPZ%7`oz6HE?KR2m#a(@JH;vkP-@Sq?-5AZn<(0<|1B>$%V zionqjpb|915m6Cn3^D68FcdaNX>py6VxIM%{M}!{rFPI61uF}`Q4P~-ON}cew_DnS z&&5tzgD1KwFGdqJ&|a65g0*Z4M~Dp6!8mVHcdf{21cIy+G~GOP>MXz+%lb5fz}dw zlkdaKL7VgI`u1yVT|Ysu2kQG9HcM_vr6g~Q9AEf>ay&Y2m_Z+?2tqE7TKD(9+Qau< zvC$u2mgJIZ;R;a}(2Om>h|0RO%cmE2i}(@zv5`ZK4W;(;HG@Or>{6pl0x;u|;g}yOsVf>2<1G|A;cF&EMn=)lc6KVJvvIuDI)v zvrp<<;4c6`P_(>kX3|x$nY5+4flEMS`&i7gj)H>F0D!}vJ;Z)#SP`984CWg# z4bdhcP$q1fOQ;n49Cz;6ZJLMLg0>50FU6PNq@KBHm!wNJlvYHSaNx-^)^_HnqCiP;xQLmK}@hG>JQ!Jj0isWQ>xB z^JTDZ%N7%-Q(Hpr*})DBbmaK%s#hl2iMP-)Xz;$42JgkKFyzwsYq@M%15TLM?K}?w z9w{F=;e9Jsrph62DX28^L|j}G@qRVaPd+t^g3)pRql0!E)=Qj5Nkn#5hml7?7 z;J9=ZQJZF}@jI zd7B%>#aUN|x4nHN%XIKa6$F`#zuniz<cS!lEuCx9m|1^L)YtTVkX4kY_p#F%O$mA>1P#ApDa1&3pFs+8GWP= zMLLbm-WrRGjL0e0hnkIA`_qWb{P58FNCb1A2c{;MwYFZk&r z9VI;w0KTV=Dry50^Gbxpc;dUo0Ff*w$g8qR>H5y>`cb@Zf-9K${^*r%-DI3Nao+D@ zosv8<1-D)?yfm)Rm{tFbJ1m+wc8`k5dgZ_d1@-|d8+dTl{cT2dq)^kqA0BW5P>w} zU|WJ;^!{2;D?)x?gpqSjuF)Bj+`I;D&*H|H0SSs<<#i^uB(X@~$tBCWgsG9}ofYz; z^uy)4iUhYNEyQ@gj%;`6&5EH+X$@$j8wu~jNaP5U4hhlWEx1mJ zg$cb)gcyi+s#!)7P7SYLbuCUd^||SD{sjG-v%I7DWo2gsPk7E`NR z@k8!O$ObKJb9xEj$HZ>_$99WF_40jIy`^FUa??dgILEK{iAWcGqV>;M1u>=VVej)> zknb7yUIfGvC{rqf$+O zB-pu;02x`?K zo9V74{Ufn-JKI48@9Ly8i}oc3mz*B0vgQdS&o~Ck#X%vow0JUNst;++GM_aXJ@Jl= zM{4E4keV;3PZ$xNdFkw5mrRu~u)a2bcfTn4eAg?Gmg$gsdMd(9_PzkE@^H>o??+UM z+s_42RDQ6cTue!*ZaP>;3-=Shd|_xxXuZ9EohM?>eh#cH&(12DaK}&J7hH6*PCVpHRt|lzqTC%=^@66JAL`O0S)O?)m zx}_D%+~Xtm8MYVFr~7_!Ns^2>!LVU0WrRP)R4bcdSfQQ-vClTUCKGohv(|+N_W}C= zSG|qR9zoi#|7FiTKJd2c)~ylc=bThl#U|&Dc3Cl8UJR8ii_19vXme8-eskH!2Z>YP zdUbkKsupIW;JlFT&#N6wrlEg*>Fq+Dp9J7&JL%!VM-SNLyI@ox-GDAXMZVvADPbku zQx?3^3U;2EA{zGFD|m&OaN3(!EnUzc4`h1yOaYemMfQ;Xvc-=v{wnWL`+(Y}Z+-g! z=}FBx8Z>tCWTKA@!fEizY(or_lYPpwnUB^~D`oxKbFea$K(4yX{U{}cVF=!IrBy;G zC7aU?hP1oYevXVJ}WPZiswjFG5vt^Mc zNKZAEZV_!v^$FT7b3pBox9yMp+#7258x;$ZF%Kdz&lj8-Bb5~2T7Y+C@Qq~QgE!Lp z+F(eX0b`FO%zP~hM9X#Vxp{xMmHTVT#&J*P-qw11@K}I3vo?D#V=}tI@5APA^HOM?<>L>Ugi)UIx+tpGCo7036%q>ePjuNm!ry#^*YKvS_ z2~?%E>#kWdk#~e1@aFzQ`kErE^nJkC`#tY{KyqN#I8yS>`eVb&WSa6)4rW5%dBY-B zZ7K?tqmF-2j^H?iq>qjD511s8rsdwu&8y@J0#W@qxK~DE03eBhAI?jRv_*t6dTH0mLZC_`NZnGF%}K521dI>$yIyA4Hi zwp4dL)b{}l@;+chTHUg)#Lh0n;(0mT)<=@z(I~HNHyOH)o~PeQ=Lr~s4SkP=+;*O6 z;0soERP!sGR)s@3qqw>E9*$Ir9VaPbf`6pQs}`ZeA9hOW4RLvmOzn)1le_w!b$NS| z@0-c*BJcVo>;nvG8W|DwBV#2bN*Yy&eW`KaQqjb#O=WHbnUOcD5oazuS~o$}Mvx~s z5Xlhy?)&>a-xTMgZzYU7Za@p8>Ue#4NS5QEYBcY>io|2V%5i^6KO=n0-A@2diqzwIXP>YZ&={|mD z0{O0>>wT?_3oz7W^n3;oT=T<5K=I({)p{eAXniG%N^M+^GnBH;UV0+=9Zc-DjMl#mpIfkS;MH`UlX|W5-6e& z^*NQXLk)?FN22R})W3bQkvVZP0bNs5%9-~N9gIkpm5_hUvI6N`9cd&F2LEI=L5`}p zrFFi$ueD~7DfBSAIjFn#iwqOlJs_uAHO;u5Olh~c;mFFpHu|R z1N+ zgdcw_Dfwycx1H?uheZUB0Hj~DM-OEajEQ?2G&7$df_qqtL8xF$42CKlsNxYMlML%x zr&Fu7ahWZ!Kx(R^>-z{P z<1$j*Q;-ECIiecyNL^x_7|px()IxdGPfPQ6O6(P6yskv(gKUkoq>;xA$|?;=!sdT18Mr&@vV?dwr(z3Ar!|l?;`Tt5;AzidDbx0RFxV@TenEZX#Lg8R2- z_5QjtfiVuhj_|4CYqeNA@kg$*CMD(*TFBY)5r?W`IyL#dO!~Nj2N9{Cz3iAL8lOG3 zRq{4ESMq<})BjJ#IJA4QlC2)7$_FvUk)vXxTY-0qeBj+N%Rd2*tMu%Z?p2`$zNWtC zoHyKV-5#ds&B5pDZ~t~qU>V^w74O?Ee#`ICuUq+srUX*zRKTe{f)ri2>gw}-w6DTz~2b`jlll~ z0@aK7EDjUqV~USs4^Y@k=Zyy>JyW0coy4dF+)~p5%l$>BhC^h;l5?RYh|k^ld|4jDs>&e_mB740G5n> zEdZ^grk!$BiTRrAo?y^%S`bO0Cw}M%t5(V!t^hW&A;iK-HO?$fBF*~J+n9c+nSUvB zrj+vQ;r|SY_~y2DJU#>>E^I+4BpEad`_0X31;$PVEV0-Mc^7c%HSG?K*5ZRPftaQ&0o3U*_V_$qIZ{hhG))vmLy37X9-Qh%;gu zq21*IKdgA{r8`024L;KFBMV|j_Cbps2JZF1bTW>eLnbjJz( z_rLxjC_5(Ea0Q2iK?)pqPvLlb^=+aFZYd{yQsk&(@*5f%WFyx^R&gHw zmx1c9EwZk%XxS6D%x;*vb^Rt5e`DvDvZZz?i?)iq6>Z`@Ds(E{01O1yPB*mPlIwfE zzWaG>A5h?>Sk3p=U!>a9(o(wE(EJpMZcoeH6UaFD?$Ce2@~^o5kLjZf=D+m-nI$hh zEBlPyVfZ##*d<70;AKv_`98onV(^ynmB;;;uC*E~rgNf8pP1FMO0@(9g$S9b2L}|^ zweIXi*WP{A^zCu??B{LK*?j=tK448fChys$qH){U3OwS2Cj0Qr+cr=JNc~}7eF|S4D37_Z9toymezf`YH@{cJEft)@3`63BK5Qg zV#GkBO~6&jeZcapg;H4J;dM}Q>%;iS)ZrE?JG9kNF|@PPC|S`-sxU#f_R_NeEw!#p zpm!fiA-(9aMeD&#OvJT5NP=ZSt;k8_`b8kB`*CeWM|z6Iker{!Cb!R8KdGA8J~F#b zn??1K+`~_j-9+aIL_3a>py3HK6q8$gU$9u8Mf-Z^#kCO)9Box&!Dl7ZghtRxveB}u ze=G?iO($aInan=I=)|Zl*7boSa!@m$W>a$dI+1^`m;68_99-&TM^+mBrPO*%Wi!s6 zGAw!J?2?0>awf*2GF*eVK*GGW$>P$^1D#0XHJAb47 z;7i?jDK|tr%>Jn(Xj^y;#!NOCmI`uimDFOOXMrNgq^FuVHsjsf_qS_dR;o~h8;$I4 zG;86N#mbayxI|S?+c9fBxH{0bkflvCD05>WT5d(H=N`fy9KU|Ie1f9BVi<@yNQgAZg2Ok(hQzty8bopkr= zew(fi2O(9w#g>Y+rAe0L83CuM7ng=Vc-q8k|Dxk{pd3^q0m2P zSU<#moiP+$O?hDN{hr^<`ntML-Ip13Wry=DbSCoNaLMV>aY>}T0+sIl8UIDvkfef{ zPTuB|7_Ra$E>`SA_Plz#y1tCMss=xw zI72N+yWo?TS+d3k4!y#Pk|9#%{3sfA6z*H4mFhn=68BizEUch*YG$1zzZ_v+xKc;p zyR7m{NF3Haovk7BMFnR+qmKzCr#9%lz8@7hsBO^)@29H_u);HEikx&X??OScFE`{$ z=SG@>_R1fKf64ZtgsSy4TT@n1sr{Cz10&HdL;5lflbGEdhSssEzOgUcCfF7yEluQC z38F0Ywhf}Y|2o`U(j)oRI!>H}_)y@2uDg-4+q~ zNG6g09)&`Sdy4o#A?NOP>|D;Llgv>#vhP=HRrFaQC3E&G7ImZ+s?U8@5-K;K?dGBV z8ukY(PQi4t0OL`P)cV;cg(*0OgvdqZPRtj(4psM(I_JY8^1SOPaJHQBUZ>HYM`?-(c@~zY09Co zHfhuhOAD%kXf~&~lY{*Tx9r~bo}?EhV8hRh9V%IJ*kZ$CLkSPOY7CAD0gEeWazN6)uxh!iP7F!X)^to0T5DUB%nOSQWiHpuY7s=>y&=e^vWhohan0QEH`-VQ(o_LAo77 zeO8IoBK>Z#x@Eg-KcY9jxZO&?bokO5#;HnGU()UCsHfzHvT4lSwu$E8_o8@C)=1M z1c`3t3Bo9}22^*Psh^qw(klW6E(=qv;98^;?zk1;!&Qp4R0<%_hhwuFBbAZLciETo zs<(~}>Us6<1De&ov82{#+2C`Va$W_Ly6bAsgMX4Ws_$@AzBIfzgZom{H?lsLki2x8 zV1kxc7OKM%>ZqdZptPX61jj&bKxcmGT$cYI8wKw2(mkUKX>gr1;MuS_shDi@(iIQ# zqC1T^GE(Q&r;?nYpAn+NZV*qj5U?GtEF>`5AwI~p_i*b5l6za@s&$adjT@)+v|rXR zd+W4wEIC-8;;PfH4TBA}K9&A5@NO*NhK?W%77?z#_#ALx;XKB?QC?XzZfXWfIFoxn zKEY@MQ@<{XHCww|8*ly6a{#GPS9|I*qo*>7#tAPiLcD##hKkIlF!uUbYZ1L9v~EXcAgtdsz`)p2 zF+L;Mf~A1Og&~C`8bW`rLbfYvId5LCc9Q|RA z19itWAc6E~M?GYs2{B$&Wpb8|mb;ji=zg08P8{nxZy{iW%UbW4Huv6XK#d&p$|q16 z_%e{k+b8!|{!2atv~0WD=s7cVZ5L)zG`1OuD&?~%86en4$l{{;P2JPI zN_heYpd_kO3^^9opOIjiL1xEI0UMa*L*)MzEr$z^VPj}$j2u6^K}^1 z>RH%D*J}BR?8U`vx?2N9eHz6m@8ofwdhk(8BxEc{tYJ>4^zM4?^=2Hj7l@TbAna*u zcf8&sa?TlJS7(cjj|jvnRM80EXCszS&;a!*|1}L*mY0@9A9=^yMCZvHT9O%-W2P>5 zua$q7{IZGjDhxLLQ_vo@wd~N_kI0jWO(>4tvT*34XIQ0F<)Q);ky{uDyXIVLu3=iD7K)z=>T-kn<857Uo@-=c(f6HXPu7` zXO=Bp!n_EM% z$|GqseYc=CYKLRvBQx6ePn(Th6;QIi-LRo=EQ1mRFG6ZHy)xr&IKSZrNN>!Vs_ORn z3|*;W#Fh|XfBG@s~7f9DF=5V#@LL#736$90f%-81U5 zWsbXi)btL?QkMxK8a zcT?dUvH6lfu%F2Fk&WhL6YVb}tinngLb2HFD{d#Gc-7*)v+yUGwLzqNgN=i-;Ccw6 ztWI8RIMvyEBEIAJ!5;zNk8J;I=Dy zY(H+5_P41`FdCy!sLeo;3Ksdmhxd8e4!hn>ci&yy2Uu-uv6}|6w!Xaz*x)n8`Pl9n zP3!}H&e`zSJ!08W9AfsdQNB`%Ym|?!T#mRy83gJw>!{Ti?cH5A&i(Cbgmgs~+Q}Xr!pZF+Ze1$?Gw47s$ix??bgy}?n zVSYoe_Y!}|$cNn#w9$ZJSbZ&Z5&25Cz*z0-P7%oeUAf)jq`zXX$!zwDvjRx=_d)EX z2I>Tb`4$@f8R>%FP=3y+9}_3UQGuLH)+H@4bwU4?EF})mt9O;RNxQ~hz2?<)m0yNi zwFm8MN;Tb9()7>SyIQqpF0$}*K6Bvx+q$r|*t^ie8_r%3C$7_rvWM3#LveZ|%J6yt z#6DoSrJj-_c4sYirTFosvt@W~{A{;Y1YIi4RPNbe#!R2~m6hACwANoJ@oirs^Zyn-6*MnWS&l;$}dDgQLl?M1B_>7%n; z{IX=^!;01oOvgUppvboIsS@dBH5hwHBi=jV*tyifW5b5Jc5BXwP6ISDR*N^qMa)P@eJH1>uw~vf4*C)Q{rqdcXcfzgDHWzFEX2 z%6w$#r^S`H2=w~=%=?YY-I1J|QlTjp)Q*9Lv`F39y(8a-$O9!7kvy#x{wPl%AcIDi zc*lS{%-jL1)oF*aT5r5F)Y43Y_(3iGEk!`Hn1(Lrox;eB&aw5NAlM7%N!U_ zl1Z&PTkNyVAN49bEc%`MH_3(gMee;mO($6HDMCv#fkZ4wQ@Kx%N{bnkB!$reB&Vyz zzv$we43Sr@x3L^=D%o|i=UuZ~%0T>^t*Y$)^aw+vGz}d**w>b|9b*?!ycvGJ6@4DwoN4G-noth4Z6tn;d@5#~f5VTr?l!?|+(q82DSwQJaYilo z0S&xkAGUq(B@NS=19~=9iCfsn(s?~hc5E7fuo#{hGxI#ERG&WdRjpaCY*2PHL1O7V z-NA5&sb{-3$~ENN4 zcU-3FsgfT_&^4G)?=ut=u~|jK<@IhoF4tMf%Y=*X#N7s5Riqu?ixrIIhzTjIOKaMcgHN-fY4xiZ5V-AAwJyIVPY77CZxYetJj3evEBRExC`n^= zM@lNC(Yf>IgE9uu>@T(Z0K}xk(J}#X9BCCCCSEw^HAggrbxYs6dg`N(nwaBkE4CJu z7^;)#N|YU~9`v-@1Lx3gT!1|%n1oLgj@I$S5J)0=(*FBk+psl}%t)Uv@MUit&!kau zvzN9kT(u&RxZckfIrT~ceVhgeB z@5QUSTagTS+Y|r*+ei!!@R`O`t~ge9ID%BL5^=-P6M61GjfOPteTJQBf8saMHEsVa zXv=uTqObGb!*xk^P;Pxs8Dr4S(#~wp`l=+PQGRqmhqm~)Vz~mT-4zJ#?HcSn7x`^FE8J@ydgh__sd%qRb8&otcz%NEP|^&3DRUTRyKkx z`XR-6AxOHd-7u!iqU;sDZc|IsMW|s=*7aLdhGOd*;k z;p#=0KxpDIiR>+zU2~Va3Z4rR9z@xfy1%p7zDc#Vb0w~%Nvrk2S``j@=c|L>uVtDi zi?-7m_Mn;z@*jlWkCbz4K7SQ{|D1eJ|J3`)+2g_?B3aGco1|iCKk{Swwk}pJyf2z` z*W1$6T;W2ru?I%=@(3d+BU$nmQaz^@f;Sfm#ohte&{6$KYUj1HEFEj#^=RK;{$gH@ z0AaT^Pa&tW@6vq0(UhXP0izAKiQ)$`&Vu^!GnGQ@E+!@)nZ9J_UI%ydOKIe*3(K3v zp^~>$cq*Gk-;bh(O~H5$1X`$+aXU$LOyALQ&c`(0Y^d6`rF<;Hxk$gel(GG^L>JzV*2Ne5M8Bzqtl3viT+c(dmh>%Vf zCpv0seixqTE}^-l6%ktO5}&hS0r_1<0=@O?ipI`$8ZCG{ndT4zk#busygyoIGGBWB zWm_{PX2wj`-~8gr)$q{0jfoHS3zX&lCKhxqzBLv-T1Q*)w3-e2rpOyb7M@zgL5^}Q zN)45qI6G>BF2@~=`DhqaxTTA;Aaw=a6Owib)@up3S{b_GmXE6^#HP6%0t?hS;r%<& zJo!6>1$E(0Uk~gM4tn*a(9Jz|#fF(^mX~xJ1PaZSaXvOcNUlb5wA(9R8ESoogq@+M z#%8YB5+7N9zU(cwd-G|(I?Crxf_uf9)U%ix*&y_N4H}eS01#&ustq~1dvKLD=A2*FsmUhwl#NbAPN+FMF zX9h~~ZlZV}_>VD9^)05moRDw6W^Mx3Yk3 z(}C{D=aniuW#kPDxyCt_;)i;QS$iqoj&dJrx)D7#hP`man+1=XNX@4+2n6ev1z*Dx z`sa5N9lOoyN7qm716nNiN_XPC1Quj?TP!)d9u>FW{)fPFgPq*8&^OsvM>l z7QNk!@&f#YDkBs+P)u_}u2;{M&x>0e(3estjQ<5g*)tzHVRtJUB8>XHIZM0BPy5og z;x_hT;yU}Cys)^OqCe-ip|{}B4dhF4e$jgL!Fzs>}C z*?^ih#glOs>1U@}K8r_Ln05d~;sl*fL*lu&>RK_w zAt(3|c3v{c1pTh_k4h{%u(w4meRkvvx=({Z3(6@rZ7_JZg3GB(qFD^bk4Ww>Rlt%= zQl-LF4b!^IrC$8n=Y?lEN6fIQl*Xua5f;R_!Xbh+Ph#{BXw%{x2(OzgWpo+%ofQNI ze2xt@U-sXzJlj^haVH>1x_@hvNu$iwO{#)aFkcPpFA%h2+z7|cDyo9dwU)ju#;C|Z zExdd>LP}TE=ydmzw3Kc~>1pZy1cUtei8&ewv<>HD=HH*S{EPMg%UFjom`e^BO(Q83 zX{LbfQn`P?v`+0rOCa`#4sLj#a0q|;q}{C=Fr66N>FeuqjazsJ$e-atUN$c9EJyTM zzjpHl4K_b=_jSEB(d9~paKon@B!ue5{Y9{;lzJ3<5FrJNRWj>Ld)v<OTLqF8)0|sy@Nz3E^F1Z?&{6*|c z{HqN5h<}o6v0dJ9w|*b6{RCnZ9x#@@;dy%>5XRe;JcIo`SyzA|r2XDXHligjXWG#z z<%Z}aJWA&LH@JyG?RHiF`Bl-xfy*0(Z@^3qMz3(`6`P%vt75;=Vo@x|i(-;tc}ZGZ zvwXUxboJtWz^{&rc0v09cUoq=q9r*?;gRE=i)+?ymVOWIhuv?kOqeXd&&JhiyN7LN zus*{3vrM{AGjZrR!iV{n@E_<@PM%pE*x5U3j{V95A|9RTSzFaq%x!{MZ}@&wFAUw& zJBmBI884RSvCV=66-d@=WP2C-COo)Zp6ZL&N50x5kw`m%P`|ao!(ASOQQ0wN*5Jxt zYQrl#FrE@s^pp|Xb>+0@J$0eSq2&=hrO4HE%P57xtc)46FiFv^ad|qmSX;wX*cv>j zKwPp>cfRy00UDH1e|g;ETgffp9R7bPYRaUB`;5D7`0yl}F!GXw$xbq($j|0^1@rEs zq$IXM_5$0ce*GyFR}@+7cb>bx>(+7Oc?j6xcXqG{!5y_O@TPQrF{$LJ=Iadd!6957 z;|S8a!&3F8mhHkAG0IH-!|`1jsSU@dvZ-`{8LQ=DRs2PAsNp-7Snr*Vlg}&Qjw|pg ztV#6hrMBUTxa_!RX?*G3n<(Oaq1E|_R4*D5$4lWG%)gy0#Y2Oh&AyV91tW!|P&@hlMb*BIW!a9gN_TqTrE0AYtJ!zXP*>C~ zYulG9(cc{R0km6P0oXN&2X)xeFD0_aeSY??PE-s)Ob z2QWHwzxn7PUXLD)-CWri?$^V6a-JFqt4-i^w%;%2Y&ofL2)BRPB6jXoZ+q?znr;`S z8#Md}`EtGp!q#&j=xsf7p{K6hz6TJ-){lMiFLkc+HmS79@-s&&uDw)D?z^;#oOo|W zD-4GS4LMdaqAMIL(xwtdZ&tl_0xRP0f9h=+l+L2pN9j+t9wls^t+=>=zbdT>3~ONT z6lcR!OSP_?h#@|qfx@uAZvth^fEA^PUh{M7YnA8ECDooAlw3m~=589!KkI2c>mpnf zB<+^Q@JZogTX<1!L5hntEDJ*X+?Es;WeOR0Mfzb)c|p6%P$;^Dd=?ZuG4G^uZl}-D zf2}3wMz6uxHv8-zwA6M>TD63M)-2qDElm2JFjCnQ-7$w>JSdOZ9&~!qQlTs_5 zIC85&49w@CwA-|8#Ql-N>iiy2S8T1c>zD_+EaAkbJ5N8>dOpd{IM zNX39>6H1sJk+3&q_<8!3f)c$X^KHJ{fL%d?hPlvG8s*Cb+lSDQCh{ORmoYK#aNPw7 zH^vsnWm6W#4w8J*gov7DIhQWD;*uFp7D5gebiRpM;5?mJpBhvtfBQ*ur4yfV+|Nmv z9>ckDN7;EM2q`kJt67dmYT()ZsN44QZ<@!5(k_Mi+>^XJA6S?`pq+mOs@@6QGN=QR z^SM$r&aS;{etUek<`>20?#NpcT z72*KfDYRHNuhu`$rZ5wBTHnH9fkLUVm^i*DrEc>^eIm2A`4+(>E89_dBEHB!-xq_y zfQ9171*m?9(;XSZ9(|*FrK2FxVxnkpaIs4%j_o|NsgTAOQ0LEKi4z**BThL)ncb4Y zA&Z@pP|d}kG~V>Kp0G^!th)S^9W=4=ikBFz#8pCLw6OkWCEF-f09j!r`P!_Z4vJjM zZzca2yPz+9vg=PBZ+iGBe)li3@iB?NXmi(9vO>3)Iprb`eJ$^M%64- zau#@wSRq$&W!_u|XmW+I7(|H`fru)c&9R@1G&|k$IG-)bDNAP*rteZOlJn75!p}OW zR$Zh`0^6CdV~>S3IjtI>a#pU~Iux%UibPy-dN+M}0@)HsJX+_geY4|6c`=P0@PJQ5 zvdXABtj?@ZBmxCmCZE*O^{904i|Z@q3KN{^MGiw;ynC4oVx|Q-6B5kW98L2Uwq+T+#qeZaWhp2t2QHN2niuR-{bisHP!rTPtGUDewKc`taV{mp>@#TAq!Er5G* zRS^SsI&i)F6D~jVgjt-@g#m+>Jo|yMK)n7;Otzr@NXr0vV;2Y%Qrg?>`E?`hQ-Psr zZ_^nlay;OW*~eDzqCcK%bhcM@ph$`W6$ty?^+MlxX}(IxsW* zEr{jRNfPG+sJV=uj`VbrCR9QE)`vOT4fvwLy(HT*h#R^xj$SvYj1(#b24;S|{D42L z3{M|ZmvVtBibr-yRS#sl)GdrPj5SYpRf;^==4V@SANH*2C!~uNskENfKEecmPPGSj6vmBv<`?5DHpI0%tnmI@$#au}KwO6gK!kaqQpu1+NL`1QF-D(lXg{ls z{i4v(*eMaEdANG~XYV_HTb<1=2S2sa-?qU&S6iwcxZYQ-5*(44R6j8){wi$x0RoaF z`<9`;*{wmQY|w3pWjC!If{0}E^wRgy4dW}53)i9#AE>TKALEYrSXkLD7OSgWH$HDL zEFH9!-Hb{Q=|%qptI&8@rI23PmsmdFAh$>gev*+N+llg^FTzpx4maH-a7wN^q3H|W z7ibas;y+Y*AoR^=LT_X7_?_w1yWOGhd6!>4t6N=YZy3C?7{;hEYAM|aY77H65Ez(R zn65>;_|)W$T4-Q7yWQAMfltjUyeopG_kI>jpXZ984IyAe@mAMhdE{Z#Z4M)LxVjKHqz3t+pU7g5Wq#Oa@Kc`8uqQ%Pr$REajVZCPv`sf@agR1B`S3n zi+hLBP4}>*c2iIQRJK@0r1vSWJwtkhm1C*5U-UPFWK%hI_Up)1{ZOth;mz<85X&eO--%AGvf9k8`;?`!|U=(_5&OtMPhE1wTDmk?zQ$;;inT5xeNA{#_55RHk6UqY%ApEo*g@Uu*I*zT1f0drqPlEZix zZq{XgxD1e~VC2?#<7OcPJ(^G?-)5oxW{F0v4$SCx7)Rk~9jFvp)>dsF4uIzBrZZ6 zrmij*r>*WHfH7`y{st^eMlPCAAOWZ*QJp99sZ?Jbc>k8M@?o zhl44O6A|{@7r%gYPQ-=By^o4&Q6n16{5g^)YCkx(5=M!P+wf@FVq?h;IH&IQ?2B`v z_%F6*YupRJ%AL*@C{Y;!QG4yspAHVH_Oh}*87~F6Zo`i+g8mRN9^UO0_K%GXe}OOr z-I47zON4HKhg#z@H;4->SKfYx-mGKoEn+YCHJ@93y0#$0pH~vfrfis<>gn~j_v;M| zMsSfyi(`vg)7>%A48Ze$aUS_Q+TRHLe;{yRgTinG^9~*!{lmT0VC#;I!8dY{*VAWX zvj+jE#4S|nKK;Y`LHBRR-x$<=`u{5iSCoWw&dnSIoK=M}sp&MG8TeTkERaHBvi?K= zIhg8eCxH(DA58tJ4t3+QW~K2NlDly{9o4NbyIic#=of4I|M6zs{p25i zhW%fr4xXGmpFBub|4?{i(0Y79g81bAUz8`))>j(azsJZ7e-z^!{2c%|IzXy@J3yk* zCIxkV<>uy5OK1s%xMOILw4(-Z7YnoV(~!n^0>`Ar+9aa~=D94B=D6AcwM-o%_z(IF zr#}hlTzKQ~=m|9QZA?kfV5-^r-Q*Iwz4K-z)raHEk)N*an{P{y8<}k_=!7C&Y<^6j zZ?Y=Eg(7Bee`dr|GnAy*otDcq$&ylGk`|C*Qwuy79|c|3Af}8S9!M^`ym4~YPb^Ka zS3}+XiN$)0V1(^vio?y`)R4_k4jps}2`i!4?wu&DPbD_W6W3jS71akI|B3g37r{0i z=zyyC5&mgOqe`|Y4Pzl?Y}hA+en0_g$U^P97bv3utjut(qCY~JidLd}UaC!*8juc` zJA3&g{GOda2|~+xI6~o~H-NZj*>MQg^FJ%{$Y|d{R&c#w9jg7R(e|8TR^`bkx>VrY z^vM=0{=a-<=h&`T0X@WzWv}2{(PJuXs#?Sia(INWc;kG*zgigioCEd9)L^9WghpJlAjGRlL%5%ivGOf0??K z^2C2b{_WI0;NGg0;GfAHs=snDLjS6A*S+^vyncABga1U(lFm7}q@sUOEbkEdSMNU_ z>MWr1NyTK^{n7HPQXafdnb1?e-W%hoLa{L4;4$mNi|)>|7=QBHxW%h$tvXaw_P<#NlxWplLK z>4YoIp<8|D7c3N}9vBku9scK;sUuq>GhlH2<#J;WpZvy{%j*0C009M!|g%|uZvuq&K7V zPl-3J%6COGsE|UT`-HhtSRl=l?OKf6vH~z@l@(%a=>sV@wh!@c#_h}mTN!t4cxK!6 zh@%(!R9%lGNt%kE^Gx5o3{r7RnfYKF(YZM^S3mBr3I?wCsLFjFDnYWyrB65SzYGl^ zEB^jw+=c33X(tub{YSC+dtoGEee%cb8|9Cx?IL#i22VTr8D(|ZO!Gcn#R^1NOqgVk zwbk#jz~*zG?E+~fmD={Pz;8EG9(X>8P)WYXTZRpFBXR4{(MthLOGEfo+t~VZ0=O5n zxCE90oB+CgU|##SJC%6(f?GbpQ0h!eA(BOZq_2_cpjKh$m78PFIbziT@{s?4n6Tuj zDBFQg*9lVzjKX%ThxzfIO7*f5gITl=U;LBF+ak3UHq`@_{B=%l37X89fktYZNb<9W z=tB5QKzohvs|V9wgh^6~e2`K3)ALchGneo)VHnUwbHqPSq%TAG%Odzgd&5kI>6`@* zGEw;EtZoA4|6%Vvpqkp+wNc!55ET(niiIjcK#(F}u+Y1KgaDx^RYH@{1O${VNM{EK zNN5rugam?=010gi(xnr6K#<-`=*2&N-#Ont=R0TIaqk%aJ@=mRpOrB(-nr(QbFJ~N zxn_CaXFl(077_&Ez@?T~>0QTP8{CqmMd`|t;u#cW!u#(^Zx#d0D%H@$ao9sq5RY(v zV&;;uYJ>d-0mR0Y;fDfZ90{j`(?j}s+FYfZXXR|Qvf@3?eKeO(lARs3k}EV7Qf{Hs z*)+Gk-sd`X6W)7V;V_ph_X-G&x-7|k$$_efPSgppFcRMm+eClBLI!8wLsaX$@rB`b zxn10Vc}c@kczMltvU*l(lWg0Zu5dsEHgI6dE7D=#uSK((R~jRjQYDX!Rs&nX(lX4r zjTLTQjgFI7h_%i_&k#8{6aXhA2{O~(A1iBur~1ZK8J50XY5AgNmx9x-d6i3sHNhca z`{-~%8#eQ>$*n^v zy;EU8QZfZYjLj-=j!);jQ2Rc@u4NZ+i9%@Aya7J4}FrVHOoAd`JjY zEEhNO+oV|@`<=*6R()#IjVbZ2mwfCn#b?jp1B#ObO0oy-A9-@hL4A6Ls!HqN4a@5cPHZQ` zX7pHXa1{!1=8ieG(#;L63Kra}c)-LfqBVD+3S5g`mYG|jRy7xRY8`VVEjyE5(?`dg z;Coq0rMby$x4s-G&=XOO1~)Ej<98T9k1ful+R+*w?p-yCcjMAd3GraK#cvcM zwnY#kne4%SZ^vN#<&KK5NbF||IDREI>+4$Enn^@H6ySTlWl$e(SeE75^Q_Y|ugn6D zt)Y+>W$z!KG%vKE8{Afqq)C=G?>sy*fgV3z?N>LH?W8i?h#Pwh^wxL9(uUwlLyN5P z5djVDRnZQ_L{T}Z!}5o9d&vFIU0~{7=O!m>W7a^&V15ka^p*fa0x~X>ln+@9 ztCU7P%hay8agjWt0@Apd^pJV9@_Jvd_MTlKNm&C=?(povGnhTBzW@3A!2X#moqAR; zSsk(E`*;DswU5SltkDCAuB3JDY5KJhsa{V%f}!I0t2oR9!?QZoCy{&!3=Fca`-+`1j_RM~M{1o?ZJlUuN(djs zf#uSmK>MmHO6I~=OlDUJxxg(hEd8t8C2|K;L9%bfVy=i~2bu^C;A;BtmfK`#8<^bF zU>mWTFR@HA(hi$Z{ireV8Vm7v9}L4gnYwep*1~X2M798Cw`dF3-{Ib3E|zH(y~S7* z?`%)&(hKwGRwZ5#2=?Cm1>&^Vh;)(V95zWEewC0e*jleh@>m3`8t<>c@f1hg0v3yp z?lxed#!0m_5Y_53JtLa(y@RY)v%!+F?lDeSYr*c7Lf{g@z>+y_WftU1=`$DeQT(=KswQx|@iXRpsNuH@@P9mTW{fa&mwm{Y znn-D}USyMlx8SE7G*~#oropPJmVp^ozS_e96eZ)z%HGF)&K3aeK7Fm4zCdT*77krdFdW~lJFw=S%t4=5d_#<2b4D=cpWFVk5vuRj9C8`l=j zdfeF-V55x|?KQE6Te+0deVl~q@=6f{?hy_W3le4pUBJ{*=8i+oIdv9+r0ygcij!e zui|eZ z*I@rC%fxfJSnEc{X+fJ8;e_$`leHrGt3&@g8>SM+l$z|cWUQ& z=qu^Kddj<}PfyE;L+x&1#Ou^po52cWJOT{TEMFFQN_7OR#EI`M8!7ExFKQsnDD}=S zuRl%n6MMYK&Fb?qt}Q4=sN$}FvFcPkoM%>^Y=p)jX40bt3dB8)U9?P9oC{p+?&{ZD zrVdvZK&J4i#c!XyWoGR78Grm)j*>==hV}|Zo0>(27&}C~ww+^g;lsUDbaFY(I-uR# zB3rON;~=$?bbWH*m>IMAlj%N`#L@A0-=}`hc-D4^f6uxQPzKr8%|9(Vt7crj26>R% zJ}0hKCRgNVBGZ1sE2z^mt)>xkn5{Z>4{YYm0%6_awfX7B) zJsl7)y+>Izivc>dW`IsxK#w!}0AQ`2e8Irk=+LD$R&R$!?mv++Km~Jbu+Ss`U8dD zMC)I1bG7rQR)9MROq%}bOIyeRbBUh1xjEWzPk7QJ()bBT2y868vZ+5k9=gy6rut-q z>vEij7^r3`9fDkafKDgO^D2YT&SXSASzQ>yAY{Kv1YBo5rb8l3^FpLFyCm@eWm*sPFEu=bRbfVG`@PNdqRe_0%I z`ICoG4$>z>Pa04Jt(I!wqAs_%1rA!V-#GA2`;?RK2T*ibUOHgI)!yMK_%v5Rf8#x) zJcWXw#Gy4;BYm<|n|N0xi~xB2^7){=7Gc7KAKM89FC4|dM2c%PAYIu3f*=K-)FP6}%!Jab%I37O z5-qP*+EGW5QEjU0YNf)85IvMgnb*#3b4C%aew=@fl+OzK<%deusVu8Yp}ml4{|&mi z$b3ljKfV}f$RCco>oWpjRMl7H|-M>s0IN$Noz}pW&;zUhK)g`x6{v&Bq`d<%HeQOeAT#28-^MozE zG6JB8XcNyXXnn{fa!j5+$7rdJaBEyHvb&wVo6%Au%_9>Grwe7jDb@vtljK5sM@1{V zum%YTn^rwq-*-a8gSFqBM(I;}Hf9l3#_x9NKbg*7*BOZNkR|Qipu)H2lUPOzAqIam z^~$%lEBFRayBX8F?Klc`Nz0bZRJy2h|A@xEXo+I@_DfN$KYF%LPH$FXVfOqKW9IhB ztW5G*TTOVaV}E-Wl`QDN8&j8MYb~$bBy!V?JAH1XfJlKW?g2jKg^v(z(x-kht)$0e z+dT_P%&m!K9lHQ)bJFr2$cO}Fb`Et3)|#-klv%E!cnk9gZ?6C!7*|d`69t))SoPC! zRdR~uFn)`H=2;>K7a9XtSDhlvi}U=pPYIeryji}zh3%3U#&Y(2LmI+{ad~=YKzuQ< zwvpINPd;J~h)GYi|ow zPI*DgjM}A*8pNNf??I%Im`dg)?;pwb1AfxAD%2h=4)geWF~l(LW6{c^M`wczd0+S* zHYhsKFcy|i<8eMC|efs%QCu<*PC@OpuF1leRpun`zy76xx?hut&jihm0UXCh>SSvR6hwfBoX` zGwv*(c}l^G7;LCdmLmPmAl2WJ+A0!U@xE#?<4vj1nUtMM%tB?xGN1DH_5$Dy-`Mp| zU;R+i+o-s@9Ss5A7^mUC4VKSDNMjj!y_C4(IE=iY1hO+&P5+8IgA>;Z86Z^qf|ENU zT5Gj0%4F_kmY*O|S0H_c@G5dn4Pv>%H_PKXK!^)Z7#M-@4P9^9@V#iIy$0^gyBte} z8B|&>uax&ZIbHm>{(rM-W)HvemCk?bvj0;C17*LD*)DHP$i|BL)YJ@#4Oi3W8h~vY zX5cS-wf8fRv~n1Wo`FZIX7<#VJCy`4zRn51fTN>+*A1Gk&*n3L_v$pWRb%WIVt>6 z`x&D;8TN=Juj@@~YVK(iAGz$L2&a@Z| zMkVV9;Jp93>c3dJ=QP!t?Ksq+n>VeQjG7&M4@)H?@avG1-~Y{nTm&`G(W)Bi;_iW^ zEpMKtwW>1-KjGrj&7WGzMXW2On5`CDOXj^g^XeXDhp`Rxf!AvF@vG~f>K4cS4<`m{ zcJ}_X|DB~UV0H6@<-z#u3G@Yt>YdsSKL6FA)H5&tr_BH8;J;xz_%YdExnvT)`0Rd6 z^1^kl3)8gb=Pe+yy~^{WsTV&fXbEI{Zw>(5|IJtQKjMGTfYPmCvkxovJn5@X5%-gA zi3mFzrvTTdW@a*1($|gm@^|aU0OPV5*UY)jeb)AGfX$Trxb&LvrvJ^0SLXa*L{~Z7 z(4A4h$%0MO^^h;GB2VR3L28_o@9jjCw>va)ovyq4|5`N;?Jo<$$tIAN9YEy9H3>NB z)fCur`g9Ew!$QEMP=6s_$@|*aj*qOgLvfS&)DL#pBH{%1d8^k>Zw8O=?Sz)M$No5R z_5b*^=v~w|-E7U(FNkaGDuR+Uw7e9pL-)j=zZ%L-GpYx&Kn*G>s5;}&U{6G{8J>m* z^kCkk{uz6!uu8VZIp~^yJ#40F{cXhmr2npu-9-+BFeu9EDUd1hY=E919UuNs-`*FH zdwM(VmrfZR-UiP3I8~GH-V5t(RSA2#37-dSzL)%e*cN}g^Xs|(C>iOec#XF{?T`R3 z)7hpw+JAfE62f+msJG+S7cbLcq>H;9bFSxsNtCts?ZFf6-+mSP_s2a~?>T~vIY!R4 z3FE%Kf8qP-Gp{pFbw;Sx4_AO6un%L;lxaG^-M!6L9r#bp^U>ks124A-`gdGH+}0_U)VEsy$?5Ez4ieH1#oWIc-+y>n}}*fBo;Du^$KIy0gQ--@$ZY-zlM}JS&C$aTQ1GkNSR3 z8ZOI*2dYlaMZV7cSluWk;$WE!xHDFx`pw*-h{o7oxN=&zp1ImSo)xWr*%VM7R(m@Nw0gIQ(_!AF?OHY6e@Krba1$VjhtqodVzzv~Wm?FaR$7vlqCBkX)Ida*<9-dL)b;ni>v$zle=e8@;FXq-_I<;ojr{H3= zd?%XmP+2RsfBQPo-6AW?+3R91!(v|hz{w(YRU~cWTE7Ka$8(sYSG#Pcd0d4j+YA)~ zxfx<@gM1W`J&26Q!e8* zv3zKLx)1SnDlf+vP$oB>4A!HsxWPC1VrWPngRUztwF=hTUfIE^q!l%1of|6)!h2wZU~CQ0}kbL4LI|{$| zSs<(Y9QW06*t7kvkw<4z=*;;B9_1SWtLsH?I%KFLd&VW}okv3QlH`#+<(=KZ8V7Fp z@*%-=clJ2$T?FHu{>J`~c3uK6oP2eB|L|&lN>e>n`8U`3L*wz?(8K1Z>^=sQWj~p| z%Fn3i3A|;LVvThjOz)Do`i%PIxra3hx9v%~*SI90! zq@QX8FV@JHGBRZ_T4Qi>fAf?ln0hf9Kb;7MzTiq{d&rb|{QW@n_?^R{^UXy?d>LP< zBHULvFat^ouPTVabCCn|G^PVI9|z9u59|-T`NxOkhm(ify!UHQ|G7ONiQvQFzt%R`#4kf2&2-{Hb|yE*TA| zR9@qChbhS?A`ERquI^s1ynF>VbB>Il5k?We%u>s#&vk<9fxSV@~DW~^; z7zPSkb>AYn#5x#9N_@2IH2{fAe*p+R6)X%iSvgl@lyuEo!3V9n6@`+5vosd;pgPp_ zBjnZ6AJ|O7B_5rg&w#~>L3{L`Sb($)skUpHbJ_511wzQ)djZ=e4Yg-*zsP9FN;3S8 z=NchGzXeBQ+;+5*`e2WsC;@kutXtE1!95%JLjJa;1Kj=dn4BKWnOPY&5wWDAq z5%7Ryd2mjhgYIVyd8kRRkA$E6_0H@G?~-Rr<1djp!{HKTA<;ecy!lS$!&Al>1-^Q5 zt7NWH8l-$g1}5z?siSk#-t6^Q0t>tRD~6$N<&c*jFZubB$ZsQNt>SitLwsm7j~a)~ zV1HwQ({*M6cHco=kwJ2RaC?R%PLHuK9U{hi7xoo(Rjme8$fKn= z*w4>Hzy?X5nKS32Gmc(JJc=_u>{OH$a{Wfn(H?^5N983)tD;`K6ZG_Dgb7WJOm+XB zlbD#;ty8RocO0@3Cz^4SUqFcvdGwa?`j0P&)Yoe#+iq1jH|ZEIL~dr1A!UidX=#ET z<(qAQoX)F z!YhHqfiTRzoQ<22;>!?a=MAvSa@xO0buKZ2i_4_(U1wK1D`aR`0l_5L%!DF(INiQE4=US|LM@zVYZgfr3u@K(iU@q#w#V>8bAme*TZYNPHE>$7_{v%wZ?OK66~J$n^*+XR#aP?`zu>K zK)w%ZU(!;{<$OxSKS}?v(1H#u7gJ=hV|4;Ak`!7+Qs3WbE6p(`?Z{D0=HRz;8JmX{ z+T)y8u#1D!9c<9g_|41WDQeyG7&lP{L{80}2)DO46hPfxaYBI}7CuER18g5Rm#YsE zuXZ;l+e|x|r_JG$rM^`fY!q<)NZ!{xi}AV%9E}iaEYGf zd^dEQA7SjMll6%Jx8rW%J@kXb`D&-vcns(W6{Zi5;aJmyVcpVq&`R*TDsT z=jd_7?45{+WL39W7VZQtR{Y?I-9S0Bwh4^yVTW^Mr3YkJ8+N;SiT=>~t|0*l!{Z7%T{*aheupn)94glDvEE$dKiTnT z%~HzKqidrxaYd^!16naC(=R;=zEWHzarnZ$q}IjlHs`GkGtL&?4qd|=gr3UO_{F7y&WCK)cfzpB(Df&m(;LnJgJKZqO z2#T4+V+edvFXl%QEO~eAvj(8(YAKz3x`=W0{C1JygcJIq9=Avx7(5xHIz#QeD4OuV zNzZS@g)_a{7}WHt3rbN9CKW{Aq^7Ujnbz+EL*V5r{<{+B;q6|Rc9L-9P}_A4?1Y-_ z=9h#;;4(vXV_TLITORm{vLkVuf?nga%$I{O11EB27Cy(u$M8DzA&5QL-3g6Ll@81M z*7G5gAnpZLo(f|_78p_?IYx;b_}hCm$sW{{HVTpU1GOZRH%If|3dSBrM9xOV#VYXN>oJ~N#*H~Ooo?flvIs0V z1B-l!RhXmFG+ftc`t{oBF$|vMUEh==n(4mbd2W~k8UcjnWs>xs?R#rB)n?m(T2u1U zwdF!`1$rpy{$SZlI9q3%eGL*fp(=SQv}4fnQ&Ejh0IKZeke@Ar?0TMEK5bfI5c)~D1eve zf^yd50}cgioQ=_(jKWa4J}wKL=YVhMBh z)4X?kE`Rs4VLlzt#k=J>05ZS*%)Ult>gw!u`i(aXbRjf5-obM@KK$eX(A=NZ_r`dL+Eip8&+zPFSD7Q4ubUB4ee4~g{e2f`ZICWS&eT0-UhD~Rl zg)P4DbT2}ZY}Fvqud&<8U;K5TGWEJ~hu7he=)3IP z&*iS@80|b4N4S|!wm!0(4j@PJ;A;$mB((y$YVBYyW(xxa3og=TFY?1nqmYG*cbz<{ z3L5WCuNax}MAA596XBIyRfe$iK3kfIp$7*W;+vc6fiK##!_u^^S%1z+dVC8)8N(6S zr!;hS%hr9GKW_h_KELzG6p1u<~W>)q`+JKm|-qzf>--i zJjRAnr^dY5c;2fQ3U-E%lmJEDd*vbyyvE?#Xvd6N9&0E-*yxg4tH|m4KqGCqg^M4t zcA&4@Vnv2tm`u9>0#`>nvAr7Qmr&rp&Mj$1n`3}lV9{eWm}on7J>IQL{_+-6cs$@o zsK=5J|5?cJP3u0$_5fz`JmeZ-xM7mYpa)8chu<{garoXF_>N^IxY*OnlM9zT)tY8z z#{lb2QmxGF9u@1ks4S{DA9A~Oon#+fKFshxbhf%5E$e*@$v^a9M6b|mTk$?Ns6LjH zOm0M11_Am56@D^h=NSJo5T4V69O~a}3<^fYwc~5U>vucX42#`vUi`RwzA|?m7Bu;v z*#=Ie*d3dEI(!q99r|O(^2kG+fo}O&3zNzbwf6$*-Y;Ua;OU-=9E*(aHp{Sbj<=v1 z(!4BcHbzoJM@}Z+*brdj=%^F32$`I9KiP5i&W|_6(htzH&0D|1C*tuNsw#e7)xk%Z z0lPgx`Ryg}fbjXs1bKfw{HAOj(od6zU!kQohULSf>-t;M-_ElnL=Db-wX&%IlFTBX zWsNQC5;$mp%?BwugVVBFc(P+8teCvBy;(U0mK^F0b@Xe?1u-xZFhIL2Uu}4=$RG zsCX?Gn^BQLpUsPm!q{uRscx=H?yYi<3V+x$xp*XW` zt^8wnRr0F$B6RrORhLZ5qsxF@r=NBa0Tes*GRfyarip}m=#^L#6?-Ty{0im(B{e68>%?FCzy|PP@0USf>_&2B3 zW=%bE6-W=@V#!4&Cv`qX)dszXC5Mi+tJQtPdUBdFA zBw~Uikhw7EMG*=6d+{QEdmKl)AhYDDr)AmaoR1I@>ez54K5$?8%|;z!;w0zm8L#uK z4+L9RG8P?>7$4Owtn;~Oks~?!t+`+C=~k0=_-`VC3-si@KWgS$b?%x;)O6-7^pq*u zCY~2oQ=1p^KoAnX=S}nT=U%+FBxBs-W$vc~rlUsMGxHptJgaCei2)7L0j~k?P88g{ zi>}*Uh@uly8Wtx~Wn$90@$zv* z2oHZ{eel6!^l)}U@sg#2c(*b@q(OU5AgIn3o=;PotO7|5=iDD1+S2eoL_X4Esp;j6 znJ5akE7~*ORy+AgRc(UDvajjJL-yK=e%P6sZc~zH_o%%so3T;8E^;w6tGgK&QJq|O z@anXiV+M5RCsXVAW>UfZ5`s{rW{IJbUtaR5DChj#aGzHLZ?2lVLnVxYZEnibO6Ty3 z7s+{!D**zF0PZ{;O5_?Duk_I9gO8(A33W&=z61{w+*-;i41F*-V<$hjEzH(TZQO)* zkB^<uQ8l$Y}Fmd;Q-QNYC5 zM0YCWUu~%I#;cQDHyeVInVCLj@291#GpJ48D@bW=Darr%$@GQs`*l<}*K5N_0sENX z)xg}MCw%vFU3BNDPNT6lJ`ZzcrE$}iLDpx!49RsbGgzeN^s*+gP0zwP#LWrQMv{Y= z$|ClnahsOch>U*4xTd1WQ~S^-%_zl~ocl0DAnzQ=*Il1y(2{j3bs%Cr`l!A%_9xRc zk3qwSu5VcUYw zEfZI=Ff2w#UCDQgNxK%w&F|prW4Q`_K|J>^$?+J1IrMm>bwNd<_mH5rI};dpqB@`_ zqdC#7_2#~CaHxE{7=Ji^{>GG(St=D8cjNQ0dW0(}Gb`821Girl$*;gaFJJtzQ94Ai z4dWU5j^HwU;2(phhSxH0<8e!r2@;o^jaaAa?_oJt> z>tg;VDPFApeD_ns`8*}xd9?XHEIs1&*Xs22>L!wh^SfNLJ-hF&I=$u>OJX&!mfw6^ zv~Zv!>$2&vGbb<}k+Ce`-TE)K__vs-zd0xUw{s31w=97@R^Y^p7A|@p3I30jBcNtp60c)06}b;LBN-g? zy*Lbw-9nzIw`TslRJ~mA4SKC-JvQ`Sdqj{ub_LGS*&C2|$rr+k-k(Rg9 zvx7r!SI~OolE0}_RMlGLefg4nUa3Op>^J5-wX4}&2)W=l=Z%}(M9_1zkiDwM7D<>L zMGb0&eZ++yy#n$zUNYk0t!>lfc8G?MDm)^!+vktb6n?5)BsM-p6E2z++49EyE>S(Z zs8Z}bVgmsn0zXb6VjUEn)M=ut@&T&FM!niYnOI}ln*vA#18rU1#ES%mopqyF?S_0< zFL~20BUKZ?SXO<#`zShP#xe6f6BGXW_M>UdAkseI`9Gvn5QFvbAl5*S{H45ODanb# z4zG9OxiuUPle{mI(f$+0fwvp4{p#>|l*#O}T;LqyMeW2I9=W0@H@)O}YN0Dmqt ziNUFV9U@Vxq%$N`R?IyT_ztJtP9HP1iT)O?1i=*|?35meeBS)d3AAr2d7@6q)exyw zlBx9d+MeqT%3FQq83jLzwLw>rC9SbVBR9T@zPw|aFC*4du5B!$J@d7a27M$k^V)rq z`aLJemPa?g3o5UoWMTT8U=Hb4ig@u_Y%sFkpDz{lfKh(QX6Td6{+;gm)&6fQ#})w; znehexQQmFYz@{Rznw%vte&dz=#c^*ZjqIGP+W9@UyYf&G4LxC);3eZ#SBxazsL#5- z&QRJz)k_*q+0dmk$*_hiq2U3^QJdk_ayI!(NdW?M5xiXt>tO#GWvFYh$pO26ym?xB z_7@@UyL4~#2C(zK8)GZy+CnX==Xsc3#1Kr<%sVTndZ&#~Ce;NP&{gs?%oQ*DR*?YsN9?udEWa@l7kpLAyd6*}WTs?xmuiw3LSwhiv| z2QKbH9uXeP0k<+J#~cO=U8jAke#P7PR}W?cTLAW0lIy$3HwWSS-b@$n{d(vBe7vVx zeVsUT*cCOsJNyr?rttdOeC*p4N)SgmBP_b~<@dDS-GbV6*1bP^HNXFJ4gYES3g^F$ zxO%YKF!LaeW7|8wS;sGUyEXk}3cftmAkw!q&KLquojmoQmj4eYJBxlY0aT6(yN#Is zAwJ{B2Sw@ZI&G#o*!^phKkxD{4$N=86T7&@u;GyZ%1&2iL3tDFuJNQI7W)hr?`hQ8p<2fJm#?zv0mM>$d+SDLHLi z38}TWu6urr>9!I`;%SaDusTi%5n7dHYr5chwPVayCz)@SK?28!FV@4xbUqS6PgHE~ zeuD+xjMccbar@h%nl-3zM)jMt@M3TG*jW?l#d*%OxfzMWOOs8xSHV*MbkqMlc}0$M zrdrVIcOFP<^{CXb@X2el)!dDQ)G9Xi(m(LVBQ7f;9Gt|=d-P47(NlM0#*rPHDDWiH z6^eVyfNl8rfk|qg{?_cVy%>@KRE}qk3{kUta=Q!|-V$4bQx; zfB3>}XXV4*z#Fw2M_oUe6j#G?9$&oY9#T6_xjAdGe${_R<#4fvAI0?GOC#q`rog$N zG@q}aRobOZ;%)?(>Bre?u%m;_W3O>G1|R;Tj><{VXD6e0|0^rS!;S<%K0E>gC83vp zub_z(Y*>V<&B&Jzj6Dzjs>&Aim`Ut0(|I$e+BH#DPf-vz0#TTd-dl9@rQd8I_K`aK z_8)(quQELz#H}6u&KXn&c=+hWxpTtn4hPm*_1hQzp7H+HZ*G6jd)|<-91(kI@F~;( znU|y^PszuMi2G&gdaCN!8FjhP;}uTjKPv53%D*U7>hB7_{PsV*45t61FL?tgm6eVM*LH;;oO=ekcaVQ| znO1I;WpTKaYO%y`>z{+{{6$%eVDJ@RW~jdfQ-BU%n6vm2$PSHA>bGGMHY^?iHDb~= zZw?npJex}2{IT1h@nPvU6Z`9uqax76!I`^1nVi9|1M4nd)|>3y{*`&{?>XllpBnp@ z%rB9TMmTQV{QREjfA=+~ZZR?a4TH#d`2RWnf2RR1rX57+SPA9$TD*NrFluTgE-u7}!rq-LSo+A4yrcDRETt|aD~m|@8H zANYy;=_?Xyhdtp1r7i;+(Bkw(9!>?2@%2gbCVr8J3`zW!2tCIh_LusM}@$`3(gzH3mu2dJ=aXWf1U+VhV4F;gEDAOM0ugZaHtk6rv&lg(T> z(eR^BRsjoMPwf>ok(^)ygVW|a7+Lo*n*|`ct5&}=pj$pRV9s%)1s)n}^5Di2>Pj1mUO> z*sTBy+x>`~f;UB3LtTyI#pbTF*T1Lwa|E2}8&bO%jV)g|)l+V^-)tKlZbD)On~a3Z zLu$X27g>$B0E@e49y7>*ZzN=+kuqM5WfXZ?nj%%6nL1G!d%Grm!$D?%4-r*tSH-MOx;2!zh~hHSfgE@~Blqx>`O`-3wR;6%1O2Pi zm`~{R_0<4Rw@`b%uGWU{GsJBUrus9BRGwQULo0W2>AQ3!84^A zEK+Uw_T+k;+w|ub01k`onxzmF>zFp&R?Ya})ifjg%>Cg^v-cTY6qo%HL!)6Ed<@D? zL&479=X12~nl2XF9YBsT?Qj)! z?bEjHdQ|$#3aL4{&hCCxr25CR0Re)29Mr2`&L#`DklmUM2VAnR@+Lz&N}W3fp1jDZ zT&$d}20{200k7kw0RX%EUta%Ud9T*w2*JNAb5)WgS={xK*E{_NMsP$LYdeCj=$qC_ zuz}oLIP%grjsEL;B#Lvb3fM`fj?_9=kgyPGB<;~;!?UBa;jtC!+UBVwS={1Vqaa#3 zeJM$%SYkmtufZ&mHdfOCf8nBIDnYD~bu{}t{Ic6R`@^+*fAhRS!uyHAurPD)bV|%J zuQ97UcBPNv8hIf)DjTWjWl(I%TWR!gukeQ1FhO%cQQHv?$cH|79**B>5xPIzkMV#& z^v|h2Nn-tDWRmQ1qs2f8)SXRC1{vMKH!93;keGdyGTj_=y}XDEbVy9P)dXAB4sXPO z%>f|Ccz)P2k_5Maf=t~9I2X=eYg8Rp07cA!*psdsq4AY*9@5@U*1h`xeDzBIHhLf4 z94G&0VT??x3bq0qEZ!1>- zlvyNMG5GzdJMUfMBG6n~Jy8B~-Wm)x2+cYT6A!_KyY5xyXX>TSDT*6mgp*KSz*q z689UCpkO};YeUemnnXhXb|q_Gfon#YJf2DlPsLAg-AH`F)l*rgocp<>2_w}Ww)h$0{_B0Yr8 z8RYSMii{%u9Z?mRehw8zXlE0I7eX-@3F?0W8wppE(j7k&1=p^*{IT}CtdQUmeK}CmBZ-l$NdSUq^IGVZpb&*26 zi`jK`Vz?T=s+^JiZyu!jYAC_0Hv$_&KOeEONw}B@qk^~ zhv!y3-pATk;cU!t16|EA`iH;>dw*;ozQ76@)c@loBYg9pNgLN6Y5hJ3cBQ6oE_0Wa z-5Ea4@p{v4yoH|g_O7@Rg`bOaL<0z@bbHxu<-6M3MD{z&eXEm^ zQ7<0PQB1e`I|{r?=Lme5lIC<1wz#i{*i{a28zM61&Iijc%47zCe}mqBInUWAhO~So z>AqTZ+FiNXrv=#Hf+2(*QB75CG8qEr_dy>#&oFNbn#3N#-q!Ue6cC4(>*5UF0lWEq z@;X(2GF_prAc^sFF$3_}<}7Ts77zC;xIza4u_ep?6}!yo)3YqM1-de+NuUWpdtzF7 z#2mLpy(4W&@M^$U0_rne_UbpVDNh6R+A5rYG(t~vF!k*-&jPbdO&8`jAj_g6Z!2>WQd=fasTpc317;XU0+`k%k+h^1G@|1AOUZ| z=Bbc%g%xYbvkLjqY!Ibh-!XvE_T`=Xj@#LAyui?Xi+TOMZ?-yPF2LFWd~nxq)s7gN9z4z99Hk87Z)+%+(c@IT zAs`SN8-zn@=nDvOR(BX`={y7=a^~?+Q`?8|FL&%*7u?&>Gt=z7F?=(hkt=Z;z{Mm6 z!Ml45BOvXdSIM(MQ?CreVod0t!(NsVMLr-#Kpr9k5B#UB{n`K!yRev6g9)Xq_Vn~g*`U(QI9C}#xs%oTb3bE)nV2v>1`CVF>DI-ao;Ykc z6vz!wNM5CR8%=S4fLd*_vn8eEsOf5wHL0Nd&cH#R6AD-_yG32EkRoasih3?w zfO2hYTD5~+0*?1sx^3$}jqK5!_87&_DnJ4FDysgi^9$`(Zwh)p58@XoPUcG0ypo}^ z#;zOnJ3bYN*|rb-ik)7>5~y_907P8e=z$=T$i;`No}V*1cS1JLOKsLj>r_(fEQE3j z9DCOZgsbR0gYmf?ZdWy$kFv1ug#iUyKSEWv^rn6VE?K(edfq0)fW?`+eQ_vur4G_gPzs_xe<4 z@Lu!nPds{kP7{m!w%|F|jfHazG6^9FhtWkf{jdCg@=-}f#{C9R@X>zwlxMNhxs(pX zH1zzJ2YHXI2h7m*y2Jh!V;FAl!)s}k0))OCUTynYKMOT5gWUA06zt{K+zl#rmEBd-l|0LzE3_i;=XI#|eo8?jhEijhh|gRU zjHRisD)qyGJh{dTj2g3Bq5Q^Vve6VgoVpSDOJlHZ&c_{nY;#^yUAr;=g(V z2Wt33TU*ZiBGb;%@H9RBGzaFS^vMtl-LbHM9aA=K*f$vj^7+(%CF1|!H9>m;q1ANy z#+#4(62(M)zNsR1P8tx#`2=0-=L{ZRd6We)OlZ&j%w(?rCz}%cGC+s9^8)-Gqnsfv zy=~~F8EVeof(!Zke`?=T+f$G{gukiV?fvJ4FkCDP5+qeo?f1Wu*jZI}GH`d`uyvpQ zE8N|gSJGRPGvo zME0sWSsD(k_)LZ;uYNQ5M1J^h!v0>uzBoTOtAQ=qlV?qf6xi#{vwyjBb-)A#TfOH46hM5%)kXxFc_hG3g>MFQMg26-vl$ z+qw^4-uJt7ChE3olke6J#>5UetefOy8|B(mCm(>S`Rej~->yL~{~A>t_@!;ojo^@Qj)+c{UmT2UfE+mi7gOqGFdYi=D}I{u^XK&91izpzq^A zi$#C#q-Rn%;oLP2*au(1N4ANdqfBVhy;BRAbnF}iQmQ$toN5t4JNP2@w;&8^nwG$kq`_*`f^ zk_?>5ZzqH0-}fi2Dg;Z!UkFeoZ0e`kt$)T-(HCB`XggIlb(P1~l}{~@1p zYot~NVZtwWF#KZnF@-@YZAv=q-ngwux0)N9{+A*f8t!XFHV{V`^|f_!nhNqynLY3K ziOIbypyy<%-Je%M%ZsIjd@&wzCB{cLgTY~v3qpqqZ^SFUSc%Xh`qb#MG?fijBNu~J zcb+7k7sxeO+!OI0a_KkR4b1kp%6FdB=W10S%^D!p6PoT7sD$vKiQWQpo9dns3kyOf zE_NFUsSh56GHsvv?1W`x+|B-W-)t+wci!+w%4w?+(oc@Sy?6+1Xd+G`Cgq&ldO*Br zMs1hFMmxr2U+JDN>%=``@m51IYc)t9OBqx>a5rgw)Uoi!(;$UlKJcQdi|oRl z(rh2kjrQQMyVd$@k%jcFdaIqgj-;~$kEoo1ihlI*BBtih*;O7=c|LIDg+E;OIN zThH`alvT3N3;F4f8=VUg@dGjHoYBmTG9~gmPpTF=pwIB_$CrB_hQaqDX!nq$ElEfa;KlW_Mk%Z zvbXWEm=*OXkwMC5VqH%}bm-`jH*-N7_CjB}PI%0FI?PQ!ii^3fW1Lf=&IQp>71tEY z(Ded(^VN7_HWyERwa|Z|bpAX(ArLNEH$4@L&y@dAyqgo8PuvW|4|%*s#kwlZqH?|8wEHA^dPOs0>c%__?s{ff<sH zN?n>EiC{hsTGb5;KsZ6HiR)AhBz!l6sfelJFYPVOf&HdV|2o??Hx~C)B3ISy_C+^M zv$C%5HzancRjho)g1(SI$Qx?|>5Wx-6HH|I;v^3-c0?9_qbzu7{U^uixF0J;dy+bd z(id|XTm8UnRNnSrnBT_50cuI%cbG)yJCXLM_;N2oIp*M=K6JW=S&X0-6ofnFzq7&1(9uq3|?* zKu(;v^ZX+&nn!uON&QUQe)Zf3uU7W;G<*tuB+QX8H$J2OdywzC&}aRJA~nj}39CW^ z-&^jTy?m63{^4AAM3)UXI(9JjzbXaeQO&^>elt+zYK_)`Uc<%b+Tq@rt3CYJUHnhZ zf@u-LW)?|0MS8uW8Z!F_weH!Xzz6WJ$GfQ6c4NJDEl;4ll#u$p7`{KZk3o=jlXg|kM{t|rj9*1opcViWqG^b5Hh65lXzy0~dO*z6 z;73$Oqyp2xw^&u7rZpn$7jNmHFRGYUzA};7q z2f7|G9CHG+=&nZG(pr$hEEL$l#TRoCN{Yx(^DL^G%Aj+s()nHcFX^@9-T`6%xllEr<%;lq zv96)e0ABP(OpR;C%Vt;C4x>Gb3 z2R}CVg$wuakR^zR(>{>?Qn8FyuUdJYzJNV)JUxuyW<|V{d;yXnil~4Lvhl?S4AI$E zUYl7ONqrqsJnPUP_vwIw>ve2=3mBXh8w0dkBrDm%1FJtY)!1H~GGL63P9cEpUj_nJ zjFWrt4<*XQi0b`CmEG((!d7us+!&k22EvGoBC4hI1&|)O z2^IxNu}Kd=#sC4hgXdy>F0{j^w3SnalXuDEEE@}P6=XN9A|ZH3{^LAjPe#)_=RN*& zbcu;Qw}p!@g2dI)m9J6|p`%-@Muc3e?fI~*ws0f)3+Z1k<9~K`#QskmH2hN~|HEti zS4qP^b?~YEpDOvU3;zpeJDJ_zB23*6KIh=fEpTM%m3r3akJ-aYkoO=}q49kePn#hw zDWN6m)cDLiD#H{s485;@Kk{hP5@QVrYHJ7Vsk1v@AQUyxU80fqN%5Ur;43{GRmXbQp6&BH;| zc9`26Lw;o-KFKHdUXH(=#KDa#u+SYqpBl~#3`Q`kOl);Citsq&6+fLeKZkq9IXFcL zB)#CV3p{jaeiruz<9N~x?fur>z;0P&!q?7ii zL(6Oc#PP@qv1V1_Q8Z6c$d{e0_lXY_FVB05=2o{@>(rkt(NAuQ?p(;3*1QARHAVL< zwl`|>@VM%Uhhy+~+s&l!yq|(zo}Lgcm2jr>UCKI%Eb`6w^+A<^=Z9P=x-=h_+}b5J z6RD&MlII-Bv&i}eu^0u4mc-993Zo9%F>}^oEHd}I{ZbO_CS?~Q6nx3RANKIS-9!$M zbLY<@U!ueiH`mg`0pqR&xq)UWT@5$NoTszN~{>XBQaqp7)+p(jW1J%hfDOZzJ zeAVlXT{E2~%R*E_wm`sy(07Pp!s+^RSm(N9zdO_SR3b*cU*3__Xt{R^WyT)rK1+~x zZZIRyMEuHe1SIilWxwBPIP0Ie4x#5e5>_SlML70svEh0TP9>GHQJYY=p*>Pu_Pj8S zu|BJ%xZwBr7TLa`Cmd>;zwYXU+r#cEA%7VcOCs^SOTMSwCS7wGE>fE2vuNKg7#4U- zz+wndzPwr5=TK5)BDHm~gh+x%v3DQ2{P2W9hrP}L(>LYr9wSq_a7gR^*%YvHA4`7u zdPJ|~7@_} zr91L{#qoePVmN&)7{r*w@&K>vk88u)pRE4Qb(u2HnZ95TGsajhC^hM>JO*3*Kqq{^ zNZ-i0-9^i|LQesx0OI+kaj{3#C0}dH_wqDtD&Y;qt94iAhV{;7; z*=WP3FS%0d%0>{A^bD&i`lynL0muiJoI`ZY_Y_87Cp+xdd-<*$a;N5+XI&^1yaLfq9I-jiR5&}JS`LFKh zFVolq#Vd9UHS$|kW$2rddQLU|N0-`K; z#VAOB92^`(JKZ@mwQu@9CF5W6^z0;i8X8Mf0AIBaW+laLs*mVwtvl ztZv*rAHOUuAgDvn5UQJRy_lsaCC#-GfY4MPY`7Hu`!R2ohFjR#R2V=?JmTaF;Whhh z3^;!K)2gCuN1-&0!JvDl@pw$XKzhk42!QRTHglXAK5S-h8RHQ9Oq->bOsIit!Z}mx zU)x0331TIeHP6MqWhwQfB8+G>&;40>Fk#tFqMWm%(w2&=0C(F2fl3>Kzs}g80DAgK z@+wM7MB!X`)Q(@=n9h>THko%@tjf}Sbo@7IpP*o1dGTEx)?_rYLIrR)s#xQ8@~~+w z4GEx*Ey94@%XE2w^otL(07DeTIQPXx!>WsY_7LxOD>_u~5Riv#ofjaVRGO}*+E$mu zb*V|Kc$wOADBeBzE4V}iNAN_v`d};)Ny_y2Zwqk=dq47^Snbe(vKHqN;byy`CG-%EiYVfsU3 zlPMm~Q;A9U&$J}#=`YU`iJ4k$i)V1}aK060S#CEg2Aw&sBR@Uxd>j1g$H1Lu9IcD; zd*pZh&b#(8*A7S&wrtiOu})kLIsf}fY}qBolFbHIhd}fY5jUS zf6M4|mL-X@437-B{3_>%)Fu3M)%$jOJ%z6Nx zO3MG9-^-z1|2xM%>ZV9}_~GoHl}dy_RBs25(dhA@>-zBmNqkE-)oWGgqerD{^{5Kq z#qf&GBZV*5I%-c~c-T6~PjCEc%-*^(vV6CJX)th1atjl|!#P`Cs<#1_~hvaO6`^C9w^v6w2 zQZ(CIi+XBxmBsnkD@9;5NhW<~ocdkFQRI$+Q|cv`qL~3?*<6cAjZA3io@4{Oz&paJ zA@rlw>p&P^ZKSD;nWdS=e)AN1XzPaf>!Mm1=p@@OAj~0c)oxo4j`%UCQ)Ce?^r$d)GE2uI%t)9Bp&K#j@}B>9(&kvmvg2*gTz$qRSlr(US0wckmbAs*Rn= zmSy!?AD^CE4I;5xR-KUQpJ1>6B5@jC&MZQz1$#!G(0Hl+`k!U5_Yd2>%<^z2A}f_^ zeBy>*t&BCF)cWUTA=bI#q9R;=oK3RvW-~oh6lUjt;59dYq8Z&v^bsamYa@-PV=j~Q zEOh$Jiiwn6P55|1N(RZtm${<;;6jVE^Ys2dWbKdK&i5?rpCcji3+`K}o{tmAB)8=h zBP9ITjp{nVAzA)*(mlC%4%6~ubZ0tw=IJ>Y2xRK+YWd*k$Aa}2Clnr;-wGphQ^rrr!M?q(uXt7Hf>>`rXWnn$P+zG#&F9fjd?z3ViUs% zvQ8Biz0hBZ8RsD&C0!E$kx6xoVg!Y0W;ihtA)2OljMdp8U2J3Yq?}~Yd3v67u^7b4 zwSVI1#5GVOlcv<+uyCY%;{z_~&L}Z*y3NO;bj~@apBl+FR1smHC(# ze{w7yc<}ir$BzJGcp8sT>Td-S#A$Y~r_8*$V&1ynX3O;oo5cclV?OXxNZOhlBi>bs zgsJdkdE58`q|*3IdYdHPkZ0J8^PBvZ_wu4VuB0U-SdA2g3%;@>`av!BN{p6aweY@S z#+F(_t#q=+s-6f8{4wjh`~qZV#QUrHB0)5oivT|W5R993wV|q0He;*PlZt;o{PGIv z=CjsCaeYMBK?zW{cwLU!;LJXwpgK^8>3M$Wg#oLS&VoP6;fb; zmcz}@&DDRWStTT4q2?5okM$_R@SN#5_!tp*2((j`_d>o;;fZ<9Dk8KPxubtY>yUt@ zW06Iq@p!Mc9Ycm^Aj~CU^oa4Ma#$<1e?pTO)10~p&3>WGdLA<*v6dhD1;UC&HOXRY zVbG34n&%Re*)EgL0!0CVCQFmkYO*2ei?)p6XzAxt)Z>)v@e9z;`L!@l0kjaaAbD_Z z{_~>+x(=FH96AJ;)uc%HtncoYrCLHu2y&p_)ullDXy~fr8K5zk>LDO#95@v)QG{@X zMI1E|PofsuOVsx=w*XZ)F0?ugi7Kv;iZOF^g0oH1Y&tNoj<6=gR|FyyrKUNWC!!92 ztSu=t=%G+1qFXLG1RPQ_w4Nj`^nUm@J2pCk^dZWXGZ+G9?Z4f{qn5SLLe*RYL$@y0 z))wh}Nm~X1kON>_U+?_%82(t&ozVA5XdTP-W-vT?@?!6JSuNVWxd{gJT1mI;oT=!X ziL0Tvv>kq1J49My##NnH#G*tB|CEEXH z-1Ug_oro_%v~k}Z#>&@r-!J3qZ^XNb^nP-jYADe44*fP=_0@N~`^DYt$$~t*S z+nPtz$Fk%s+MXK6AQmmN_7yKm0pxW1|u5^8j)%$V-r=H@UJUR4`} z{7z_6Yxhrl=f|JP0Xtc+ZMikO%IXnD^Al8bhhG>{tH^3`CqB; z+47$p`iZoscLvN@v3vTzXeq?6HlSJ-uC)Hq^(NTf*Qke2o?6}aZsN4oeB9NLxH4!t z)bJU(09~TqiC#9o&NdXX;g{dcPG0X}vk3K^?3r82G zr;MtOTW}nEUMS=xUD8e+-U_@)khrfSw+uDQT zu#k0DxL$;m6bcTDNPWFhJ?|YD6yzC@Q$weu>B{J!_D0s5*H*8U>y{Mua?{&?Gc!{l z^7m83_3m6FwI9!#E(xLEZ#fZB^`&7Cl(EUHnc}q*p$T1N=2*SPULEZLQR91raWA*z zWUaqKtd`lUjElK+dT{vy$#6vbc(JFz@UmTT`7AMbdDr5@TT5Cc0AyFBsUCWsN<~oV z7DQpqDK@e~OyLg&iNdr$yIr+~H5RMkWsTO6&^nHN^7*Xk76{zHzyqCZCg>@Zw4z(E z0FVH(h-Vhq9_31@l-Y&GhP5M+rPl#ZUDFdvc|T6&=BMb4yK3qTmr`yX;__d9$93y) z#8{8*-I^QYLD3#WD|~|9S7SLJNIKH|c|X5ptzkdponJE6r-aW$@Ff#2--|FGtgB-4A!c^aIa9ju>dAl;G04Za(+{VjB1R}Wig79dvzob`xR>bPl zT3fMiZkho2H?Wa^FsGfTnMJFba;d2RDbrsWbNG4j>md_+a>}A73YngeY}gD1+$wAb zLmq_QbWU)H5`lK*6tf>j`C7{fvV}rKkyl(TnjU)7Lh^urZu+PnA&02r#adI7c>*KKsG9f z9Z}*gk4m41;X;uJ1phiEjTN*O6!0M-)R5jL?*GiW4KxFGJc!LKr6UA7fTfT90TM9X zLKMC~N0<%NExg(odNyTG2yRdCTq-QCYy#^f%!{YPl_aF>&`<<(8ZI?D_}X$`iSv+V zuU4)U*Xn& zeO$+feafmuIi;OD)pSdEFBz~e;gp%rU%pZs1esWYee8PPT-Qm3FHefI1`5G5PezVqMIy%Uh zp3440O%_pxB96yz_!@{i7u-Gi2K1T6j#*|sf6b%C@z2S`EdiJ8Hq3aOS+Ck9;+%a> za%U2B0~;+eHtI7Ihz;xds5j@iArn(sfcQ8Q?t(racUG7jh^5~WIbgX5vHQt!^+Bhj zJGDagJJzfZV($o;EUvSJH70lk8qQW+d<6wL^mdpL@yu1)QQJxF4PlU+&j!` zBKq_cWz*APdH|r`3^4?E?+ouw5V0Bpp@dpk+wvEOz|IWqbk53RD!Y4!?j#&;t;^U= z8*SEr1$Km^e2VLbnPnEOh+G>z9dh7!b-xtdKYA_vEmd?`ygrxk#)WS&)G9EHWHV6F zK3>B#JV@^JLxt!52Q)+)l4yKz)efKa@)e&n7i?waWf|<^V`vG`AR^ z&5IBWEV<&D&cxu;+Hlfo7!KFE};y- z+&OlL&snvvg~5kvW+6t(S}nR8I;QdIApqnxm<%5v!-S2Q<~xeXyjBNpWuUT}X6F`q zYz2fu%2;K7dynB?mn#<+Ure!-zgE9( zDz$c=zt+-XTpG$sYB6V|=(manGH3`Q5bPiAKxghx;Q+)y1+U^94X>bF<&u1j*TWw+ zvGw|Ya~|S}oWW3^;o1U*CuSBJj=DTNb^1Sa7TT5%F1+YBevXl<*NERT45m*n%s-F} zet2rdkK=$xI5x6S>L#SEMDT|SUxzr7dm+9Ben{UG|38-U7`~n^<9@KT9=WPw>&Yu{)CEVY! zze#G>@d>CaVeKb}yCa*2yQJN_Wuo6&Y@tuiSGO8*KkHId(yn#A?d(>;dzZAbZpM|l zem63BrBQey+z&lFMs)GgC0}5s3aLAcC8HKiXGk~Z@ua0S*x1n{Zf_rSZ?>`a&{PwX zCgg_U-GmiI&{y9Nz&Jlf(}e5}gW?3WK^R-yiml_i@Xmj3Y|H}7<26)+xFA>j*if?s z)_%6q+OC=8oG6KbLG8r-+v8My`a*1jnOKjFQ3YAB>A-`1#@k@WY)!s~@`b_C{&79W z;3_+!fc|jS^>;$rsYhGXByxwAZ`ROYBZlOs$`mr(DmhHfXFtc}_+7W&c>H2-^2gPKx=lYQyv5LQVptiu^6uP@kQu%x31lCM4HFydMCQpMi2&t# zZq!eXWZB?^f9A!G+Jg%=arX&nU04)i=r%E~y}q7b6D*JI$I0oP>INAL=&JolInP)JQfy9yvCL%-6RsfL`;X(bSnkIhTG@OOqz4#v-KuUMlTbl)I??fQ#V^2QEFY2s7Y#SzO`@Iq8+1_^b`**fA z(0WL>sbibwpspLyQ`8FIfJcui+T-1Y^zvj%r~=+4X!wdauLqApUHyC$yq`3ux@1WP z5J{Ekg`vB^{&0^Cbbj;{iYKib zZ@UK6=uzxu<$!GFOP{RD5y`7TdqAENF6?(&_{0sq_$2-j04_fi-cP@3E8EAlkY(D2 zNM-gy%5Z7w4m79CXG?3ci4Im6)GH{qkO6-$f1z7mD%^+fQO7x1L#}0=y-B3l_VUmHnX^+&S_6% z)wdYIw9P`iOp@J#(E8(buR1v%bFk+Je|;QS6f6u-Rn3_`{qSTu+hd_j;q%9|%~#DP z(y=pzXN#XRjYe?BJ!%pp2kh9Y%3sU)N9t5FX{;Xyv=iFsG}-(x{+QpH5$F`GUp1P5 z8R3r~jN$+4aQRo>zhz`Qx#;o=u1o4dotNmR*h~e&k7pWB^POw{q&lgBd(>y5e%B51 z_(i%rMb9B?$64p zc`cgfoy~XDfU7MHX&guN{C<^TSn|AeH}V>ICWbIDJEURvKcxI8-zV|2UeEN2+>(PoXl%>< z+1~oNx-OY4RE%n;q~qD`Q&5)Bp2x-P_c|>*VOP(;Zkv~cXUdvE5;Is09XCImyzR2W z29{F%$-&iN^u6!9R`X%gJ-KzWuLA#HylnZ$tqD2tem@>E%S^o-)O0er_&MFt?9THz z9)PRA=;$a~EuWa8(?|r?cYF@o*QQsaN5WEIDi41>y}#+?a-?7NQ-}>Qek|rbaK6oQ z*F5p?-Sh43Smf8PwmTJU+nzpl?1*g1>2>tiE59Wg3PuiGdO2YAbi4Um!k>v+tk=7C zkq~>QOS1xB)IQwoRQ?csp!e~z|F%hR?1N0-*u^t!Ldx;8=S~}*u4tK-6bmvj6OnT~ za5(49x*X5@`!u!R3jJb(-<2Iv-0620;^!(wU8cb`oce z=FGr9IkfEAIEU^_(xxJiUR;S{vyBslEYy%zo+Tf zu+bIn?7Wj_uUOlqJ5m2V?9mza9H>p{276gl(G{M0w7&qR_2t-cl30K8DJ*fw{-8>i zftWgDoX42!^B-b3_H`c3!@7YE`X?vJ8I7)(9OWh{zTi*cFfw8b zAa_~qol7r6e4Fs)et!*{G+sT}4Kvq#m!i-$sN>*W!5f}$LM`FPoMVn4fkVbSzc>C_ z!QcM;n{&CpsBhofN}i1er>EAwH;XMgF1+#uer*cFdwJoLiVz}xu?cn(B64>e;j2H) zfhL^MjEKTlUsS+MYHlngl9M>`t3YS`=K;Ngv zT=U-ZFPNY1s}vALqK59~;~rhyL8gJ@72aYUZ)k+p6RE$`8QHhS$~Oc&5xFrr(QqNR z6gHp(6>EsCgx?MUhv$0dz zVykU$Jd<~lc=v@kUz>69O?W`)xq4KDe^J9D<%N;MH+qBmmQvG+`Gr+RJTSYqj~=dL z_a*Cyv6~ahVUri5jM-k3dJwfhDL3%kPZVeDl{2rs4K{Q5iI_MG>P1SXQ&&};{AUkTia^-$iusLt|= z^r$yQdeVya!or&qFs-LmOU2-RNTr?_k7B3Kt_LnV1ZMV5$1dCt;#0Xpf;A$gp2)@! z`Fz-ZlVwVaG!Pc1kgZG%T+u|@I?uqevbtBj4V1AlM++;E;M=@y$P3&(H zhhL2)v?uQx3Cay^5;1RUusBmYF+1Q=2uq;7y*qBnx3IudNEx1AY+<{IDYmEG52$+i z%)tOy3MJr0)j2vR!{+~ z3RY`U*cwFDmA&Ip7=OmQOznpTw~L+DCm1tyQgclWH|$))zX5MJtjDwvT_{7rGEqhC zGTd{cQv4fjx=f=4CLn%M3m74$NXN)SdOxryhadZOKd|ivC1>jON+WdVhN+7|Fj=2< z4@ZkEz%G~kT3YQ{l!^q6FI9I)yt13}RisuBKi%~hrdd!&EGCkwZ`9=E=+W!Zzpy_rEeszz5L0+&FrHVuVlZJ`}D}xf(;|?IpCIJBqPK2J^eUduTct>x{{&ZX)@rm zZMWWD){O{vODv`^WViTL=S5wMU=nc`mp62bSCm^D3%J4|$tgwt8@W9VPkwTw84ICC z(?BF-v?*`$EH?o=5C9MKO=KgJxeH_5tGa?~EV?a3ERksf+{&p(QKV;a@1|&y9fFf0c<1 za(Ut(eK7=hWNo8;E$ya(F^3K(#g>z|PjR}4R>7jxBt*$Z=>sm+ya1|$Bhn_=9Wx4X zjVGeG+uTZq2V;X>F)@QagXZTI1Q>~o57lLAWUtQ-mh4Ja_t|VLlM5OKDOl*a7Tlo}DmKFd8PX@l87y<- z&Fgqlhl%-KCx)kAaE8m)QJhO^cs?;xCbF* zaF#LJYhiKgxsu~7AAm@hn@qd|Iw&xXNVl&^CiXG}qD7Uh=DB#&3rspwfDXr-T6jQX zZAePU2MciDQlFvys9#Y*hPj>7oG|Xu?kKk(0rR!3SoUb_t!45g$iJ51$nK4Z&R1<( zYL3~kJgZWpChkd+Z^>pz(}Ge?OntR~@Iq?B1`*vzUXYXUTbhZVv#{{Sz}hWKhsm21 zFJ{QPr_jxJck{WcE@mRpKKm;V_pPMNUtLxUcz+#gyX^HIXKK;<41jy!dvQxrrp#)R zy0l_Gj|be2nbu&3y?k1x1%wGw#WgQzLm$efD-8p)5tcQ_ak}%LiJlH6wPBRzdvfLP z8KLyO^|k2j=cI>9A=NuQbK{K3n(50zy>zuHwM1Js|?FOv6#e)e0JtotkcNKL>;I+DldJvnE8p^-Lk%8k zQ7Ie21-SB@{G8GK7QNFv%1NrV7c(VF&ft$~{i3D#`^Vo}Ui}x`0PR<~keU&+8J`cM z76%qW>YXFMOkdAxyYpu`cHu*{y7MX(hvLC@V#NEh74Q0hXeWSvZE*-+?dSXXI=^yY zCvwQ;YPzXlv+iI(w9okg^*n^SO5lt0jT7pTtsI3McID(QA7t{U#Vj$`8f;prv{Aon zAfIHyFXOQLxo$7LrR1F=2a^3=QWCeOAP_)y{$1pucGnu4M9Xmio_v-D5`>b|NC(p6>k<(DZE~k24b)^5 za_$g}f`=hcnHKfL2t}($iPPw5#I(ZjJ560*w#|mxu*MfzssO*qHooY1kqSE96-S}H z3mFW^Fl@KH{$O|?)DPq+k-$`lo?Smbu*~D0YGn4HSJ|56A(@HB$GoXd%B)^9wKnav z&x;wwC2jDBNN8Frb?5f*m2^Tujy?6OI%QuUmC zpEaF^#-Q+0siZmxYC^O6%FfKEpB%ZtmzopzZr|#kU+`8f5n`Ta<6 z|Bf+`>AkMR`QE{_1QbO|4mMLlPRe#7~pZ?pqEp-;>xrDcNLrZDaj+R z4|%+_B83rQ;5gvGM8Bvxr*Fs9k9*(HmuPUPtV4Fx$e*|fGtH|4HNMsoVnKoQQI#vkXIG`uY%P(4oo;HHRrYYevtQ z@~UL@ZHe*|Iy=6%%q?q%{b<2VvU829c+5F{s~?8(@51B)Fng}~_W8hrioq{fEQp9} zLXK!76=(;TOyE)3+T;dSFpksVGNQcvi={f#PP7i2xhlFUQ6yaBmFX+uJ7&(eiIZHcG_MY8S-Oif-GU0PB6u7qdqWXaTo z=cinl87Z)NCQ=nX`eezNI9XlCxbarGp?!GA{H3>;OEwt6ukNZC$2}7;1KK^D1EAdJ z(yE5dM{v0n+6FIz;Ws`vjbdos4-RqHg72|`y^q5;W|ChRS@!urD@&tFZ=|HZA)q%` zKU1-iU!F|&d!M*n-Q{Zp44$zGEexO;S(b=k=v?gk@s@mcd0aI4}z?PSB&(>heyF8KwzYjJw zUfmD^VM`tiGFdAH$h$F{e3!)69=HRJ+}#PQ$%gh0y9Tk>7OLwvG<<=vX%jD@vrncO zu7h6viwQkn%6n+A%8`ZC7QtntXl)VbebQiyU?{J*gn=x&&!%}9GG(bVUC~!8CwIdF z7Z2YR^K==vW5;0%?VLJ_qVs-uLr)d(7AZ&*CIrbB#@B6qZFIfo3az5gjVcuMo(D!S z39=UQ7Z(ucdq&U7NmIaqYiIa-Q*4&=MkOU*Aj6oaP>L&o*-y5iC5}amo4WlBr3N%h=& ze?;8!hAnqgIH+QCK^ZPSbsEz<0qz7r1FMOE*=fZ&KA|H8?!hwDogr6(YgG}|+yZVM8HQ#hdIF|!Z>G4RIpUeB7F{q$`$!IK|VcO8|vnptbWnPjiII8ZsO z>d#|XB=v25{Bw3zjWwaD;kW*dFb8}gQophd2yF&0VxT|TN+KSDgFE6B#|Y#~`L6r& zs=N}KV7nQBaDiW3O9C6>q`SQ1RH}D6_87p?51((aFf5f`Wm)bGalzKx2Tu%c zU3lizdTQy^12&%gkHxsuQXsSkxdHYgfPQlHy`rOGh@1)-k4nz#vo*x`u!5CfUdA## zSz5|rJZiI8;>uZX&f4iM^>Z&Ib|K5l^MPIJ?LPno#m8UtDG}sScTRX&kKr-riP=tL zK8{YV5i!h&FB#?wq#tqZ*1ih{Muu6>QWV=JY3zvpx$Z)*i=VcPf~j9bgB6VqDHPZ# z6P}wCpHk0`3tRyykq3D;)Qt4{ zM{LkF?fdhHgr};?Hl%FCY&S8_6SxXMR_ozZbIx??lAA1{yI-Ut2n8N`2@@_*2R7BSfWy3%7*4-N|>EOZ;_&!scbUF}ta z)$iGUeUjP%(`|&Y`y39HMUj=`h{BZ$C~Fy)<7FYIJHw>#rM`^IzSFKfzFgVCL!6T$ zA^|mX;Fn)nJ|R0yifRr(LUA@K=BRY~321x1oV|C6D66zA2mdItBYt$=Y=~jCAQ|Eb zu~_IIA(xk^hW0;tOC(0LsUMHU(l^pS?*o#2l!mh=`mDV_RdrXO6I6Nv$U4Tp>WPhx zdTkU~P0zR@8v{$5v8yd$j)5Y{FGfjzjO=7(`Bm0HSyE5i@Uc z{A;}Pjk6L?xCcOEq`@E{Bn-EhQ#0(wUM{~u8Xc1A%XRSqwMk_bnHsfJF+4AG1u>d- zM)4ccp(l)!Z}8x}eCN*>K?9^3-kGOXA-vV`ta_eo>zab}f=9}dq}M)cOy~2^#$r*O z>6T+3`yWdA&vP2(1iklxTJE4)#7xvy2B6JU+rG!no;D*nVTKgDfDLCycNzU^GmhOk{8dMWYD5#G=-Fn`G=xQ(oKEcx#KehTf zp7CL6*`z<_M|gyThN74@#(G{q2xkwC*km<=&cSL$Xov{87P8xU9Dg7OE{ zYEUd}fLWzA6LQ){Cq_zd2HQUwg-)>{6%FSummmb%`5Y`Aye&kZ8BP;^dCZ0g)x}3o z4l(R~Q%w$#5VoPU(2?bVBk}|`cx;#`)Y#9SZffvs^>ZfLe7{l*uAe_-vaF)S#$z_e z#)DD5`Y~g8SR*^Cu4>3d&pS zQ1xy__!i}}%v>Fc@9p%31Lv==jtq|B}1D( z#H4)+$orINagQk8HPi@o8%VwV^*Yaud7qG7c*> zG;>9Z!=^fk&iBiC8Go2}uYgkViTRp%)2FQIsMe zUAhDaBq2dcD52Tt0s;x47pVzVs)FE-@BYsD-o3xGf1LNcW1KPe*z3=lnKN^(HRoFE zUUS~_x~~hq<`9?+t9lc0g#%EU+C3msa4qM-UJ=a}u>HJ_-&?S4cz#AGXci%eOEwy@ z__CMUsPK)J$nSs4yUpdc)j@xS*JqYgK8K#fV*Z(f}WM}Y99bNrh{(ma`Z`MnHlA-g&uOa_1fB&{; z>)&Ne|Lbx7f4RhStsi!!K((g&7wp$=AEmZWyw)q{z9r@Z4`sYzC_%v>i@Q)marQi6B}ug=3m%(-;Gx03zZt1+ zw}CxqCAE|?3mk$f@}t?F(iWce#0Q$&l`CpeM0yn;POQ*eD=biYNP9vaaqbioqOw5! zLuo#z!hA$d#n2eoS$W>C++259ESlIY{E|%ob9q(og5ARYG4;R)3|4IQRewXFl3kp- zdH_4PiV)L0d|ww8r*oS?=Vc&YVvT-kxBTK5Zp{{$JfD8$&Qt&87fyrwsp*T!7KEZk zQ15fjuo-CFg7{cA=$1Usi|ZTLm39wa!7Qq2Y1CUo3w~(4zo>4D9m43l{u2C*i0|>C zQQgn>{pk1=N8fSR)C0j8%$oWM1$iI|VX9!lW?S@)#Eu3wjn7a=7ncg0Zo*q>ABI81 z#%}KR+M{MiCMlf;NUj1qW{KN04i|4xAQl5hw*(+w&jsq!|J?6ecvek#-Wip^B2Sk`NIM7DSx(W^m3ePttQc4|=5-4lh2Ql&Vh^x_}1Aq_ z;=tS=+b?!cw>>4P)6a zu5p}xv3vXw4_Ur))M9$gW_C%|peH@}%$(;Uv|dta&B4!=n!g7)z+2`;gHKW6+z8D{xahBFbU@8{eWc` zDdDrBjR|@{9L^SKb{9CYD7JaJuW@69$ZdNxujMo3yAgDSO|KXX3gtj;W}l4Jbj7E@ zq|dv(TP{Al!9OkC16olhohc&7;3EB2*a(D}?(d6OR8|VRP6L1cqmh00%x(<1nl2?( zl<}p0AhI>o@beB|W7ENT4O5E;2c2#n?nQYCTpi+xicQ5HWpDd$GtpW!e`C3MK6}BJ zHWh$y_0Q6sEPSQZATRFbdlvW<>Q4S3Mck2ug`(5jzmvCygAEAmD2>nU5Ai@vNZC9rOXSW-1# zV}cQ-y-XVdv2*o6_(jI_xmH@`At`2NO$IM0tGHrEgV}MD(+tM?Z+5&-5(Y->C@*kDb_9ZD}PE{JFEwW;mPkU*7D!O9W=@5$Tb{po3 z$Rm|=EwlZJNI>eKG0qZmekZJIe6eODqtmBH&E>`IF~0GNq{UrsP}V5Eks3Rk&5m6>QemgU(9!o8u^ma9H0G4Mc|o}S@3>2?^L&75{QFx3 ze88~$XSvh>PTF2e_z*8S>6*~CgSyIsA`l$^+ln3Q6?#&5mwCp2$uY40>GeKq*RNK# zeQh_H>Jq`}5n>N=&0Pkb+N@-`yzGOW#$@?UO5)0);(BCTo`INh80AI_|Ch#Lv%#q& z(U%6e*|G{gw<@PpX+9iW5t(6h?_qEm-=1CklAhFxVZR=ELu~pHU|KU>d942U|qoql< z9!hvaqZXt`=d2zXc4~Ih)Z;8#74IS7V~`rUuMK5mM{@1I!50%&dm;U{s^)d zfzhw$Eh@jK$gS+18Fr_WH{@$d6&2LV3G)13=R!U1iCc8G#OtxOWUL7pCI;Te(lUyi z0tzVopEF`GB?TA^?_%r*uZ65gw?F5^bMHEKY{r*K3+BE-pq^DA}EygF@kf8>SB>LlSq~%WQ{0yonzF&l8xKlBs_h1^n0Kzi;As)H^s!`8rJ< zYjb8d$Iu&<`)O0s0Xn=x;pL}2&rv!4{x-E50r|y&4?6y3S0AZ5>JCxIL^les ze?{}~{`F~_d|RP#GpIh4me;jo+JV?}PWlt5PYI+Bza3&OHoP7@Z5>kcc_S>uVI;_X z``nANnw6VQ3Ip3vuJ~y#4L6*psm zfsKM}DEqr7mGT$VS62P3Q0YO3+mz0Q0OAd5Q?ul0tvX-U^W7h^T1J&Ct-m-t2h=Zc ziK7#VU?tG|KJ1+Rlhba}0mjy;2i zZCkB-Up_a7%frKElDASmCNSf+^YsY8zDb+FLm2<}TqqE_8oxK%yiwcyX#;-+lRn1S zu20@zhU>gYm)LV?M6$ogErG@9#&GoMErVKsMS-uTg{m?>#q=J_xy?3ZJ_7PJ0>~qMvt{qvEP_ zEsQ!1ZQfX2?pQ^6&22fgH)wG2uVVHVKw(mzU(6&!2by73qO0w{IBaC%R?+x0;e09| zV9U3!ol)$2fyJO^17euDqO@Gu{!!TNI@@9mab4sSPB>HcS}vuxqyi^N9#hF9;Jd@p5{ zG|_bsZq3)XEmla&jpzYLVIW^?Yxl@^oTu~*h1?cF@I6v+?(wkuO}*=wen_xTJaT`= z^gTEx^hx?^B#rOPM0|l0dFieVj28ue84=Lf-r~>)JK0cKzj3j8SrsN1eutF*K(%}j z6D}_Ny>+);X9aN|d64~*hykp1G#MthWouH~2Re?bBSxZ93;6qfM?C5)Df}vOQu%grszuvjj;g(4#qvnjd zy6!?mDF}-d3*)Ite)nV5m5@0NCX#tgU^J-d*(9}Kcq5>!Cv^879A9-9OJSmH%r7lK zKBEy&&p!H~KpH|Upb=Kh3J&f0p)AbH%c<=Tm`xQavrrR?dOa0Ih;D(r-N$Ra zS`KHwP1iX2?KQLu`{=xWs?W71ST)KAO6Up8+~}8t04z?;d&+ic$1Q)m2tuJcm{pvP zd2v1dxs0GJ2*!>yI$rA=Y9%}R=Kx04!fNT7r;_Xbtb%XmYPmRXA3#cIupp8DO!oTf z+Erw=Tq#pNR+v}m<_89w(*x3(cH>6wxeo+2O^#0^^3im4^+Y_vRvDeaN>s7LVOn## zefg)@AqmXuxpQlTu9{h`1Kso$GQFasoJN^o4MY8MPr7B!!KrT6B$!r$kQ9p?YCCsg ztZ~~a;OC2993@|51%vlk6OCqJMbBqPM$;zW&V&1dZd_M-8+6;*=I}x8fbq)O5)rd~ zcP~xw#RGaDTEo$^yu)m0_0NHdTt_>3hvE2mBVULu4RP^k*NP8??JzH36YbhK7{Nj*jOm~rp*`K&uZLYH54dF%`mn2Atue{lh;^|KIc)yPs#g|lQ31yK$~7! z=KJv)0+*z{frd$L&A?s1NhT7(lT8%-tK=n|fP=zrExgxwH}LLAG+)T^y?DJ#hetyn z_#Aa~x35qmtfrkV`o-Z=2d?A3&19>99i$>+oR(TO4Bc1zig~S5{S~YTfv`-mg)=J_ z#XH8{&UL1))c!FURSYJuU?jeW{|h@>Ro8<^^QY#rdm~1BSWrNBP&+0LDMnb=fv!MT z#IDAbgSH>zej$1HCWq zK*Hr_tRmG&naXT522yNC_^KH)FS0avJOew)hecRoLP6X&!|f@T|DmDP1F$YvABZ0?kuR{?$fGz*%_>06Q| zIg`h4y(atYC7xXYs@o_lS7UH>(Z(>rS+;yH4K<*)IvYBm%Ia40Lx(nFtr7_>`hpE~ zon<@Q_J`xLxVkYU3Yp3mG*JPxer;6da}lvKcUMDMo42G*JrkQla*WC_o`rV0G$-xr zpP3PM8wzcTClQ5pj7so=weoy=TIqP6Po$yhK03HPf*+yi8XZWnGc zzT&Mpi;B`GtxbVpfP_#P&F$|5i!b|6bgqZ(^#?6|g9I<>FO;>I*F46?SONc0qr{LT zRyM$^RZ--6S_b_bcR8KgpiRL?LG)-X4oi>+MArQTa^8lm9hJv`G+dOAG!dA+y(Ac zlDdvU6YH^#!6n4(pli>ra5#<bWT1fz8hHByJltcBQZ-FhtV zjlOTyqEF8VCs=-aSy$?NsE*t1bg)v1B)BIKL#b=Bj>JUCoQ>9w9Pz{(sW1wJ@K^&N z6{@{qe6yx7v2$1cMk=&3$F&v$nScBTl9qLswwE+g#ZSz$Dufm0h{B?C&3)jMlExwk zhaxHcbeFBWf4~SHk97F1_-PxJJQA5(@0QtGo#;kTpxa)+v9?@?#_->bwGk=B9CF;K zaYMc=|0n5^hK2=xhDY8D%#oo`<2kly%}$~^=7;w{jV|1sE^J*MXHm`W%ai!XsaeR6 zlV3Q~Z2j?t&9^t#8t==SIyi)si#zBktBFxc;vn1kJHI%}l!1q-Ze&Jg20rI~pK5?g z{a~E7&Q9tpj;?Ar3JpaQs_DJ@<`d@kwjGj3o?kh#Bt@f?ri>pg+rHLgg}THDar6i? z&x3-LZMBo>#u5YV|J2Q99HFRGvYu+h?_j_)7KJ0ni)qyp`N4JDnZ{em1*~s3JB_A& z2C+MMPABT%u>31H+Lr`$JWY<*)`kPH*md?)_jJSOE61vFv_M-^FEo0Cge2GqXfucc zpOtx`+}tJ6-LT54u6}CILD6ONF3xZW`X0r}p9gl4jQKFi0sX+*w4k=Z6oLS-#zEBDzn}MPoyb? ztnuDdQLWZ(o26v6*XdYLxcSdayx;gI8L6aGGq&^kf~o9?C2a9U`5S1kb(`rIHn792v{i9&#^lJrBmd+p!|@iNq%kegJLT^RYBBmC?g&i>GnC4DCK z#3T!pvy5Vg#=eZx3n@~u61~ob)YfePp={Av0C9rg&3;3j2Y|u?(nv3jsChNZWr`Q> zJ~DYZv!v;<;@!SME9SbeQ*IuI#{2&d>7LB=0td*7-3fk3jferDtgtnhzS@-@^_$`1 zAKM?DwT1fbh%lOwg2`|+M6c*uN2bt?gj1l(8FAe||U%v2EaHH(-)!mZ}HM==q zHWIxBYH~%uZR_u=SuXiWGhWX#6`t)rgw<(u+ywbdGzPhM{?+!m!rcNu{51MjH9+aR zxkeJd6tjz$ptReQUzSV}S`LZ+gtRHdU?ODEHC*X|vGpL010;0(?>hQuhL4AfdmUVC zZsJ6YPnYLLcJ0>#U3H}`{$jf_n7hv0H_`Pfm2`b*nV!%~hx7>fjhY3qvP}60V%D+l zIhy)yfi)uW8~Ng*JoGYD=IpZFX%i+%4~)X2?6#9KVCU?2q~Fk3!(=rSdsW?hbB3{% zV`rBR#r@SB5^UZyPlT7uekiujJ5$|M)f&y)lgMqKKE1lpYrk)BcadJ9;?OB7UbeWg zSYuLJ|I_qy$m630t*Pa7qU54T)BUFN`TAe8+hj!;%3dURzGpO3!U*HzBbHH&s+>wGShQNMtxOKe; zK8Rhq?>xK}kXNOAA%BD(jbc~V``oe3O>vL_jV*sBeN20q zf_;X}7aXdUlik-5wwC6ZoR@CMEio%eYvvW^)QH4tz z&!(#ge$4lo!8x-qJ;FvttjtgZ)+rtsfbW{)ApmT_p~T?YX%=l;`l!?5sygz;I&1-K zhfG_)s}aRM6w@kl6~<+%uw|8Y^u&|F7~O76&qh@ zg>>{wQV42F;2LU~o1Qx?Vuj_@1DIcT5J3r|2M7{*Y2=8rep`V<;?2dc&ui?E?ito; z=#%B$v)!_PMK!J-Jg>HMu}eP~5RuzFtRGQwNT0i;)5LM!W^)=@Bk%h6&x?Q}-0;tP z=if^dR{VbAkG~%(N*fQM(NlJj7SKDLst*RWZs~cJ-~9N6fZAR6!3JjO{iawz{DWd4 zY8+OGPW8YR_4{pC@2FX_2q3edek9dIm)|DsB zZ#)9jBq)7Me6$CC+cQ#B9T6;*OAA1%V+^c^Flq(G(U)4=vxB71{!}__x2QSZUSno9 zqM~KxPr~9dr+xF@6o&LE_rx`%vR#KdQ`?;wE)O%6+ubU`ri4;DowCg{K+wuB9P=p69Y>YVQWxx&|6kdu|SIBo@D?$kPJkY~IN8FF|p##+_qZ z`u0z!fDIbl+-mYcE>CQR$!SkPa3rp-eUYT{or)>0i6D4?^d0Vi(PDT_kGJpL?6>tK z92^Jj(;MsiJ;Tq}FSuOpEx6q;>Sk@trRH6fIVxUMH73Sn+N}bjKg#ubDk03@=x7Qi ztr_F4@0~HUC0j%hP{K#VuS8#vf4hwYG{MB2+Q%N%E>_i;E-i6QoOD+jj3u2tqp4`V zuJ@2XmsA95a2|F-ngo=X zNUE^}JNl>Fj8iDG##RmFIFftW*H0+bZz)J(?M&qrq!)Tz)VF`{!`HOajk0=om_vsV z7?&W|61tuI2YIiUx|yy-VK1s%&Sl@EUen?$4TeT5otadIlw&wjxfX(yRyc4Jw|e>z zeY?#qV%)KL=!&(qMmCaM+D4Ypd@)Z0F1I3u;3j zc6RsbV~U=lnQnfoU!?Gry~Rv9<%2VY#dz!ni1$V}H;?9Ko7zl!;UeH#5u=)_K4hPH zyRxrCC{9m#sD~{R_Qd-5FuevSS4c7T7naH_s-^j5ZK~&k#1ewI1I6vduwr7xE}Dt; z`jyra18KIVe;2aMV1za~b%i9xx4H2avWd_w>0>7u9DKm@$Mcey5>!IbH_&~7#<%5U zXwGz^m!Y;~e2;#X+E(}xMhNb0Ej zd0*xbDa2qkn5+%a*>(~6aC%DBqBhphjiA9BvU3fv^Z~WmQLNX*(<-)Ud?9Z>tNbV&x^e0)?ElX4Wb^7WRz@l9dzL4yEuwQ?W~Fppd1e@Fj=Z(Po>K&o18 zB&~^QYTFMGm$8oN{w&2G%OCGMVlrSIfChsuUfc!Vm*}s%)T$iErU#zp)_SUpuc{96 z!1a%Wg`z^@c-v!zsV96Bmp%rw7umnWG``bD!wjvea?o7DI5Gi~acQBaV}ju!&D9%0 zYYizocI~;-#0)r~js{~qsDSir2ag|~S~PhO+0T zgoh`WPh*dwzPx|Pp!#g=q2z$M-#lIvJK!Jbi9Lj!9&@M^p}}|l)VQ2qSc`h}cUe&c zfiptKaz0LE4C@6~;JEf~h}yY=lY2YvcTiwjPUmi4Oq-F-YGP0hEm#f_UJxUI*YaQ2 zM@AyXQMkf%eJfds=z5~&It3wJ1oJ!nKsB|8+O1bvW9zAI-K~SLV<^U)#Ah^>kfydb zd|vnL5`_t_U;YRF@#D7G=Y~0h@y~fbrQTHJBj1el!MeawmRcBRbUx^Xe3v2Jdv*fWM@3+CU#;bf!4T1)?IUKSCIjg*V1z zKe`fCWpmf|OAR&aFrk1tsK^H~N|MdG0Jx!QH;?0n!ji#N#N`@0I9pn`B6EiGBIx7! z2lOZLNWz+!cUO7?Mcj$wWGO0#3)nLW(y_TmL0DK{k376-hUtgfrAOn2^E^YE! za`GHNPwc-)Th1F?w()GD>PPHd7?n+uXS`(k`t93K9J~m#6OV0^{xojU@1-@?kM@&ISMgz~tl=w&H2 z^kjm^7iNv#z+rOD52NKCAfe#UYNYeW8p&Rf(~cy1d=J(@D!Y+9&H8xq!!DG>@pSUm~z%yjPSUVIg zooAqt~cJ5R4UExymn1afFOJTzc(}HZev9@{PaH1Mv|vzs6gVXekxy zWF1rKKp`IB4{gM9iBNT$S*MKqh;v5!+YvYGB!_A}F{co2{(S>fgr3>hY{MjqthnxP z)RQcN1gx9dKLkGWf=)UNI*(I|G%K~5L>OkNN{=Qvx97I9A z@e|u%^v@^FxJ?*e&Kp)QcE3p3#hev-_SEqO!+w`@(4IhW!x{Ofdo+|OILslpV}vrI zWo^w&R~|4gn6dZ8cr^Z%OJ{RqLEo~PF@8m?F4`UO@0w6bw9ShZng%y(-+l1*nDL~b6JtcA}rkrLBgCuz$_Y5ObuD@CCm zN(!?}duMOE-~7Is|BE9~V?kCu9lF0;eBFXv^d*=f(AK|IKdx}hXy#PBEi2>;wQSKM z_*srA$)D<9+-BIf>J+pA-+NrXe4KDixvJctB%!1*#Cs1Ok1C&{uPbl2CLskQjpe}b zj_Jg{M&aK9K;Bql?sKvLLEn~FTVAhx#ZC^gqR=~{Bj;0r{ZWJtJQK^!Q(BhX7;i8? z?fdn(Q%ih}yVP@mUMcI5i8MZq+*PP={IXPdRm77Xk9QsqVxReA#P+}#G^+_SS#Q?=zVsh1_=*Xwq_|zDV`X@>e|ELA5&+$ zaR&&5mJDKD4wJXUgVVZ>I4r+IV<5s6wi&lyyjfDbo+GlevsQ4+==%kRu@$3Avdnl} zJ^^o3=^vl2!Ipu1zPfh)#YDqnqTm_FGy{RbN%>uX^I-LW>42WWs*L#hoKdmoyH=o7 zqqJW@Nm+l5a%s&fP-MKrK2dR4aIATr*K=T0m&V}I@yy5i3oG&R-e{vwDk~a{*CT?eR;T;5a_drbs4!8R&NfhyI=+dN&o0YLx z>(JPs8a(#px0yx6E3wJy35>(R*yOr~3T*pmS-AKw4rV7*xrwlM&BkS46v~1yO}EAm zeg8Ikuputj zjyf*mik``X`*;S~1+1#=K_O=wYwk+|<&&Q~6D8wcc(2&Gmb}!tZ;{p*Sn5RYrciO| z`MF8F@%rl8fd+G}w$G3r-%Q{^sdwbERQt(v{!i{d;)e}#$1D3`>pVQHvRYGrHE^)N$toJUZIgsFYX zxJ`JKoy?nfda=yP`8d`>lUfa(8IRyRC8tq!S#C$yYK@Oz*iDuE45E4Y0#DZ3ZMU7> zyVK;p073XGCxe^5X-MLV3oY8BGC!+XA6jbn9{UmF{Gq9A`e!cU_1aS;-ESNSRL-3= zMTQvixaNDzSQEQP+cThT=RN`U_{~F4vyQ{}imk`vJ}**@d)#_&Azq)X?;5PX`D+rSUxCtg!_I+l9z@x{ zsFCPB181zm+Yx~Q8|$qsaWt|+8IEu0d{o|mpQxLPg2?Q28zkQ>8wN0^!rv!Wh||H% zr=vGt*;rr+Rp`^1zc^qISY6gsek{N#SC`uXy4-WKEjQE&S7y1`jdJrR&p{>m4I@o* zqS|z4?OcD#ft!WLk>WIAY`vLe z-CC+k@LuMF`e9#aef?--#_BTgXo1q4NCrhcX%q|s8B4?}s=H8zI>o#o+D2X-csT@O z;Sk0q^twTw@pP--v-SH7i(!Qazb@I;7b@%3jq|}lp*-a`+Y8~3+prt7Q-(DAIKxbhA?HlA~+2m%RHV@G$E6+98FX)bvk zE|;)nhXO2(^NzX=?iZzJDEG-pj8$G|0n6{tz7E@HHz$XXZ!` zZwJ%s{1jqz*L(Ow1)9ZE>QjNlP|q^aSWjlvplFI}+2)Jak54pi|I7Z=GXF!ftiik+ zu7F*{+^I&lO+LO$kB^j`FR{EX5X8U;5T1WdEL8glt?NCT(RJ&Cf;3wAc29wpDFpt; zk^uyj>Ok#2Dau%WC+54yS=5PS=okg$q!C(b;Mfw~f<09i|6s#yr>11{k6fKkK&fXT zFiLRfY47p+f!uE+p_BKSzc@_xYcm+>o2}uZuMflnGS(Nv|1dC|PAt_U3bA>?@b8Vw z!pbElsuP_9<}{5r_8DKG{zda?mg3cBx)U+HU$@pwKHc)GFB9@=RhZj8tlJunt5?q% z_Kv5EY$_Fto;jC2(9r5`xKka2aHs^Kc!FXF8ai|tpo^AA5?{%1qi)p1@L%^JH z#rjtcr76BFmv1MvjuHQ3pOnXQ}ym{i{1I7iN5L87R9?Bh;p13-dJ_5!559Z=_fxkH9$HS#OnO8r$ zi^BI2U*8-!#AOutNmlC9X%L%YQtIXnaUYYNI_{gfmyn{$Qco$YGb_N6Y784?JE%;G zUeT3bN(EmdL08sPJJqb^aL2Ylv(ud{G_W|nn?~H7bI+05u+ZBe_4SA9RDb`VB zo$2SrA}5`tu-eKCu3_6KJ*dFieQ+DZZ+u*!*4lDh)52u`YpJZOko}b}2}N6%O-dM1&=2 zlS@2&`??f84v}I%i=v;<-$oN9`zj>O8^0(Pbr-{eYK^d%n|Ub({RKjuU#FG0oSaWAaNW#;chJ6r=%_zkn$SHofTc5F9EnKlf1r zXSG&eP@ebj^E_f{A^s`JOg%Nu(Y?smdQuV!>S~}v17NxOE4H}BZDUdj|0$$+WJO`g zq7W?r#H#^%4}3fwM6GS~*+<-Y9ba=P z@6qdD97Q4~7c>U-+1L_3OjQ~u)P6mfKl#;4(-2XPp5TD>oq=xHMW;Z-F-5w!lcihK z?F4V&zbQx;)3&nKF1_x$Y`rZ~?vIv+<)hfkZQu12l(#O@;bkH&zgLr;O=EoGpBJs_ zR-fX9C3%!E;u}GhltF)Cgl#2l(pPKx(Qw;_)5l*NwlQXe@R$1E4cxQ1M8B+N<8$i* zB@$IriL4LOrGrWJ7(IvY1K2^?17mPK7D6{I91fjjh#3-kwmh2RoOQ`{5ecyX$ zD@bl#toxdDs_?n9tYlng8zqPW@+x`b#W_dSoxER}hZUsJjYx#B6)sH=6sWx+wVecQ zpZIy>;WwD=27Got>5ajzd)yT*>*#o3|i47wW`b zoI{PhZy+z!t6`vEy5m9Ioiiq-xc)yu_f`~aeM<;dH^h;dQ8Z`%n1}ebTYwg`qYvFj zx;*Nbwhmx~LB~QtAyZ~zd0y4ehYTK}tsk{}{QLAKizpr7K6G>?H6isCM6Ej*3(=Dj zAijt4YG^!;;G9Ha6y>h^6*=)Nq&>$0=Wjdd_yGAkJwWW0ji7*(#~dSRE14h-=K;hj zkA-40p##d+odRW*#FTc*LfO2Q4g1CxcYe##{h?JLw__96X-VPj*kM_GSWwhe)nN1R zswp_b1|m)^(t;D?Cy)x9t%{F-Lm{eML$87eNPz92ctK6SY8Z~7-Ff5Ih6()xVAJFd z^nLm8-@{`SlGicVM#|}WyvFn=weR?kgSA_!p)N&BN~As+iYn`<1e0{rbGc~l_m|gy8VCVDG zoia9Zn%!)F0vucXBUi50J8xj#lm1b5C+!Z${MY~CRO}xj=#iZ_=uL~xPJf-Ee3LT4 zb9P^64 zbuJ|2K$|TTVx(~K$m>(GIdxyu`afm7$mPj;FkpN;r;2_7Y&853Q=v-+f#0%@*fV~+ zOW`Hk<8W_rv1zH_TTsJEWfp2hWc7mBfGeiGpWvHPy!Y%wISt&f2K1xn!R_i#F8YdQ)L*6 zo4e>!3cECw8jn;P1k#?xGd1Z4%=z{;R%Std}3TdX;{UG={L4JkapD8%5qrY;@L!!udt@;e4S!U z%OV>p8V|pj zP1g?9G@#qjT5tQAkw?+g=>0#bjHdVu`ezbK(5zh95fxZ472gWFRTeE9ZUOx0D@h@!Prb#XOY+2IyYf zZN0t%Yc{8LXZh!-DDD?%`p8|ndwxmZR^rIStcxXV&4f1BKyD3m_^--xB7b}pTM5WNN3)9Xy0Td6o&%cB9tAw@3K;D0Y;wTREpQ@($^C; zj@NS9=l{g%#FgMuorSSQ->_5WbacB}P=c7kkB-;BR6t_-9_2HjcO3^aZ!ZK?S`Q=Z zNBSgkPqMYOM0kCohu;e|)NA-y?~qDmmsJh;`)%ZSt9T*p-w6P>aWTT256wsI)j zq3e6wA*lt*UdNK|Gs&W-&26mm$v&g}wOeYi04RR`i{q&=xbS*!Gs`BF5>;Q>87)>k zRDO$1z-Bm3-5C`b^)OV#nnURwhUyA(3JSx89j%v7(YPOxev0jWLkr1yN{l9m#kXg>|}{^7J5QmKQHRf z`I;E62ZKMJFxIh4qFV<{i!Zuv9k5cf*0)T@;|n6lEY*`aueY3T?OYnHZ!PW%5V&2h zV+?&*TdaSU*QQc;7NP@x=riG3i!eRsSj2TMhf!@)MN>(s7+<_&Y;+_X;A3JN z{Snyk_`?vC8nZxHx?0;e0R1!~4ZN&roLI7&7@ZAJ5Rv@~sUpQ2>)`QdulfRJq3y!V z)D1;4;+Xl`YS3Oe`R9{g9HlSghA7%zXub?dh^Ik^1F|fKO33@`f@;p)BBk*jlunHy zXzU>l)1qt)-pvzFep&+sdADhX-4k_H3BD;e*tL^o7ZcOSD^cjXn{#xaGS~Zk2v8CQ zG4IwXS@qkNv*;2widx?JVc0hMdEfT$IEA3xWl^sPrC< zfgo^mx|beQlFSahDr#B_uTfzJ)JYvEOT>TjZhL0CAnnO%68e3*#VAGq#rNepL&eN0 zzjD$w-#V&4Bm8Lwez^Xb$?Q(Ch^ysxPibidb6nZVv&Kf2w0)6el0@K5E(lu+NrCt{ z$n43EK^9erp{O~3&iM9t6mS1^r6jQeo=mc6*DGFQ9@B@p>twpJM{&)kmarLtIjfwZ z&lLVs%q>1D7ovzoHv7_Bfc|D9nJCT_sLLoJ#?1vNHkIxh0TZpR$@?>`_j44XEohw8 zut7s%xR8?uIAVPbq9~`KN2Ave3}o#={cRu(F(;*qpLMaaY)rP%z7J=(n*c`cCK2E& z17Bd1u3OgB5|leSLJd)#p=$v-E^HAS!&u(Y`F7ZSF0##dA2A*d`wRDC_*5^J{rue``UPr%zcYc z?1I_30AAvfG`41`8w=|JN%3RM!oc-j6PtEIhN(qpNr3x}4!PF-dkk?Tz*T6dqPBBE z^lR?%sx-Eu!AvTl;}U_P3_cx`-FK%NEz;sqImUD;?@1b!oz|cB*x4*tL+ zJEvPf{4%{&O)IIL)y8~dKVN`TZQM~Ti1@fZfe^twJ9KThBQ9?dm&5)@nPh=z~uUr;8D!@us(}wJ5W_g@?nK5P{=) z?wAM_N(GyWl0B=9Tbx@DxP8BQ*QMbXhn~$Z4t87-P$;`;)FJq(cG&W6-r`dD0(&I) zPf3iLD7w7yJyt6qxOdCHd6WJ9|EDBSD4}|2VyaKH9=NclKKbJp2kW1bBJp~o?w($* z3FEn&2g05|e{pP||EHv$_)4{Et&-kqZ_r_U&0+Z7i~md&B<_p~jtN}ecWETTd?wL) z0RRdsuTT%JQi*2`kzx!CG{MfB=2}plog`B}mCkFT1JzDtm5l95OYCqjz?D7) zriX?qIZVp0nz#RO3Q1yz`>@Eq+5ZrRvZLR69Ef?$zP0}vrjzYkW7AC>>w(>{P*u?M z*4eqhaq`5li_V5bTnDpLD0Bh8mN3Kj=XFZ(1Sa?U;a|JC#Z6i-E$>v&*MeBVGv7b{ zBm2g~BVDk@fBg4YC9P~&JD2{8L+k@*+KY%z8oU=!mXA+JNr|{L6?d8EzqR+>aZP3I zx;V;=il_(yrKm$E2uN>YgV0+bA(Q|kRYH@{K|oPZf&uALl~9wAASLu7D7^_Jlt4hF zgH%HoyqP)Yd}q%2=DXiL=a2Kd_jlcYK=#^uueH~+)_(U|YrpUFfC;}(4*u9A6j&|R z4CCuYRZtluffp}DaTnj3)LvV!_ACMBNAH;PkHih|AyjpyPS$zk;Y9McP3s>DIrjgAR1)hi- zl8r9W?!671Iaa13XZFWxs<)hfK3hQLGt3_7xQ~AmDWc$$6n}SRNIEJ}Q$V^ywomBd zc^1&LS@0}|pc01l3AwX$^g6FkXT8c}b$!=*xg8%K{s5j`VpU$XEGjld4)P1%Fg2vL zFF9Hd+YdH*FV48WVcy>>k6KgFE$sli6-1P24{I#>d&vAOZ&~=kNofPI(dYRlwx4?_ zM98zYNylIS0+Wv7Jrq{qlIM${(X7`Z7G@xKes0U9TFcAT`=%LwObHEfkE%(Z@LQFA zZIWn386a+X7goU?Ef7wAUo$(@PDYJ9&p30Hnc7r-8YM0(!IkbVG&g!>;4}8q!r{p; zQa$hX9uCJtErm!H5)ePDhm(emx41epd;Qp-z0G4f(GXvEcn$#+FTOLRQkF#HJeu3K z#P+eognYOy8c&89g?dtk1*6|sre;cM)z?=i&1MKcWZ@(YQ_}J~+$xQJxtmz!GvJd> zQ|+(P0^XC}hAVv)`;Emgh0?I$GPZ)p`W)?p`swwOYoM9x>ei+CDgOfg3mrbVQkn}< z?%X)`@mLR3AF~aV$~G1K?U4dQT5?e&)iv}f;pRax02h}0d&QLxw5q&ll-_} zvrvjMIu{I9Q+qXMM`#8U61_@afmg%l3*=`(6}`#9c{TAH2*jEUcjEMwPqi`1#m)Uc->eq}4b5TqRF#mTZ&)<@xZ1YCxtn&Vw6Ms*JBunu@4>Y1*7#@?m1e$j*y?{^}zm7YUg60L3$TOn_4ieBr*3 z!NQD@ZW!wP)f{`p`+ku+Lzgh_ti;|R8=k`YgWAT=kS&|Ms-@@@c#Ss2+tg2KXMwftPQ>r}9WNhKJSx)R9kq=;Lf36sMm9TZNT1#&5{1s@|bhK#Uh_g#hs?$*Vnr z;*H|z)btU3k)xLwCSiI^Uh%-N1F=#Z+DYpW2y!e{tk#2S4qiIl5X_lQvXYeAg}U3R zEoKE{YhLx058r0ttfpK*jFtPJ!V2Ukz0C!Vj1#r)LBRq0y(^BmqbSiYAAKbWYrePc z)DNuDGq_V{UhUqf`U?D%Bh!z>myN$EuIzqGxw|r_cGGbUG#gGo%mo{%m<_q`=*|3o%*bai>2O;@n{JvmL*&T9CWj*=u z=N-Mqfd>fZc>7PA)7|*+Xw6v4YxU%=6%mu=oBO78oB>c?MZEM;}^5AVV^|K?pd|F7PKKN-#vS2!w9<80qVv!rtnV}rBpfEZwY=((Q+ zERMTSM!#_PX^(rV_)S#c^GLOf;hZ1c(*qC?%;xpfQ1{h}Jc+NNqC$BRqysi?=V=tj zMD=NpP0VV1%HTrnFxx!=T7|gDc~N?_zc zcaEWsWtw^xRhX&s^-h`|r#;J+{je~;JzoNljcCrl-NgL*zUdwLO+{0eJvRw$?q~x+ zL<@5EVck9DP5P%t+1dEw6ijwrR=!Q}%hqDs6UT`cg;JrvMr{IxVP3c@^&)CIM}AiP zTH&wwaC0HO;L;9r0@BN{Mc$mr+0N4I z>`mYjE=}sK91=iO4dR-=80_-iAiA`-;qwYHg%1HQ^^8<+av)30=e9RMpMTLAGcZsy z-4V-9cK%93qh|M*CFHhz&xFTca?S$17l^{mD~FuSfY4%M7)mXEzK+lIEOF^RMbP+? zFS!+RD||;02xVxU)NGBm4!^nZBS^K11$?94JL3OpAlYQYq+vMTpo6 z2#`9lYJ9!v_h8>UGV0a_YH~QT-%LNAFJh+3<~=WyLOnw~>B~`3eSf4@cNGh)zUiCR{@5*{ysMf$`zt*~9R>)Ct%ha9%jLor4te56btxa}cflF=8-ZODN2@LyNoB7$5z$k+o0mnS86D5oZ}>*i~7n?*Slf~ zEfi9OqWKT__?$PKL6hWSXg<1iY_OB5(_@=rY(;l|8xHc?w=#iPS@U;Y;MX}5-P3Jj zLH`QcVw2-s8hbL{d0Hi{KoONIt-O?V5^DFPB0mdZkqOY3Q5O#^)x^WIT2vnWrM3JI ziT{Pb>ZN@6yFM`WKV1H|ou8XWV3cpS61u)#stid$=tdLVHA1v7@y_)`i;X5YF1r11 zCvf!K`Bsl)@HY&Y2;A4`u=y@q#=kOrI%%=vd zH@Nc=uLjHo@?2-orlQ*zHK_EWx+%{s`>^(!!NbY#O!|+YTbh;iO9-Fbsva&=H|#Z^ z4UHiZ>UrTSxO055ycBom{LNmsq54||J7W0x$3SsJTw!q#4tEa_BD!Y6{~K8X>KbR7 z=B3a}5e53XGrxw6-V1@B%hN_&VuAKXwBzc)N?*MbMi-Bl-LZ8Vjx$?Cn`~2xUe;Jz zrbw%3XJsXB&5vNIcfSOYDh};Uor>Gcs#^DzE%8n8)V4#NLyVS#Ur;o_z@xB1T>{V$ zley56iff7Mn!|jZO5DP6k4nmF1?VM;GuBx4-$PaT>g!&Ma=MG>3#Pcdv;gwdQ7tQd zMrVr9P}oRe)Mwx2vA)LIJ2;$ugH z3KTkzPB)alzrlOo^2&5MBfizM_vhxZ9ZGjtT-&~pu)F~^B+JEBBCV;~$QJe!ghF2B zuZISgEp`U6oqzVX^8_mrJaCjWf1@Q;ITgq}I*GpsEnub1b@wQPxHaV!7JE+xj;C35Cw@;0`@OktC z6w;R?G-yW+n!UrkIcuvYe@(`uNLsMl3_py*VWfD4**_Gy8E!D=SV@yS-Mq~1E;%ri z6NiDMo(;2UDl=y10Ye)ND_A~;TUc4Iw=y!~y*94NKP5Z)ew$6zxgxC0e66%U4-n#` zOKk^BM==6^mOo>cGz7M;ac}knrlDQ8&U8;gBF%Ba!HymQK!#2C^JC*4h&K7C?%`2D z#HJ!HmVr)e876IbwIx!ZG#lH)e(-Sz+buQCSEE@8$<+4vDK%9K-Zk{)66uSmFU};G z|GUAIT9V`HyhQW7lmSd~^jcswX7>l|pELLq*z^4aq~!$5%?l zK|-B7w%)ZR!nqZd5QGEEcbj)X{S>w1x@%0l}_6F zDqA6LWwe$w)CB|H9U}poc!v9$F(Soh{U5`M`giuN$_RZVXhSw%4Xr(?CoGSz>+(y9 zUl_t#u^Iw`@rm|HT&F(I2eWaN&liTiwe;Sc4^8~!dZ->! zh2W+WYI_sNITzIE_7*c39OhnO6gBrZ<>n$C^HNNW9MY5D8?pkda8rpjSQeLHZG<(r zhIFDcIpjBlzrtS~2u_cuZge`lt8t@F8EG#kr&SHwU!^9v z@L8z$mnJT|tmFcBaE~X=(n8Y62Qi_G!ndfMRv_ei^t(ns-7tn48Nv8i16deNMV=KXV`74hUg*r^mU=S~e4~gGb;8>kA-q)X z5m&8U3ospQnmCz>^S8m~4Fq3u#7#dZDq7~I{$~5M6+;U6ra8a<)_1YW8n2)OwabQt z42*$??4KxET*TquPb;ehto@{_?=HDLHh!(1A{0`Qx5%f#u1PT%mIRnSJL8-pbe*(y zrbfgDA`}xS#!b+9Qfw#zTJ7BodSM`tBgN&eZlHS*0%4OeE86U0e-%gKih-KG zMqUcOS9d3H7+_+eCK#TH&Swx!Xo(~7v6|4^Rqi6jv_+qrv1w^jU|v!=igecXZE3OA zmFBg%20_E7TgkSUiY*&@n;DyH$0P;bWYgg3sUZYgjUaZ|qe0c1k7R(ij&oAb$}tZZYDB?_BCVV-M}DGKU6;ykXTn!*HATc->EG9KZD~%7CjNzs{ALg%N?P z8O^War-q}r-B0iBl)6^X8v!jg5&78KK~Q+QDeif}xAW=BxIU2o$T>Wk8p8esHRkP^ zV&#y4>#fIgPq95zylDcna_6SfKv3-!S=VYAbeCbrlI1XNwV2lt|FkzCSihdR$UP{g z6mNL5=?)@tnq{hO(O4Zet&wj1l>0-uactmv@i?w}0l$;zFoE1* zu6Z>mRNT??@=OqeI?sP`A1mikAFlfW;>)auNoDW(JPZs5Kv~X`ebC;;gA+yTI<|dCu!1z=Mey+Fb-USva>Z%RU9#hQmuI~* zW*LH%C|o+bbB!3$*Lu>tU&p{cFQOMxI1%OG@1%s;*&jX$&dyC<@wW^%5T^-ls%95B z12jGtW&|gYg2Uh5iF<%4tTgMk1c#cqDxTFsQbj|*;iR%uO77seBu z<7^FMCxWeG+&yG_0A6k3*=)ITbGe}yjBi7DP1^lAL{V>4e}!Bh0Et7k2KdFD->|~t zwoY)?lqV&X8d4qIMknl(Mw;h`Jk;(P;oLIKIVIuQc)wA3=gcfmm5B&@ZdA&OGY7EtE&ZXlc@#$D%O_*2UB>8hIB_>?GB*=6$Kcv zo=u4LUfHZIn~$pU63o=SvePv^u)7OVe${MtnHxFQ*L1Oo@Sx!)HQ^M=N}%=q6>+uB zwFlLuBgJldtd3Uwj(y&|4JJs&(uSnC-64Q(K@QU-UEK7}c=%0r;-4}1{CgajZeEM( zp@#Y2hKzsuae?D(H*iv3KVAW4(D9%y^IiO(4*u!qPns=(#c#(iR1DsN;WY7V2k0>7 z`KI>5dk_PmKUin-{*848JH&J$+4Zoqi`C7HniZ+izArYI<61cuq3;E1lb;R0#w1i^ ziDE=AWZo~%N(qZjqjbR=l8c$w`ERo;t{4S1=9sK`W#ysaIRJ)3=o(F5IOOSdCS5Xu zC(j@}twY|VX4LQ~UeupEow(W1#nZK(*#xWn_|*Q7wfhHiL~w}6`4dS;dtPKfhlPLP zGC=eL`byPMpjd`5H$x)b-&|f|nn8l=TxFmi3$I4MPn%sc!vUQ~)E`~L5`Fn$dghfD zufc+P$CFlE6TF}RFgNz;=ctbJ1SV$8Gg}0TTO+@qjw~peUKp!c4_1wXqY7cG01Aw| zk~~b-0~>#5iV!5iltXa#kAx)s50ZMv)bCiE4h$GqBC~Fy3FG?w-WnhqojyTxrc-)J z`?JUjEnX@6K8&WIdjh%sVEEPth*#YHCIWwuQF zr!OJpdv`n2C&kQ|{2)dW`fF(ztuEQ&z_s9{UgQoAD*!H(^yNJ5Do9Zs>h!0_srQo< zt!KR^S~tImc-st!GJIL2?8zz7oICZ3+$g7NWRYuW3xjKBeeXiGl$7ZvqI zl!L@>0+9)_$PEt|z80@24yUYv3`K!BVSM4kEmIu^$Azt63{1Ie0#^rl!oTKQTn8mU z2XK~Higcqu+0#|S8TzjjPa>`oTACk@u?&iQc!?iL>R3_d<**>Cd^H`vKa@<|FkawI zC%Uo~O&HFaVSop^?Mz0Nz7hL_?jg(#v)3RVAW#&TsL^-Bk?Zn%&Ze+xbCEPxkP$5Y z{*V|~j_c<5{ACjfcdJ29KfA?g0e7+ja7^VE7X%bRXfuUJ5 zJ90z=^v^NqIrt@)6H=HzH4O4{I8AsY`KP}i?TDFJR(mxthG+FpH4IsQAJm3Ko7uWa z)BW%n^bKdVE5FF99S}ix&m5BtA@ATf5tc`%;8o0+E z_J-pC6AtLP=ofzb94$-^<9M8h@1Pm1dlBd4$IvBA!@gj;G?Y$i?gI}}mqqfDOiwRl z5jq(jV?iCAv}^>f(>KzUa#s@@vCvk!*n1;1j2QSZ5lF6U4KHRh{9@aAua+Ihk)xKKfG4sXa zM)~JV?EAEhyVsA2sE5`VTjEyAlj1?@F#1Bm#jrC$gU^`TuBR+x%#%Bl;)6$u7dc-? z%4U3>f3eYaE$-2ujMrcOWGv0tad^5W#;Pt0PNssD6~p=d^1M?(iyHIw##7311w(4x zNM(G)ZQ)~-6FsTbhGz}$`~A~${xZ}5W}*r(e<5dTJtbu?qad|JF_z2k*NZ#Vt1x9G z?w5D}AkQn7I0^_cW!xQEqO6-mzW(UKO0Z3;Yg~vH|0MvJ!9SsD1r`rf3#q%Ec*4^% zrPt`#(CpWQcc-puZdE^=S?BYxWiTklpYXK$ap(sc|JU^mETfAm)nH)WK?h351b0YL zDa!0ac0CsF*+fYCLJVU>gdE%Ig-OD3xPKAV_y)R`~5>mrpUB2>AbN_kRZB|KU>j zUfvO0qebCn_o5KNGWR$G0k9s=y?Cd1r(aLAs7la_49}B79vb%IjlO-bWjoc8rTa!A z#O(Fn^9KI2|C08LKzt#KEwtdsPU`s++~9X6X|4}<+GlV-4O1AZkguXYUtMDOmvw_S z&Lfq9k4RgVVRQVu&s7MwzOp^k;EnnB{H}1uXPPelOJd=*(lHkY(9t>HU}}{B&i$O7 z_#40dKzx&ysCg&bZ~NQTF=q7u-i&^u$h7}HV#iAS+{;u5CKL5uK8b!sK3fs9#{2L{~Ul&bsYv?fu9pGy_So(LUSZr$@%4A za@^RZUu0cUk+Y}Z7@nsi^Gy#QTnk)ag8uzop23Ik;AY!6&^Or|-z^kG4R*JW_H@O9 z5d{%%?=pC7*=~dgJGo@!KmXN%Er%Z=U^=VHbbXGIk0Vv$tF?zuIS1}T7DfY|zWlUv zr|{yVk;$s<{I4tDnI@(7wN}-zbq8TPT}N^D@F(`6)7137%I{1M8yBzq`$-@)lJ&85 zX$S}e0z+jNR73@QifO6P2jtG_4m$U@-|zU0<&x6UXvC&XMrL^R4XJP#gE_d}N}q=W zgqY&fUN_64fOY`qNZzFbrWccZT9!EIf0Xy`>DU`2rWus|hyEVHe$QkE$M0tM*8qLJldmbgR*tF?3;R5+1y7_t z?TTh))MoOB4Ni~SyLVX)zem0{Fa{2qmY^-8EE^!TJs=ugk)^GXtl{7;j)>Bvm*3yJ z_#pyZ3T-8u<=%v+rnX6 z7t|8p9X*cK0vgI07%0mSA;o1f=qf5vzfpn>M|;eYGAZRJ_oHQLP2Xszi$@y!$KSYq zItKX*SpG;-$ZC~yd<`A27OVq_(~Jb&on}zo{FwQaLQ3TJjzXBoWEc-u_h((P1OAA_ z1FOi332d!Pk@5iVri*m-N(Mt};FTG#qlCV6;QmIzG@=nSKk0LYGV_O@>Tye+Y0xsz zfTy&JcdLAmtblPDAwRnW(&4Q`=1J*(N!tq}wLS1DryJ|i7EB)EvnJZ^v4>0LzNCU; ziVfW+4J}t$xXJ1;a1weHfx+Pzu1riZ)P`Ejke3w?NYB);oET?4=&>>!f4?$RH+J?r%Lhlyrs$*cFZmGxa+QUO#f zu_Rq6I2{xvU}T~UzInGf=MnSoO$bQO6=P;l??u*j8hIWieh?zDfCVECaj2{-hwTZu z0lu+Je_wl^v3n{u;vu*kQ<~+F&kOJ0ERwB=~CqVl$U4|>Ua z$XlONrx`Q=C4}kP@hU@`7X=ty!KGmNStBcz0-*?}3-qf253y8T{@Jyv>dB>lZn_)x>e$@SE_S%f^S z66LM%m4C2SXz;q;4QmOXhP&;pm@eaQjt}r1*iRYCxD8g@G6|m*)*;c=Wa_g4a(ldZ zLPs!w{d6J4@TRCc5XYBg%O7nD6{@y_uc;0A+@JS;KbqK;P;DxnHSr6dauI#5e^frJ z;jx7b3H}mtuYG34Oj%i34dv3c9c2~Tj~R3^E(*_g5@fHgwr(qChD;?HiY?z(6fP*U zAZkZ2kdq^WgPSapSJeO}~taoUJ+AHj|_1Q>*B_;DOqkC2j1hbUC>f zNzjX>YKwK)K3;9KV{Uo8+y1P7sTCH+u`CE+tL_?mkq>Fw(Fqe8y~QW8P~PW!bh&Yt z`B3I$pSSIX>dy49ds(hMS_FA+nxs!mEkz|gKlx@E{1(K>#}wOCt7_gdJRXro1_O^Y-uTS_&2K zh;ni;y+nCmwiY!L5HBloBWQM1V~^Fu+%Is;+x{EzR>nE~318OyEh;j~>X72+3bKl1b~Mv3ZkmcuC%AuETb73Hq8j(nOjnthjT!YnaWmWI zyvUSPxB3OH4)9ibVco^?VC}ED0X}W-n8a`YCxnhwC|t@V#xDUGDht-E6`6vxhfxu! z@HWK`!AxJuY{b7tZw5m-`1H_QzQx-~XVWU)gm+ zMxXc|Y`GO&+7RKV_N>jF$6}eYVX{Di@VHlBM?x0bi;y}i1%0pfP|e9#_K_AaXSQoCUIhNOM3NI)1*elDwfNI#N(N>%0qPKdjdX_4tRV_Oh zh7C7VaH4bb{Po*^wBUbgqJQ0G9(I5&hh|XfgH~kT8&D=MZ%W&?K&6}5Ke~6K-+Wok zFD5|pZcvm#iw>35eVxe7r0P$BRTtt&j36Gj8cgIcUs~SRo2M=>vh^#!0%^3#8Gi1< zR6NOD@B!5f24E-RmHdSx=zVec78uJ6PVCLQ>_tn~+yx$_n#87-ZftY))ogFYk}f4r z_TJ&t9Bl2d(djkNwN16?7mk3=!h4UHek%Oy8)stjwDq=1z}luKPU!p!HPXCtq=^!u zx8_tYriTw%0#(G|v^J7yV6i6>^U_MPR)X@CCN7L34{Hhb1;dIyq=JyRkhrn4kEwFf z6-xH_7=?As($60>wmmZ&Yt0Id5Tl1(jSM0Og3-60vV)YcLVv9*Z`^ZtU}4>nnTRqLbd0}>)^&i(ax?V);AIGN7frmv7WT!UNkDTE0I5GEO8 zq)?U=-j6s66awuSfc9Nmo2?h8bbZc?wsZ~7FLo60D9S^a;FB_s0~%Q_^+F$#fm46S zmBE1HXJ|zZ9!lzkOFv8ridHrhcH8QBT9DfQYWFO3@67BO$IFY4jzu!K6zsKNcibro zHPkk|*r!5=cx6VMTy$Ib`A_FSF!x{XjPtg^YPeIRuT#<@`pgP)B%~W&__=SB&V4KU z&NOr5+bQ*avXxdp2irA>Y(<)o-)1SdVhJF49X;Q4PFW2(3t7AQ6S_9XH!o(iOg9?j zXgX@Mi{p;VHH38SR0d$|Nvs-wG~RI{#;J)VH)vgBgU&lX;|lGCXc!{uBm5RENirVN zVUH2(^x{A}2|I~r;3<5-MAXK_RdranEo78xmNY1Xp)rOn z5Qc|pF4IrL*5zuZ7RVUVPSEq2sIhJb-52P_5kr=hjym@w-=G}_`Ll5XA+5M9^Q0n! zp&|lu^($QX!K4;@w_}&~3inPP!J~34K90W3-yS80m<7R|8B{7tY&u744)^t28>Jy`RMbqDWL4g`I=%(1LF~gy1<`Tx0L!E3*IbH` zvQ!M1TzM=k@8k^?{eTh0RRwu*zSHdNqR!DhpBVX9IXGww}pUcMETHmmQn& zxLR0ED&_af$v~~~VoBTCxxSovSurd^&8V2+6A$k_EJnk>74ypDUTHLol$-abRccQ7 zhsX4paXSdJA>_d0#GJ<3MRQ)QJEO9|pocW}jOzX}qRwrI^R~(nv zGZFmb`Ri}~O!xmUoy=C@%2r3k(TPc%m%3E?x&4{3u%A3BO3$Y%`iRz(lyghw;OO45 zo7MhdkXzV$ny0C~e))&*%jyw3&kjPI{R>*9(UZ^GlxG)(4*Lk+U$`@iK;B6gpFdB9 zTDN7!erI~GwQc&HY3yvDw|88{@9@o7K&p0DW?yw71%-$ zU`dF)S?6NmGDBD7dadt9y%HutLCrKEpTaTB5TffaE+Y+y=R$SZV9`+3B&FZ#gLCd< zqK&CbBV-+**mxVxZ?!hOxMXDe*ztqJCB?xp;e1`)WLU}N3H0jGDF)|)4UViaKZ5^2 zTw;~>Dn&>I3cCWuhrNBvM?*KOWu9lSdAC)KkLflHTPRo+)Gy z+hjq9Qj?6@LLh$FZr)JL-jKPYVz)pH}lDD&ZMjFq2PNcAGhiuQ|H*6C*qW+G*p z)F2=gLZfMPIxEjSsBzm~ac!g$0D~XfpvWoq8ZB||>Slo?o*vk<6E;r0XfNre@9{vM)X>`m;yLz}CL* zm$8tZj}0)*c+xr!&r5+Co-T~;;d>pVHa@^i2iH zA?(fQ^5j=!&fWgO?z!7@l@0pBYOx?A7j&g=)e0xi&q=xdac2yYH8ff+$>YwbHL16( z*O`Ei{>~Eql3O&WUWR^df?#1iBd@4CGVMb-=drz1AKJz1Z`CB7sGsGoaI?1p|6m}I z#f6xQT6XXY8_k0&KdzQ)5&sQiVv3ENEjQcwCYNimr;I4X|s!?y*JNkvI znhhE!;1BM4LF7rYk5gI)A<}{^Txw`Sg*XBiDB<{m9;cYH?2D53`(xEcuypEB6I<2 zJN}(XbK`*H(A4m?x3wIHSqM1H$ADp~nFc7{J9~&dx29PdZGCl5+9ESv}Jmzz>0%H+Gd~ zYe#Flcv(wUQ-4=#98_HRti6Q5_#pl9{+4r?{nEAbVm@QPk{Jpzpn=DtOPFd8A1{zV z<+N)WaU#?bjo=pnbZ$TlmBoW5b>MRDQ^{TU#Yy8`D-{XVq#ha+qS1-*+JsC{G2fXU z3{K1wZmH$_o07?Q42%Y)p;vgIs9CdOyFH~oBo^ony zJx@QlJ5H54^QnP&Kj;@k+Hs2|nm)ycr)Kj8UJ(wnPZMk@(Y+xK8r3M zbxkcf4I!I0!2&CEF4SN|#4Ae=`<@n}s$J7F3IKQPd;Wv?-ya?JnA0c$le|Y*@!>in zaV^$6ivst|u_V&Ar&<@3^f(wc`~p)sSkv$xZ|b($<29Io;5%ixxOflIVCjs*VHm=ts5&rM zX|?us7{36_Y60gVA7^6xZg0uaZ&|?35uSIbpomLO$fsrNNM}7M*!<>IyP15>JEGze zuvk%q#CJm?=3-K43tt`akYi6j_apa)FTYJ@Xr*sMO74-Yr1a}2=x&ecNaf#H_0$VI z04mAi9C3LP%{oDR)E6Y~HAA+5piKA#pGB=!1uIivvZjd z>NGwO2dvA$?LIjk^GoZNA+1Fp_{}Z&49ysSFzNO!v{P!}0pKus0YP0c*-vSRpbw=@ zMCL04x(0xbT)v7AuPUOef_;~59lkb`?6D@lCORf%`<*F( zQ>xSINN{d~p(8y3(E7~3_vqz-5g9!SST2;nPlQdyL({_wG-w8N1iuFn9N_#76&lq) z%xZ6H%3O*te{J|7I60e95#V&>GGtWc$}&q~i0>85;JI68FBcU9X~Gkf$7>1VwttqDrA5;GXDm!QW*rADa7`iqN4Tu(rqQ0TnpT2 z!5tcLV+0R|9G1FQPb`@BO^7n2P$HNqM+Y+UNIM_m0r`6g>`Ch_kwxI%<|x zX({L{11UkrYY(5sR@beH%_}1t2HD3~{0M3DzLkAo!EzdlldA)=)~o%?gT8C?u~?=- z5{I|CSMoFe+(HNZSpkssJpc4F#D-M4Dj#G19;G^}r9Z2mYpP$%pcVv*khFuL18H65 zYY$z`i}MSrwC6FxQ1-x%L?caxN`oP3z?XY&J%uJxhy{WLJZ4yANOlmTl2(6YT>!?* zSZ+0Ea7Rtqmb^;u4RpJlV^-15uv)`6A4w{bhi08(N?R^j zh0n3|4X>b6iT+Lj1;V}stfw0hRx3XZ4$Ns^Y=KPrU5{=7-fv-*Y^UyiG}Ipnclks z*R}o7wJ+RojLSO2TA~aw%)S56hfE=)rp_j>7Vq+~Lo5h9&;m@&$Z9#gQd1*N$4F(P z)%zL07YGFX{JbzflF5zF3M0X7Wz@NJK!-Bo?&|^aG6LxgAt7q!9vs=qG5qc?R`7q? zd;6Vd}xZgcsz9ZFbPhQEF#pJjEhd8d}7bU@ztdlxdyX;bUk!0Q;ke0&FduA zBnJ$cwNzhFRfcbT&I*5e68)L6ek>Q}c9 zhrzAV&T}DRtBo2Ed4-F)I&nR46z;MZR*$21G_$!Xb99zl8_tt)ps*b`%V z;WKok(Y@PD9zCUp?s6$ODRN>D+E&y}Wk`87*6o1-%NrfrPDLOFQ;dAj8X|P@SPjXk zXA!r>>b~D-0#80M}ryYNeeAs)zk3YP4Af59Fo{%v{t2iHN5M{eSp)GHuGHx9^A;9LCA-=a@o@RI|-uC zXD9T7GFoh1u-gei40%sW__F!R!UaRr<(1Yz(S~tns+$(vr?*covzJwk!So(jrRXJy zZeRJ(jFC!cDolm8BJrCp#*hB~>lZ^2;1Q;9;K1ag4<{;ZNlzGq(cB2OoQUi3ST#-F zR92ee5^a=v-So?w^2m-i{nYUIL#(_+jf$m+Pd_PUd=iQ5ti5P-*ZBt8y~5p82k-k~ zYiOLnA+3-m2Dn}-fW&N93biGU#%p1yEh#!4QPs=^c(_eco+npkAi(TL{ z5KKQ5-uEmD>|NdV58)Z5Prog8U$!z{3T^>3v~PE~R*-t!2>J?IwI^#Ekq%!#&R5ua?r5V+ZU$n!5L0XbfU1nEJLdlK+4%DQrk~%qcNy1!me0R?ULyT!4b;WZ zw+J8x3bTeXgmC|pU0`BPcZThUDC5MIT|en8j8jlft2s&B^@Y(?)bobsQYKp3GiNiJ ztxxh=o)no6pi7T^Jo($|xIHQ;N$Jx1&HgghbI9!EW!4aC$(84J5L+>;nhcpLgf|rK z@bS^}k#sH!y~gRQsezB*Y(qW4bq3O@xjpX->u&*vJe_TGI8PqZf!!x_(#z_{R)T4D zC8S`P=!mvZDs>g`XT`{;5$09AS%8u=H$$YlkcP;gH#QkRIErQ zMT7BTl8|^4L9i@F%dyXT#xow%9U@<{XOj|eO7@uTHES;nKY#i*E_+h|u4&}ZDxDrTBQ z^t^$zO}2NSY9{E=h-?Oad4yWM(@xyJQ0*KoFDM;{^KKKcaM3f1s$U#&@qp>LbG2&E zKHNN%)UeS`oni!qli|3p5<}_vaif{U8a)F_Wo?hTzg7Q~)q5rVd@xUzcnkEdJcW1Y zx!*GrhOi-tv(ij|G%W@R4;z#XY+4Qe?<}PM*ZV%t!8nCjik6Dr{@^`^|D^g+PSd-Jik;a0 z-uR>Pk+3<2oh}UdKflz%GvH4JJ^OPm>kl}sCT{(yr0hT4!3|#Kys5U>!CZb(9C0i_d~bm`jyQY1(TNN5^Lnh8<@ zgt|8h2uM%p0hJz_(u=^0=e&E)x$oV3|9Ag)&wKy({d@3bt}*ABYpyZJTyxIB7-O#e zf&DK43-BX1X8^$17ytwS0LKA`4*&p%IoQw3^#cd~1wV2Bx!b?s0IdUO{)Tg^{;ZwM zx%21sFF8)K{~7R$fv&zTfYSu0l#}d#Jusku=Z@VyGgDoCBk*4dAAiH`n^z!#L z)4P4q!qV#EvCn_K<0tKbgWtoS*MA2%?XLIz^{KrI|4yC%sQ84Vlb-{p!Z*%O(wFma z4zp)D_*s{~;<7(+`@iBhe&Rv?5B)iH?)}7lVPO}{vU9MhrU1Szu?rl z;pq|Z^I1RPr<_hYdBLHa>v7Ic1mFiS1Ly&6|Li{}&jD{70C0T`066sW?`8HG06;|) z0B~;T?`4v&0f5ub0D$t&znA@8CJ!I@KKM&>KS$@FGiRii@c;miB>=$J1psha|Aod$ z{vF;fa!4W^zPveqE&vaJ6W}61AK(RW04Q>>tANXZD*%=KVZa^0(ZffM9yxsU=#itx zjvYPD^$Qo*i4$CBPn|ybi{ROF=LF9R2ndNvT@VtM5D^f#cQU9*s)_=$GP~pxcFp+1cYS&)3E$a~-r?}7af08x&( z4<0(efq&^O;OMb~hmIURe&7U$?DPxZz@bA2jvP96_= zzh1co@i{Ag)$GCJq~rXm&^H2tD!2Xoi%Uu+;O4%`DY!qnd(K@0U5D8}NyYR^DhE7m zc>m?wo&2)zQXjsK>o_<%KP%AH%i}yo@FxR*=inzRoX2noBD{a0{3Dh3z^yX}f4u_P z9|D{_#9^NI5HCO*uvH0gdil35{E7T42^YX)4s4d#Y#AL&G zE>=<^Wst#0-ImE{md>=a84#PRc>_Y2FpWLlAF9Q`3OyPmcrHetM+5?6MiS$|i+VEP zbw$B}!}kuhbXyDbVXK~~vw_EnvW~_x)6|q_$ugnVQNH+!bGE%FlW7hqLx0k*U}Y>~-OT49>tH`cXDN8`~3MYcTVY9(w&KjND(cy}Uxcm4FzWB9=j z^&WP%78Jh`_j#}7yFx{nE(RD_Ok@UGP1?v5bIt!NS^n>4G*w#Vyg=W~pg14rK(-4m z6qdyH6ofcdNZG^6aVjLqm=`Sb>0~8%tK;!k0D!hdzvfII$Y#<;T-*tkJgf|!q7&Al zQa{@RWmMR$<}qEkKKn9C60%=j2KUku;GIsdIcv=3<3%YZW zskA|CvTIp_)WN~e5pzE%1PhP&%^DEonRj zg5VZ{KsL@!Gia$>FbR`THKl%N$$Y!Ekw3z?c5-X8D6iqFlTM85?;F6(+748 z5+d12;_(T=y}gTz++GR;(};Nwd89N z?!`qc?B+f|{KzjzMwX!zfuzMtCs9fck9fy-!pENr)-|ZGpp+%kcWeWcg`!eI57gKs zuFtT#{gXL~D*A#7c}R%w6=9#o9F4{IV7~%)JdC5q9y#DWJdHrfG>D{*$%hr}(t2vV zhqPWJZQ;^PX${Sl+3ZV$L^PUhY&kbI_6!Arb-t?57EtS7EIU^(erbkRpYC!mm5k)x zOD@xbu2hH)4-aP!?*;{hziQH~Y0aD{e$-R0pdUnGtfiI(P#!0|!4?ErG2lUbTs27p zBRUIBK0~m5fPep3hL+3L)cA_&_J!2?r(ft<4gP~Cri6UECs^SN-efsH&t<&8L*X);J{buE|i{{fNFM_iFzzC7h;C%qRB^5g1iJtHI zQf9bgpK#$wPWDF{=FtRWO?Y%|evw@_mbbn}p=4?eZo9dQmuMuwm6D;oO*ON#8$s|n zR-0@#sb=RaX=}wIW^=h!n$BtJ=YuHa0T!&P zntXztapd4?7=$$`t;|t!{!1W+RH@TS0PtU~w8i(0$tYlF<8waEdJASEH5*F4NLF`b zR}Uw6pzT4mqv-I@Vaeg>&Xn>Lq(CaEr7pzEV=IV%Y&^&>dt^LF^VRHvmfP^r-29$l z{kp-0K?C(*Y%wU3zsSL^dU3S5a__O;J^iF{RA(_qpf0 zV+%s&L&v^Vv3|HjTc0Eu-lv7TQ|YU8uMv}mu!JT;w#ckvYq zZK|MIkIszykqW$}?ryt$ASHkIF*uuN&{C73_HcuKzkdDNJD}#R`)}&maYRv5i;hdm zFBREj{$Tm+g}JsGuDbAiCb|FdokP|GiE#K1|4CwC%DrS6%)Jj&f!{_5^ zkK7QNGrk8K1}>g-xKE0T7w63H&3V`olv1G?r}u{99#Py^XKimVFm|#o%V04sekfyO zMXoiov4NCS#^m?YKNNDMV?yT<01$aoMbxFRWFBvP$KZo^Dx}#`e4V~b6C>5PHb41Q zT|!u4`)ZH&y5@y>3;lNGJX-rx6cdZ9a@TB`_4JRQXfGstD0l>~K&(jdHL=W4*kjaI zU`D>18(XP@{EW9_4Xrt}um;y@U4?5=wGd2@--4a{yJ3POyu4W^<-G*t^1%5HeZPhG zm}{eqgB`s>U%#UU$ZIM%RG&Ghb;xr${V~kL2o4{33Zo;u=M=u=Y(D%c_e-7U_lOrX*zAtdY({BC_O0IOkAUk>)hOG zczZGq*vO5y-%yLK-8INrPE0pOG#B2l7uVT7Z$m-$E3RCg^s;$1n>ybKQ);)~ln?&V z>di2#-v{jS?gQ#qapJ^(dmhSIiw`z7yH-7Ik;AwxFg636tP0IkigZji6a`N#&0j3Y zET&N4^S#r@tv%3_U<*np&a%?PeK@sMBY+j(er4ddaATrliZk{Mw|VC(6WKr!dW(6queqp8&M$&4^6>_B@)WayB0Cb^!U-7ItHwjd}*b{U=6-0BdIfQayQ1t?h9fc zuPLv)}?uOi0u&TXA@Z!!E0I% z)9T)A=#89PizfCe+}2W3_1PI7^iqEiaslj`m^#5o+av~r7aFvTqg2~uNBXKv-CtE2 z?LeS^9K2yC9iBcq-gl2}y<1f{2EN&UiHDfr5Y|KICq@@(GQjZ8h$}hH+LsiJc_Q-5 zX16UN`v8@uSrRWfRbR46sE*<8w>Q|w+y}_*DI9(|t8wWatd{>wA-&!90`1%gt|)GN z0N5ZLh>CO4^rJE^RM@pym0hya^{v+PPp?(@a z>M64gEn}tB_HmUut~f2-#&DMg3x)U=D3GJ(TYtI((_agglHU)wZ;2 z=`pP2U4snRZH-H&b||0|JS}*lAcXXTFIha0Gsucc-^lxbE!BO%V(wmkgEs~i&(|X= z)@MMdriQl#^M*37>*vPt+iAH8A8{r!%I6g5bmCi#1h@$j%j~eT7Y7zSUN7uc^4Aa_ z6uw`!*sOfHfTuQ|*+Wvwsoh-3sh#^87F)U-0MA<+2VIkUKuSW2w;!aA+a|jN^FFMu z4fciRm1`FEEl7VigeU;-b(vkUG&M-}xlF3_!#vD@Hw60Yh+jOTx%_cf(l2KNNbm5S zgd<3#SHJz+)8;>2x-C2riT4Z&rREjUA@AWJZn?pO1`I#z4`00%1=GSlXay@JCZlO3 zv+lL)(Yu!X++LTWx)>%Cj;=GzE%&UN6gg?%8v4mA^^?JP&$Mu`J*C_XY$)Y_CcizF z&_}nRynmNn8L0UYX(w&>##nnCx({I7g=hn7ot&8JPuh(iyp5x$ZRUUNJA@VV=)b(9?JKC_WxEMa&4_U|r zOUHS#ykS`|{OGcnc(Cr?(wq0Ir8$;O8h($XwXSt(NA)d@YpjV_YUunpRT;_J>n=bP z$m7b{LO@a!ELxSijPnPBjdt_*0d{O|k8LlMl~pB4nqdAw+*ji`S`SE7_ZJ7g104L= zJ?z5VHZ?844nq&ko{M|=eSFMcmYfCh>&0R!4+eNs`iE~--)$ADrnM4@b*piD5*FEW zFT{_psZCFdIuQ1u2CEwg`iHb0N{2Z7atsv?TN0YGL^sLu9flq1S zrqr2>Z)#1sUBCKjKQ$E_w_PB6iOV+UJ!!0zs`v8G6Pp7?8 z1Q?Vx0U=L`aFP?KA>|S-*+s(o28ymLUNo+HU1!0_0hgETd5^ZG|32W6_r}fPP^OjIklMPYn(vzx zt0rjgZF095(T9*nl zmJ5d4M0QP?&d3=Xk}vMCZsF~pzzGN?0lf$3VjB^KOE*ZXiwjc2=1fM5cXFE-rlb>y zGg()JB!D{>L;L4%zY+gW&+`A$8;(V!Sh2C!&mR#hsO@d|@ZKS=&KyLX*9klYIB2*L z`8F~N`*crAb>lf}uXs-Yb$T;(Y*gar38jC?UI^N5-n1lWe}zUqN&rZL)W^)>w`WiR zn(MCF2~+wHG3~a&Q*q;&n(R4&Qe7)$ZS7M55ARiU@3Y~^dPM6Cv zHXGsmPa_?Wd9-hJ9sxGJV@BL8hKmFD0rJO~{e~=3cl8RTBl>yp)iF7Oqvf|z*cIB? z+l?T3PlR>2A2tJ4NOUB{?zIwSH%;%kwKZiE?kc4Xk>mus$FD#{e<1wre6#YlPd}n{ zje0J3vD|(o=VLC<6nHHwRSeWIrk;Pab|0)>RTxs6fiDlri_{!b^PS1c>QhznR&=c; zelEb#06#tCfCF_;1Q-Zt!y3#Z85BqC=c?2y*asXlmf9JQ*o(NZrE@{V1hf*wvTR;=4r3{Q(ksVazmB$7GG0SS3l8L-;ihS3jFcS#Yv8jn zY-DY0O^zE`0VwRH)U|8u<3BEz>z+ zW6zv;Q)~7EGTGAr)-jWX_PEla`oM$x({&h27ggYkl7!|eCc=k z;!;jkdYUibQ!}>o)<%& z1Cez_{?XNjolceEg)6M}2jOU4iCkkYW>XzGZv_M0P4x}!jLRcnRXBE<7v6q0glH2I z!4FQ&frE{J^8K%#>Tmavvl381I^uZjP{aib4|%7up^!o2usUeD_2eD1yBXq6a#l6n zyqXbf^E26WE#p&xs{DX>1@mc1}&2wrp755%}S2aHZBwAb9%Bs$o1 zVSW9=xrO-K^|#rW!YDf_Dp9~nU$i~QuRX4I!LAq*YKXnko2kNFOBtGTdkZ(s&+axB zp`$>bd~tat900*o zTTS)G))abLKyia0kB9FiBrTXXmNAVb?scaoNd|OkSq$%-7Xg z8SyWglOG<{nOMm~+-i|NFCCsPCjvc|RcotuJ=;uvyxphN*a1mVTJuliqPyWMVTNZDSnmgBMF_-B8b4PD zHOI<;NF-SlcXIcsovXE)umOgd)&K)?(hCS_Ie0V62RSnGl4ye&?39`yS6tjY4!3^c zG<(n?7d~3}L|6YTrc^W09!G1peA=k(iMw(2p3|~eeZVfJ|L@Qn;^S2 zW@3BZ&YfE|8M;hZ@aU?&#W73_LM5ycbWk86w2c=&Gb@m^`M|AT$acwM$k20}1u>g^Z||`% z8nOWrEUHWmBNGCZD3?+@2?2HLGZq%VqC1plT--_Y>qJL5rPIw1e9kMcfIuOkvFH;n1kCW;IB9a$l12Z-*!1Gcud z)HfCZfvD~zu@B9>R)q0`ljQfU4wQy{DE=-;7c2~qt7*||QO~<; zw^~MMTchHVL@GVd#xo;z&4pI=d`}{8FT&2ItDh$Ow9%{H4~ z0|KQ9(oPdqf-8gtUdI8gcgD+*g2`{=V=I(_3Bo(y^ zGt1sGoEZu&Obf&n#!@PWiyM7q)E>Yaa2tyx9KKXS8KQB%SdUPip+}L%}&^s>O>Krv~ad}V=_OP|>hQ6iP+KnC!GBf^}?+(10R5y||mO7c8 z&Xc7Oaz()1^Ny5WV4het+B6cwDRugNHGUVnG}IJs7DidV_d}y9a*(?F)`cnA{mr>M;$?vL zE)l+i;Qe~1%r0NlbRU3%M=(D;>e_oeJNCzp%T#d$?rc$O|BopAK0qyH?Os*czq#(a z9MOKa4=C`|*b0isODykIjehPuHb9*hG#)e`4|uI zmc*~9+Ag}tv^<8`$%QtF=m>w_F;f%`gN>qf6+S?M9+IL}ZNw8@2JX z@p_p+T2M#6?it4AF!}iiqZ^?447(b%4%oysha`BEwxw<9{v|nF=|);>9csYIx8+cJ zb&k!LNYUdJ+t(%ivvalJk|h&%$Vbttb&YwLEdw?c7bWufpS!rqNC8e)d!%WC0!r`M z{Q7Q4K%H=2!$(i-@)T|()L?j0G*3?Ji$kTQf`97L8-l*fGAgjxTnY?^vrJs$C_Aw6 zu&niTWB#*NW;+@W{6y+*-H4B-^ZPZx=WR&i_170WyOyscH_c4C+X;7DP0_E$#WVR& zaw6n@EF9~v*nK)PM=?t2Eh!(^pu$s=aeBF5>4IV>p55rPOW8|)O>|@?k9Y}_bI$hA zii_ev1oC$UJ?I3xwcMj{a5D2FGZ}Fky-f=lXJvmiEZfjwWw8SYP1EhP6P1x@YJ)DLL3t9BN@m%7vj^)*Am|-1 z6XeI*88EzF#+s}UYF1J1;&ruLP0qJ=(TJwG=nfqXe;Ij04k(pRiY~D^G$mk14^ zaePvoIn~ihn-rk;b~CQbqo=tcwA8_==8%s$q&3s$4+75Gj*F?uocD5<>Pk}Pb4W_7qdDzJ%leqANH|u_QW!u%X5Kw@1N&pjnY77IV z3*uy+{Hp(hupktYJgbicU$d`g6zxi5mBD(YA_YOD`Q?3p^@waf<-#ONsjYPF<=(U0 zj_qJ`u9yPv0^y{VMVs6%ceuZ*hLcpdduZ@SRHqhy@$H#J2&34GQXs5IE~~6|75(GIyz4(oF(B$AkjoGHkP$YCq8B3J}7JxhK972QzU)PqO1#ONxmZ5s;7wca?57dlQwKu2Nnovo8NG zX7&51(WV+#&CWQ&&0Uv5Q>fcA82r+)A`)M<$(jSTv#<1p31i^S-obsHfpOqqbSxgl#p_PUCXDor(&3kvKFauw|>Hs1F}4;=!8MBq@`vRzT;FQ9PSJ6 z)%f1xyWqr$&+Glg8*TI6`6yf!&;lc}x;W~)Rd*et*JWNc;1$?vu0k2Q?Dg{WXYa`hI_`XaH$$$pN=pGou`70c&R%aD#f>%G(uahI`FBpf1g5P)cBRcD%w^f_A?;Lw1b^3Qy#R+=J!plt!#ilT-;id7 zoj%H`FqwLeU=Bwxym0lAemHUJ75dl^{QgU?JW@^^Rfwc-$D_m9jFRG`eyWr?D>IZ| zmtzZ;uYX~t4fYfeqlh?NdWljC}6t>KY1+gYb^4ts%0+P$b-kID6VoV z(Gocr$yw{luBe}z176T@@SU_m+k7#(erQHGs8#jj!fIK)@9>Y~nPM9q*GE}8M&nWQ zzgk-l<&JSZ88cqdZr{_F&mUc;s_a4d&W(7f^S}BfP&s@Xd4=fet=!dogOP|RSL26J z;VAr#M`=r&dZV29jfMKQ$o15F2k#UG80(fWN=H$%U-4MyVzP(CUuVbIw9nv)^ikId!nZ){u5~-ZBC-frb)>@;(nMx)HKbY z@|ROnFPVY~GX7f)jj&BAoev33E0SIznzj7e>(5Va&pBjXEw}R5*bBxN7LjTi`eDMQ8c}vf2su^pjIfU3Se4n5tJ00E)KW^P(SY?Q{0oHB3p)&3Y zo;19M5zq>@6|NV2&y~|a2ZX!3Y~6I@`iP}16jih(s!2YNVc6Lh!x5jKSCx8I31r93 zQ2F8V>_&ZYrPU;S^KUQhs)i?d;0RMG#gx|pXVmy_P>GNp@kr8#Hb^*${Q2Hmz%_GN z8x(OV1!gcAmV9^8FKd!bA~TI|0{-tx;y;$tpVa^VNZ>TXJ)7fEI_}hv!XEo}@AAyQEz>z;+hyRLMa*R!DXUFBpM*L69%!&4g*`|FU+X;6l z5U-n)T`V0^U32;d%*)qlt}2^_Cta6t(Jc(^&nWw!ZvE#Mfb z#*q_?3penN-4K&OsHW)XG35(IsN&XsY5vo{{{uI|Gvj0F@-r2$#7K7^d5pqV`itJJ z{saC0EP5=5Dkh)QYZol&|nXNa)#9cmKx8fqs|QV~1V_49=+je)6}!75@#nl<;&mCP1uA zUw(ExXY9_I_DjArk!E^W9}N<$1cEnjzc&9|sdX>JcFdDlHaf4&cxNq-`$94+rX+^n zn4Jz7;ZAXHOl44v1r^H1^E803xHGAnnyQ9o>Dt+)d7Bfw&8yO zJ9VVimp~q$>go-Rgz5IE(1-KIxks03Zy2~6T29upoiGi%A$DZIH$ z!@I|dV}9mI>n&8UaW_6`3D+OD_jU-Nyw8fCqN$I6kIj3YFT4@(E)#=^^sgLbA`b@G zT83)L)ip$GJsF5b_X+92TX!f056a;U#h*9_b7F_lrfSspwxpWnZ0&Pol~VlP6pht^ zR`Ke+%Q=6HU;I*!6@R2?y?8Km+4tShQYsdcZXGUajeV)2%D6cwpgEBO9KkEb_R@Lu z!v+fz3D%A(g6tD-kgajE*k;iSPo4--eDI%%acZyA>W=D* zB4i?hFk*A4$yD=l(Zl4s z&m>P(CsPsl`@gt?>OKKcnYXkOJOtJPzrr78*2T^#3Wkbtk-+PXuRr&I7?`wJBq*(x zzl&5I>=4Ks{kwsYU?${UgI?48Ge*KD_3} zO~vi4RJIKGrnsQE#;~-`>f$44qmC##oqt(oa^W%EO15xY@)UvH>ob`5&u%>oP2UP(Chj1)@(8r+L z4;DGzFX{ELzSY2Gf0HQ4JJSMdrDywq;zt6}MTLXqF$@eGk?AKdkJ^xo8#6UwqOr>M zswr1rtYG+qSEhSuDD_ZdmFgkunX(DFJKk?I?1Yg*s*lg8Dk2#+zB;ydYfZgInGxF+ zMfSIL1bBHg+=}W(-6-X+SW=FQEPL_s6HGXwnD;8_NP8q@u&+}-!a338$JWCrUZ50# z6(UY@;i9&>aCZyEBQmlfy@&I6C)Q4jbBT0vG>@8*Hxg@T%)XicvU zcZc~)@wyZ^Z>DEuL(s3NX!P@zDW*dB)z}DYMYzAC z(>Av2H0D)KC?vEKuaZ~bukglmBaY!!CU-%fs=)5ZlVxI;NiZ2ER$Kf0Y~0 z1B83*VsZwr)Z?YqG*xM+7Go-BT(_MGbyfGiQ1?qj_ldFA0AA1<7tYVx16AMAdQ98V zS}UIcTT5r@UH#+!n~puv>PGXzm6F;7ohF~oT#34>Ip=!}%2_Lg66=-fNqKGou*quc zQl$Rb8FRX;mf`BG;nbGy89f>JC;@KQ{p86(=1||0a%+XNA$j@+B^G4)ZZ? zFk{9_#ZKw0AKNxkrc&e)E2B7)Bw87SCI~^tl6u)Ki*HwvUp?qBm=cAb;pVT=xb2tV9v z!j_0J@-y}7q-!Qca#QWvyniGcdQ{Et7`wVJBzF=e$>{R)nDfwd#hpA>!-E8!j$ck^ z{dVMk*lqq_W)BH&pZa$7O62P$fZE@iIEKu58g}Z|&pn*;S)RXu4jiUqL`oZdHYh^- zre{aAlTy&And+%XMVo9WeccTBc4ff%C3+k#wLLDdrYXPShFwG_sgdgkCztFVObnsB zfJ*%*6jWmSgbgpe_ZzAlrv{UXh$Gpxh(X@#*san8X|k!|5s^~R!-*$i8oiBkxjiVa zo-zgZ9`&kYp!l|s5NS0+&V12;-R|`4~`nNP&CBM|)kQ?NjQ4UgyEDlZ%$60D^RAHm@Ow1W~8WSAt<(H>`iu{7L zK2)3V=ba0@5b5%5=mrl5KTDxmZ{#(fZj zmNWYR3yv3mAS5rzXEZLWt7LsaU!bt7oX6odW1^|tbM%p8R=410QCpbJJ;!Jsm&m6^ z+9u6*`+(1#U{r6_xsg{0Z$%Nxs7ctCtiV2?#rlVN;T}kJ&&`g_lfsVU&K!ST(c+5> zt$0l;g!wtzufnINmX~%65B-A@K%B>}KnE@^yThjng#t;7ALj4&ZAv6TG=E20-5#hG zAFLgk4NX=-&RwB%sfj+v0rf7>d`V>Vr;W4G9ZY1I=yj?MX@tmxN6Pm85y?>Xykj=F zgy4#65qjm#JA%#mFq1RRD$RYr%Q=ec+Pl~N8W|ei738V!pI>e9YN+LY;a%wh_asU8 zpb<)lc#*OV{mT5pbWF#`b?Xo>LS8kBOSS7>*WDCaDNte}G?>P#!7Io`svSFR+Iniu zuh>&y4C+G7K?jsy;qTUj2462(hRqP?&FHnB+?+LsrSDVAXfc{#`FrR#p-m=PI<1c$ z_YQj%JcYVf#d6};T#1}dYuRRtCBPACnwcGeg0#yP+o07@qrnR!WtdCRYhLbVk|Eo< zUs;KWfFMCptz(_e+rTQU5P|KAPs_o9+N5IH{h;u;-wY=PolBiUdcqp-O)q)nC-|9z z)|aNwT^*8sYRrGfeYu))V?#ZSjL%CDyb=Udat~D-&ox?i-Eu^UI0mV?u<wzdICa|23IOB2wf4$ zZS+-YJbuvQxs^hro0eOHL~*laMniLiUoR{7!SUA=QU$yjehZ=iGICEd~jeqFeVr}GqD4H-EfZL$(8-udf0d{nndiYP@fCu zUOY=Ht#X}2>uwPyJLZz${8xIblbq)@beOKK!cUO+=hNzo!efTQ-EG{$H3@JSn?wyV zv8hUx66ggb@QXulMUcLTd*OM7C6YAJzQA4l)kFk}OC!NJ9Q2ijp~)s-y~N2mvEBTz zQ%3U^bNLP_r##}~az@gdTghF$Vj%4;FgN4|jsGglXf3Qq(=jxi)=GzOX4k^sdRG<8 zne79RlM%+X4ZBU(H-d_5XF%6x@V1|?4L0{231)by5Zu*3BOnf%}T#!x>>Z6+;#E=NDP~ri; zKWBVg{Uyu2S|RR@9#-Ev%D*7VAlzFoD%-xjvtfu*50LoP`AjrU><+ zj`d?)Oi~dWOmsnqxH(3<_6H~HPP$`jXNX;|uA$VsQ|nC9w{ygLJp+|Hqp2~<*YVRxx6s7?k!_p6!{>#*$Go_%!deN1aw)Wu$1KxRgz}UK7d5Z7O9QNn4vej zhz*ue9(WddYAS4#pD|cE7-2;|I{ysm`Kg_Vsxh`%XnQ!=kzxdL3L$|slGJ4?Pu>6D zGpV0S%Pdz}3CpW!f%8kaB%LcYsNoE3k$tFsVXum}Vg?E^)Mu4x&MS%V3sam;cPK_w zl;Q*LQ*ZuvMd5#Sqd%~!FltISj(IP7mF$v+Ny^(2A2vE zDvBDj5JXI$gZ$tNT%dqJ&p?GJ8p7z^`sC=}OI-@&)&Bkqwzdd~@^7QW>m7t^i5+5Z z^aGhjz{T;D;^AB$ULG-!9F4an#Xg(B$94NtOZ&>HgK!1V z9SG!t(0KY!kErP{z(N&ng&j9R;zU|$yyPWtTH%|vXc^5uoOs~ML{1N)bWXDk>}if= z+Dys2%ZRhj2fANdfk8n1|5`Q&1Yl3+uz zc`t7VVXF7r{96bhc{BoWth~(!+x_Sb9jc;gs((!;p+uYvZ}$a?e}VY4)qDp?VwWsz zK$Icjs6HVJ=eWaCQ7ZNbx94O=DVKs?yqXve{m}4+`9f;)ZYID;<`pPIsRx?&H(BhCaTV#a4 zE$TAh$X|}SH10jEIr+kFM&tKWH~-eXf5X0HmnlX!V;l1#3KzxvW(Q_4^r;!UEMO=E z4juehNP|Zg8IR&C5UQsWe4Fw5=LaU%mG+`38o_VhAwO3R=a1w_&D4sG_7#Q5h4=Kk zC6FvLOf6!08e?+=mLJ9EPo(o0FRc(o>~N)buh}jG0c-aIE57Mhi40NiY{B%X>exsBa&D^$u9yQxa@( z2@^49u@*Q{IOlErmu^V?Z6yAVoJbIhH^|O`vlErf-<}>>_Bs_*vtH4|@u6E+zdQc$ z?-c<6X(0)4A)aO{FHq|i;cc(&_q6`QicM`$idbR=@qGC`I8y!#uA*Uzd8emii#oHPL|E1wWEc@46eH-2t->=)kAtD@R7g*Ec zE+VB2_8dzYYqnR;ATQVG3|`OD?4VD+C<=PS`};p~^#6#9DyV|>ed-`RHtxb;!%H!P zGQKVW+GNeEH7PgRI;~M{UY+wkmdf)!!4cE1K1{vD?P=_c)ItP{;FJB+d25S)teOq^ z>``(u4+hQ$XS6}!@{>6T`B6@AVQVcM#`u>|%Kz2Z{Uk2w+ec3Hc!mUm6FtrqKN@Eb zzzzjwlPKN!Za&L}GEUGl;5|gaI+mu=9dPQ+n*dQK9Urs$UUYV5%s#-r2n1VqIeg=n ze_5qOLjwzHhvlq|y{r15P#WCqdVx8OKV49)ccFI<`ns_Mt;4jr%Y;m$#f>8YDg9u)m!AC8Q97?Yj9%`EXWwYxjW;sJ@P zUG0N?q=f7w35xv?t9?57hK{1U11%Cg58LR9;}ylJLUoJo<4>7ne`;3%&oZt&5P`{$4Q5W1kw~~T|RT{M#_pgS5mA~NKrjf5NRd7xgkB730trYHNT{Ji zx;ly!fdK@hOMs99K}vuCfw9m91VRfixP(2i+Ma~>_fI;a1RuPz^x5kpBGr?38l|mRMDKE zG5&z2*DcqJrx&lIXD1;980qVGXCUSL;3I$!13hMIW?!5#yLZ8YT)BCC(uv%2AN;Yr zO_dvMPtiqC=v7e#22ystsU#6*{+5(|B_(DZ&Y-e8b^5$m*QSbC;w?{@XrrNNsHOGY zbo+=qw=(QaQ0rRDF8CB@CvA0PU9oFSsdt%r_AU>uszQxh*AfY?85*0FR*snkJOBbp z>!cG%l{2rZ-E3jM81~jFcwt=P@9VRXS0M^m#Vj&IQzZIh!s4tMCR>x_em^H3K2i|Q zhM*9lnKg}ZwF!3agK0NFw@$p*IJ z{FtRQ$SgLp`F;D=t{^Mu)Gzl#YW?n~?OO_paModwGU_*Lqif?1lFLaWPcDD1c4FnZ z*HETq_adUL2Se7p_6yE)J&G$4Z6It>QVv1aIKh&p*~f}(Vju^m*b6@7l}_$~nh>ty zv&qI&3U{Zx`#$!KrXuviC^6*&`oV2j7!q)V=d70e@NdU$7N#p40k0a8R5LWhYH7KxDY9JQttrpZC zJ04Zy{Rlp^F4WcOAKwr|bxx;_W7Rn%Kwy3{?c8ZkeWku`DHVYRo)be`40f;~^$VTC zb{}}->^l^>cY~^C^~^PS47()dIIkZM=CThk$bNe-A;{=6!rvMl(GwMyigWh#8)v_> zVYZVuujo`QCPU%91>G^J$w5kqOw(t30=)Gl&i{)IiKCcyie?M$8F4UFyOupzr-P%5 zEJy#)FOtJ1DheWC(Do=QEJi9g-_a{bg*0?jBhz$jty2$B*Q+5Fkp}>(Cw#$%#}_Rn z^uaR7oQ1Gjn+G!{`3JbvF%c zL&{;l`EBUf<2;-Dd{*q5{3~d!P10}*Y|@pl2DJJ zeM%u?axO8Vm0zmcvJRN$i&Jl4WYXfsp$=36`U&B4(XVn}53b3emU|K^AEP|`Q#0n> zmXuc@%;p+@Z;Nmp=0*~Rk!f>R!Ml_aGtG+B4MR`)6`)Il=}IrZ{>VC7X6_(;xYst_ z7-G>Bx1x$V;034D9d11Bjwm>+&j%9_LL5(r$qbYvb3&d9qTwQFDArq5BVDKz`b8fZ z@&z-*AjnZ`2`iD3p9|vpP5S4^qmtpLv5GZ4m1AK0p#Ffxm2_K>>CL>ySJlD9IHb1| z!#6AO{P|Ah{SJ#^m8a6<=^ssAOJ>kJ>@t)5&_&X~xKrX?C~Gg57`z51IfwK^`HpNI zC2PgbB;6vrkW_9u7SOGS)-c)5)pYxeJF%jUoW|tiGE)}K$L*wX<=zFRifr+n`0txa zr)ohI%2Xq>34%qJNhc}(9Qq!rKb@5*B$zkXTYGw+^vWhS?54G8_PEy4sAFjbEvz*- z@ZDNM(1Egjxq~A*SS~()xbSJMl7;m38@yoJ7o+D`vWnoTK45HCkrz1Vkv&>I1Fb0} zmg3jf1ThzJStf)ZaByTGU~1&CqDxgLuFlGKK3nuYDG|U^vogS!h6^AhK06c!m~Z|B zx8(1gKu9Zp7+WzZ^mmJI59(}Jybno3W-(4(_wy9#yM9CJh7O(T>_#621%m0^mY=B4 zL#{K+$ALsZWOkcHi6L{De4AWi=)j8u2LWA=2dP+eE$?_G_6Ph5QjrW(CrvaAJiz0D zGSwHPH2oU}8{ry5?a_`r4pvO!4Nlaox>`!dc>UKU4bjs~A)$U+M&P_0hv-`5m+wrb zjx`H(&JGbN9mCw?)HTJ)l3!N0blrNv)d}wg!#yvpXJN$8bY_BI<#kzQEVY6|Ht*{n zcUKNnm6d)J|PlGb8W6l(N0qTig4Nd<}?}Wk-&#!WXxSqBnIF2?^nRa$H-5kEL#jH9G_>wyN7*ZcNr&8z*PKc3V-eCx z(N0D#EuWXKuE@|^*ESy=?#a-YqBv7LYfap2O_=^kQ1wo<>n$_9qK@eo$#ONUp^Dfb z07zI3R-~Q6S!yoXI^skV@(_WEj?p1L4UVA#5}#k4vLT798P$;xXzL5`n-9_Wi(c#*2i<2q;ogdK zx9g=e1g*l_?MM7pEak@HdcJIB=`iST4<5z^uYO_eZ?FB%M=gAY*ZD<7ja|5J)Lymk zv(4JH_nGvmC*!)`jgOCwW2rZvblZIfIS=Hj%-QB-t(;a&Wh}46t1iu~+T_G@W_y+` zUKux7;*OKn#ly}~4I0RY@2+mOH+AWpq%28_M6Obb5iyi7PtIbSLogo5Z0z!OO3npY zINQV%f`LS`yL9Ny_tPO#j*~Vd*3;zE1J0zPxQt&cN4(0LUwV}4qqin)rnl}LXVO@2 z8!e%O9k#ffuGlx}%h^kDe!R)usVQ8MflEfkg&$r}Y%N_J*IQ3z(9t$-bE&u7JQF8v zO)XxcrwA(>Q*}upXU0TJbY_avG9!Ksvh2D(`q{VoRyZaz%ZnPOu=bsV?TEpSMDL|d z`*PnqN&AMy2SGgpA;Q1HZ~GG zwvugx9Z$`shjxd(s`*kgZhF&Ya@lLy!SDH`?-y;>6gZ5@xk$}1ovt^yQLk|IKn)`9 zlluxd#G-XA!wA=FV=WK^KkIL;-g!|@Cyq^nK!KVmVzD_fuu73+I8qK!h_4@;A*Qqr z7__I6$$OUY*ug=R{Hb{dBQkdxhJaY4qg|<-4cEyILMGQi|lWLTeOt%M=I{GavYE_E{c$VLP=bKL|Nbj65uoTS4Rs+-D z7Fn2n(G;-}83UDY=Ow3e$atRRmZVb!a9Qjx`jEzIxfclj04X1Ju@^o{>Fqh?a#^G$ zgwv>yz3}PBjuRW&5#?4v>mM1;9>)D`sf?PhaQevy3+~xJ`}E;d2W<$G?a{S7&egCe zhpu_&v`twXpv-1ESV7IVTYE@~Rb(*WPSLKangpLTMW%Zy0Sca*x!~%RqL-y3!fN^t zpj>$m+gvEQ-BtS_eseAAJb@|9Z|oNkFyJLc&g6;v@y9h#AZ2_Nw0cG6h*aOEX$Txv zT~^G~i7U}Z-{@F^-JRRurYX`uIR;({1e#-wdEAJma;Je(0JHpe_Er z0WEH8vbjTdn=uw~y^-bsLwTv-Y@hU3pUCFr6BrK}8`+Oul&TEwYpV)D6z=^J62H&G zYD_udx-bm>@$iHQx3gi7;BTu>b>`qC;SJeB*$Z{`G11(2{b5a*;40|wQi`%rPqE)` z>rRP~QfyIKFh0W#&5;F8lJir(F!lesqzV(y51_z5MCikLlI<>W+UxszwD3|{GKi?eVrzV3K%WvhY#>niL4J(NCQmvJ> zb*by?t%|2MsLAWgzMP5bZ!Ze=1_C!$bcHd>y9ZVo7Q?IYxcZ`N**su?p>FTx42l&p z0My;#l_Z%HT_@&Yrlh*5qPvkNam_R3>m5UsJU>LX0I8JMqb`YEosMr7#_yfE=*bnD zxVcC%8zP6YHYu*5nEp6@EEUo;m%|4BL^z)VF;a4gYD!AGTf|ufIw0UA|?j zS7~{FK-fqouG2ZW!7Yo__5sUdPDdag;1lOuLY2zz+RYfXNM_7Xg~-`L=PIrRR%=xX zya_A&h;oVkaoG7?&?Z?9c60J!d{?t9`AUxno_e?cHPoOax?^jktPz;gt**Yc`4?1j z7{EG0mPhfO_yv=)Ak-z&5vl~~|LNDCe^ByCH+zW%;vH}m_1WXmCwg(Y^nDP>|5K1* zvO4PR#OQbjOk>h_u;%p4V2`l7nmGD#76&w|s+g6`vQ!l^2KdK@+cp@kJeX^OS!lXM*b|kPR~zk zaK^6+$P&AfE&&1I>f^_h`fWy1`{(8I*MN#Yq%@pP7t(oG^6xIyubDy-oru~~&(6e7 z(*$@A*s<|3tisKw&Ax-jAa-c`+K!dc`sB8of=-iPZ)M-?zGZRniJ}OB!QD|wLpn2A zm5XE#ZxZ>b7I*e$P|$`;kn2zssLE7(X_-1F#u<6S{6Z-th7^XdqHAjmT|y8imbF-# z?rQwn9^2*k)Ke++n{_{QJatrrf@V90h()(cF}TAS+4AREnx10Vf`rFXvkJOG$f&He zp(ufl5yM{4;M+^XKfYKT*5Pb;hW4HIgOksUxmTwV5J9BHRTkZTJ!au9=NKnsI!&FF zUzBRVQ=t$e$JV~=*1dls{I_Aqrju5WCN>p_*w#x+@*)!iKSwpNzwfJJ5$CKO`;@7m z$O*GSU2!UO%xu>urjaY;Fg&0Z(7*{vX@bF$%_tHf7qT*P+w^a}`RU(-WB4Gmlx`rG3yqEobf`?t0lEDJV&zSW4AkbL6DK8skCYr_Ii>ApYw;Vt|BigrH+abR zlHZUb-~amO-Tr~?oE8rZ=Ku2jrog! zkICwVzxaGB9umFfx$|C3b>C6sxBo5v?>PK*66i`W(OB%Yhkv|>UTFM?dz2Wvf9%1b z);}k^#^RdjC+r_cKPd#iIJv3#b!C9yOFF5h4Q~SLL9C5(VJuFN?8&UE%as z|8|QcfzradB?BUg;DPBnmEm0e{Jm0;6=%IRKtaw z7-GUO$wM!#GW+2txi>>eG~`em@oCuA9Yyq-AA)x7tF0VaK1~G8Q9rMd5hQxu%c2Fq zKGngNH02MAHAi2_A4|IbC5u5GtPUeKNZ^`&$~&Yu93Pu&i#~_nO*LR->i7j*ii%V#6uXWIHvgM<|{f(g{V zDok)*Zaxu?KEkdiro)%mI8;1O9cO+LwU}XZVdIf|%(AgL!Vn^p+yH!;DT!ShLr=Dn z+S)$?_;Ng}-kr{6@q3)}QOb!ARKj1Fa#SfXyecFhm~?gJR(X|Z9#@V32m}%GsxSwW zRc>f0^@Y~$B+~D7mz)l2*ABwT_*~NZrOywaI))|ZkX{#qWI*!^hNJ^ zjW^o?`-Xzq#crLtU_2=xHU3%KynStj{(Gn+jfc+ZUqi;fg9)a+%Mx{pFdjJ5QOdb> zy1HF(UZ-y5#{;}jne$$rPVz#d4QfEfMx$mvsG{TqH|!IP*p^Cyo~j=S~XNUWEjK&3c% z(RIy}QKYB5;MgzvFeJc_FTlTO(mZo@C<1i~KbF6i(^Z+GGJ%2wg$t z-}zpCeyI?Va%K{hzpA*`a_+a^{wkUJ&oKx;d;?are)E?3rvL2m53f%B4;V1t-<~GI zSG%fw9hoAxQ|En?$|_pRy}z&xXac2BE@<&K=yYpkRy&E`+#@C%hZd?1(NJpwV*YywFY@JWXx{efEG%MxW_ItX%>8&0LIB za*vS}GE^Y4E--Bmfi@t1!5>m${ls^E&#p=I{GZA5%c0z3JeBA02zghm5}~(@P(AH$4eVZ z@f@bn==!?{Q8AfhMrLcxSAC1VL`ootRH7D*uNq<3d{pPWwmdBaV?<4JVzLO^7^I>Y93WG2axW zB0=`|MFKA*zB5Sx1qx=|yw-+fH~VG*9^~pEV5TS(7;&er&r-55c$40dvI{*`;r(Ks zcwNKLs>iUNmRNAwV0C8Au&hD0Zq8q2Y`gaC*NRQWNPy|aO zgx>}Qsc1J1d)J-R;p0(O6u40H9Jb+t%=!F+Rk$#_ANHUGQ=Mm=dLc!_>t-j&Qp|Py zFb{gG0g@SMsyb?zVOv=SQ&?7pT7GRLvNy9VUx21&<@1b9S^!PnPk+6t;jNgn9FY~h zq@bPCtryW7>iiX`U$T=Y%??z-|QV?6k-MfYzxQ^#(A^ic(5npML2G`TSIM(V6@-O)5`lv&CZRC+oViJeg7Vpxs#d z;NY@PXi@t}+z0O^ukE;nS=Unixlne#`xJQ)ZW7dU{+id1Wo_fJ06^ZF*Skt{`18CP zY1V^qKk6i7t&Ab|x~#6XA7()&&-$}FNUMkaIz8)E@rtU!?!5-PVuLO^%zc@js2;1RC{juTpN*r7?N_+j_aBGeFtM7=sLHeFVSl-lD!_UfZGD|!% z%a@9<-uO?^d>V)j?!dlPig3nGOq`)p2fJs^* z4O*6+ZmlKy)DLEe2_dNnA|I~AreW9p^?xLn&P*oia_g!Q~NP_TDHScA$fFL@V)R88Ai|^3ogu4@ty(4<4YD`(0h^A~Fi?%ed4p%?W=4;UQ0k@uO;pT@s3MWiMny@ezo zcQ0i#AeWD8!)dLz0>tvVR$55&8OXPbPTaIpLG^0m=Hg zOQ&Pww#nFXW%S@cKN2`iF69Yh6F?p86wa)A7`Z5m@0&I*5*V0#Ql5Yj0qPST@5A2e zk68dz4FMry)39Bm2jABjQfNoSn+XjWUv1i1FAO@>1Yq6zb1`IJ^8F+N-g*|>V2v#O zE2doKp{7U(#Lj=ZG^x#(n2*@;bgCbGKnqf2oN-x2a%`8+%-S}YEym4DI!zCF1K7d= z#HgB$JW;+tngOvMol%4K?$3c~nN60BUcJfR<-t_~=5i*oNj#_$Ifjsm zc&BAySZ<)e+b19vg&-C*tH>+PJj7?rQM?qGumBsHS+`i<)jT1<*sJOD=LnzUdZY_XWtSLl12usbLwl~ zd;KG@VNWtKgG#W8A=zJFq1$aTuv8d_OnCK*?~gh72Z{Tr!MA<*W0(KDjc+lCN3DMZ zOOF2ZNABr79q1HCH+8@u}h>)UUdHdSkoB z^6ZTVEQO*PM4P5pz`5rLh3OwPeTyyLvj>}uqV{Jxu;pEeC^y%_5PEgg2g!P2{BC@Z z@4URNF~xtZy^Sj8&ND{mbI1W0@suv%iZ1k2E*dypltNB`6U68(3PC5|3y$ctt7x|; z(oXSfOWI|2dYZ)y0v`Z?MU97XP|vh1EETM0gy0Yc z0tKdD`DsvCP>v zc+goeB!5sGA6@HJ;llC>w;ug{^>Z2 zb=Y(?*9xQ1LesleQ(|)QQo>jH+^$`)=*A<|_hA9jyaWZ6v|rnf=$qDVjlYDG`uard zxFj-)AD`l45b->*qqg}0J;Kl`URGw%lZV9$kR+cK39h24WwpBtb?JZM#s15G;!%&l zkdqI2b7Qe`1f#J26>RH10fqo3iygxn-ftvFSMf0C1yd?7taLAas*2F%>U*<}1fBrR z>Z6KT=u`dgemSSq@#d2{GWo_ZHmDOmsV&4^Qkh3x@?lo00c%f?oZ=r5x2;@xB4^-^ znMn$#6nRLLFA1d__I9LDHsJ88Rc%aJr-z4ARylyI4v{Ts`CRHI@H(V;AY-9FM3(E{ z3uWqNsANZvQ+fnh1(SiAZaJYFSYY7ZffYc$a5JvseysL}@IFW(0a3VvT{8KRZ@ z3!|}iptP|5w}YxmOBm??{$;Fp7^|AQn9CY-bpaz>vOyh^04za4~G8!c1|EGVz2+lr4DlqZrS8m)JjqZsH=8E` zs%bERB<(iIJ??~xnos>cBL-&h6Q5_{V;k~I()^TvvdNm$1!g8X`E_*>XE=cLdLg`FZuA05113Z-;{;$OU0^j|RyO0AJ!a?X z0wS;uttf|Ixqx@9k}+BWsuZJv0Iw-3qSRgB?(o|Bz^XX{?14|ki@=x7W~MqVgIDcP zg6Z1?d3zoEInP+N@7tY?wleER8b)1iew7YUFJi6?m9DstJe8N1@|__i2V0mADp>P0 zT8}eGL%tg>TVYOnY~5nHj*{X_ej@P%W{M8vje(-DpPy!@p?~}4W){G&s<8l*_Vvns zriDaWZ(5k8=CE6#^@VN=gu=qb&y;2H0`NVHD&TNzX;@*iSua0$b?IlfTFr?pPF3HvO)ZjBw7))zcUGUUQsifvi zhl(_QLfYvwfgGc{-1?$dE{n-U0^vKuA<-1Lo4}v*)5> zYBoOM&!4a(eH7-Zn6p*Z7LyW%PidW*z=Q?^Lq1Lpc|yjQ&sDJuo7*_{H7zFOI8bxY zq)_;8T721h<(;M~ENbMfd+k}wm7?O8HSe1^<>v=!-VWfZxl?W^&+{1v3Fu%hBZ zz%oQAZAvjwq%W7FzKg&ND-@L$cr3+aB#NbDfX;A6SZi!T`0<9qzG`hXUl` zWyWyo@s77484G&(nuSF-jDt7=krs#47G{V*nF@Eu|6@FEI3x`mPy-L zkY=51KM(Hh#l~2RC9LAI76B@d+-L#A0sU4vnaFEr?@lFt@QCi2i+y4rR4{D$15L1n zU&}kyT`bP&LOKGtrhhvER4xvNm?dk!@Lz(q8!C_EhGL?>0x7=*&?K^J-P_+2>UwLB z(Un?Ugi0;euvo}ozH)XTm70?}BCl+vcS)$@&a8@%QgjEuUFdHK-(k!vP{+YrKX^%(Q9>Z#Ke z&KmfPL{}$Sj#zX`%(e30`Dg;dqlICT;$rd6ad2&Ni68PM6uM~j{Abx-*7-qZ*Q zTuE<{aP_)@7-lu_C_6+|2h#OQEX??@${mBu?f|)d&bO%xn0j;k^33qHnHbeJDJ3Pf z9R)v_KxY|iYBjGFn(-3UE-sN!Y{D-h5F|tqUTJCki#P?4<}!;dhm+(?-o`C9rrYe0%c3{$286jtLbyMPWiFZh5cN$IT&bo(cC;#Bx zVqWf#7QM4w17sJWZGG=|zy{eTnfG+=E`-g~>N=c8_1`ys-lQ)(X=gIRS}xriJc&lR z*cz3Nc_I}&)c0I4<$9q=fR(r2>n*TlDJyI5 z6wNDjcQ`DZw96@!wsMlmuvm!`E;$nxv|~OyROm}y+NpXIQC!_`M=icPjcQs+!)vFn zBC~dY65d%VDR19dBWasKT(kXlT=6p8q3tQiY}KM^aVDK}cy@Y*$jV%yg9Rj?!Vndi z?W}FGH-el#L#<)b%V%$>f{c17Tg1_+Bhf2i^CaCXmkyL(eK$Is-!QKlK&*VXH~$&k zZ8lD(&qU>%K}X0YEZX#}=F}3B3}%4mYYVrREzA#+@Vf~TZOgh!3o$1oxI;kUw!+yb z@3yaoiq|%LZmYEwedT{pSyLv`=5_On+t;BM0z`ahB4UM)6u_OG<)W~qhTi(yu*7il z22A+-?)ul^cP_1aDsEql(09>6UXnetQHI}Mx^e9=SiPbP zUi3zUZHZ3t(}>0i|{)6O9L z59C&ttX@T4lSxm!-C`^^8ix`2!^Zq{HN=v+!9c*gsnva!M=%a|IW^BQHtgw=F(?ey zjLh2o(He)+4X^h2+<(7^*=0S8Hnvs9maDng#4LG1Ih1%*+#4$l>I$SFTTeUydX~vU zVO$5#-oQIi!{`>Mb{kQHnlXy9^yB>^Ir3zYbHrM%bYf@oii*-=Fcn=?aoKn6XzHL% z2n5VDHk5O-2Ply!2*Hdp@+8c*u@h1n0k~0R>j57Nv3ap*V0dSM=~bkUkjefEg>7cl zYFm;Q(0D=t4-T4xkhB&B+L76I@6B@LjYit-oPW8aK{b8xAkHeUeRHZz0HJOXS~h-6 zN}?{Vs&-h;kq(CT8qF-LqzGuJK$rEQ7KXo)YpM(qhy1VNnY(de5H)=4wY~r%G3Vs$ z)zFhwgiSwEO>F~uCk-xFy;Tn~1ng=unhMJ|LU$|pO&uOVI>23taovUW%&~)HA#L{0 z*R4Qu_(YzaLwzFzftH{Eg>3w&Yn_K(i!Hpa|7@qQz<5PsDabzj=CYRUU-m8zTuSuj z7zL}arI?z3c@1HfS>JSditL{*>9DJ@_r7WHrmtOIgB+s*BBq}AQfrh?)HNx3e=@X2 zrSaqa^u0=Z(iO0dOgSui5o;7b1_%0qmRf@RxL4y%_@BGCyJyU_w_TpVB%d6-ZV3Zf zI#RWN`?>NwiGDY)B(H2F9b#{nS&~xXSLiLRKc>F~Yg>BNlkq!WhqZ4}eu;~XW|(vm zDK5CQI;gertZ8}FnAdO@p25oVs7|F!IW8^B*$k*aPAtP(yaa2gqCrd?B^Kb)%*(TW zv-)oHhjkciQ6DtTTXIi|_QWX}$}Y}j#$q+U^C5hFmk^qt!`-}JjahEHN4Q>WWb=#W z;rnhhfKonqbovcaQqJmNKD&LK__;QBrT3ugN68R@9sO5(S&hV^OYWDRrA>6@?pQRx z4^NcUl~LqeYhf8qvNuO0c<2r^;B6FWVSrFFLMx(8!G)2S+jGh?VKfyGnaHWe3>Y0ioUz`(Blft=9*gg(?3jb*!X{ zS;Nw9gOF7(4OixHZDw)gzVj$hk=(*%d}t8S#2U%;S^?ne#M|RR-Js*_zt1#NlB=xq zP-PI9ATy@3Qm5D)(+nW@nzAv?{)jfW_9>1@jakKI;QwtMQ@(1YUCM?)lr(oa_C7Ii zB}-W>eg3JLpB*Xwp-pB)<6y6)Hx1k^b#CQ-h55TcPTQ=5fbY;tI~09GtmNU?f;b7t z?PZ!uOTFkyj@$(1+r@h;cUzbcbY&CX+Pco+%W(FHxaqO_t;!sHjwJiFS z^y5_8T%u-=pDkdUvIsR36df}tX93@6OH=b+{BVDP_fYwjJeyh0)`u-_iUorEMgz;F zr5yo%owyK**sa#`QpE_%Am9l66=P&w)bmBk&5yQ{k77?<>vu916dSzOFA(H}zjwl- zb5Z2+M3G_@17@+&V|r$ZKk2^db6V?~nYvF-G{mEB{=2sA2#!_Tke^pF%ez1A1h9aR z^4$4!`Vv+he2Gz)ve+KMJvrhsVRYV-7p}K@G%*sunN*;Ws!O0H`GwBzVrJNU?2Kdz zOcSBNG2IXdA(6`L?$Ir_=ZUbSn^AS`&17|$;P8y=k%+2Qx^>eYNe)ZvZ;EG4aL?C1dlMMFlq!5dne7k8 z=eWM(0X@ZvVVIYkG&~JL20EXT^r+e)$MHMVqCR=3y+pEtpMcP_L1aoN-J+y>*Tm|5 z@*PvMb45>g7?IxLMMiDG2?+IgXtx${#ja+)6B&j*-G>refwF6xG+m{u&Fc#a)ly@V*bCC!rsHgRbm3*Fp-R{@&FSSWW4}FKN;^?Y zuc)UxuFe^c=Y2rYUO)IIMdap|8RT6SjtH|QJ{ayOd z`A*0v`HplWEjJ%^ar;7}<3~wNX$GPflj(7tlzxwt=$NXe4J!qKIN5VG5QCv>`GqX$ z;n&pxrG8S--}$V2Xb3J0(pGWl-NL2W6FcYTx!D3ECN=qK$4MuFo8dDbOOcz|;NnGp zc%~5HuIE~u(ddiJ)|N>D0SPvHi1K`6`lZJ+3(lBsa7s1W8c^AJGCyM{Qd?lJ8YU7C z!I(aoVN)WL(p$@1ZijO)79%v1dLCTfj|zL;)0LNZk;vS zRF4m)_>5zoPsuGErv?Nl`v;R(dC@qB$nyI)7@dY>zmq4Bys)y4x} zfa+XXU_s`HTu-UDYQY^m*&p;tW<&Pem%kT<{_ng_oPHYr)!wnD1%)B{3;n>Z&+38D zgfxI5=U!*hDoGeraGz@XP9)(h>|KO44{2a?Tqc;}o`zvF1~_i_szydLsWI8=^agS% zu1w&#x$;Uc6Qr%25aX7W;51YNC;LMocVsI_RTau>Wgw!5>O$R=nX0vbNCPlt@M`Ff zKI@Sm6>Q?8Q#bUEGz@%jOilkWzQvr))n78Z!hiVeelEt6ewZHdQLCck!azj)6hsA;a#RA!M-1f^leZO6a!IJQ^j%`+(3tr|GrPO@ z%H=~m!e`|!HGZJ`@`~;ir{Nqm!H5vtKC~sD%hCHyoGVGD2E!Tss%@gRYx=1d1+OOx zGIUGSlPw#VzH(&QH5&C)(yWSB3YJly5xWm4IV`a-#;V%HrZ&uK5=e2}{ulmp;{wFxwvRHZ;N*ZvteOTQ4PnR9iPxcfPaRGo1g3b87#t1GT=!Lg&JMw zuSe!l2TC3vldO-=e%(r>udYy`o07>~xnHh1hxRrY+s(GFBh;!G83Hoy1Pi5^eedW;Tq$6X_}=iAa)O=J_W(` z%5zAY!H6HJqC%lzraqgiTuT-8t?6REvtuqL92%^A%oJrV=#oFLi6qami818Zi6;NVYYF_#m<{A#6lMozm<uR?J$YE+~o-y{KjAX9yqWZobF_-)X3Q7FjZRle&3b z5xTMQ>)l;tdcQk2g(I3(3V-(=S^z>S3faW9hbWaCn52jcDfh3x?(MtL3$<8_i?cw_ z0w+&IHV*#4RDMxaXMWbUxRO%e%1cdyb0o`1H6zk!(5G*dU#u#*Nrhs|z?~t&C;Kg! zcVbPwVTsb7Nj;)HD|aBkR?-l4Odqs)LwNit)T@}BQ$@DPpxdkHQxQ6bR9MG2+5O~# zyJ^b@k;s_&-&PWMGS*KU>5MiARaiVEE~9;W)0iZuFm5?L4yNy>9vmz!h>X{m?WoWB z{;dBF#9PdnpwG`*wJ5Xay}ZAEBM~Ow8bj{bbv-*}HWu#G_;GYgE8=RR)He!X$jzrM zzs>Mz*7CGeHZ6%9uLkq!dS^$Un#?U;?hvYAUCq23BP^izV?*k>80z>sEI@fal@PpC zdOuv^?Pb0SHtTKQ`}H#SI+t=1_iY!`P%)mj(69*XU^m#$W~c3vwqAj;-Das8)sMOI z-3hxMm58ypu8h*w(1c&kobro%-yTr!CPV6? zK7K#qSEEzDj_{2;f2{M)PGmkR#gX1V{dQ$Or!d-(#!36wa%80gJq#}E)C|n zA37I+pN^!PSBWwa7hS{TCFfBhhT@Xg0fT5(Hn~8~d|cF-`63P8=WA;TZ2fh4iLJge z&YN{&50ClT*f-46q+ug%^=a|XErbOlZ~;-g!CmXtBjp7Yjg}nwoY|^XI(t+lbJXk7 znd8%4Y6zXQio7#NzHw3++nHH?B`HfQgkXzhzhol{zLSqi%uL6a|!?>_rlghsssYI60IknB^*u;`}t%5rS zFP+^OZ4+E8okNDTE%vJ@&g02SdbOGTs zFCpsgIm)BMnbr%YgyIZcML$4cw4b=LEU~#Xf)+;E5Us)$>r7E$uy8$g)`JJOSzZ#$ zS+QA5rM`kkM1fLMmQpX9cTsAtc9QWAL54auC++zvTy;51)`(w-Z zeQLHO;xG}rMa%#-`>QKk9vMQpmsV+Ws2@cZ}lzQ>xL^ZVvvP@1Dq; z3g4W@A+?JO`;Y#Q@l7xfBe5c_}@|bzf6sA z?GpV})USWQ^!{N?&cAN;{CTE-2K(^O-u}6z|K|qo?=n+vX<8K);aU7E8S%$?o9sk* zx$Mj57|4k>0re|~@BYb#RI{>T!l6nhZ%OCiW&>}%EcJVx&cpT3#kkAt9@{~b{mwUW z@b;(6{V!W<~^KlD4)_vg1~ zQ;Uc|733}~#3rjBo;?@6aj5eUF3> zmPB0=i4EljixJ*6_yE+sk0T1SUNx&;`HlTMod;(Ls}Kg5Tw^esiU8JZh2K5c)HF#l zMXI{I9vAEPKmAmsbMb2E*o4Xuhe+xxDsLQ#gVJm*8NMmLg6V>SbYk=Ge4K`rn+2Rg zeE<233>RnyuqJC1w=4trVLdRH(z_E$?X!QHam+ztJV_;p*0u?bpnjkN~oj|QD!w`^jx)C=@r2)Gm(o}y99hCiF0D#{}u z4Rw8H|gnhd&!X%scW#>8Y&0k2pg!c z*lS0JyCQ2BY})X7>DS!uy=6k%vIktt1hl26>l=Vi$~|SuN1Wpy&lXcSzzcO$n_DZR zyF!w|VXwluuD6HXEBnuv^qwrHF)3C?LNG9KDc&onNck;(gY?^wZZ9L@;CH@_v*&Z_ zBKwZDEj%qEyOL=tPM^#*9F{C}wYE#R?f$om|7>`u!n@HVE#mW=-Sa||4&2RotL44B zx{hz_zb_<2oRHVeZrYi%%>`PntS79x9+ZLSbKja;&iaPeo}X<`-eWlJ4gbzpd<4KJ zTOXL|Oam_(nj(5_W$#hbD?@FfNJGPy7#8fn)kGz_9VUnDtMj*|o5FRPJ7yGdKK6G$ zg73L)l`w$m_VdQ#w;Zd^E*>B{I+)4EEN)Q8R2Gg1AHQx`4VH{?N;>oC^Ud>)NuWge3I^+if(7Fqh4 z+t(HmvQn`1-L+-?cgbAzA#3OxX%+p~7ER7&>UTFv;dJ4X&zK^ErfwKdLSJwr*s7<; zs`~Wfr0lX1WC8|8R9 zF*^V`5SV{4q4yOc6Wr*InI2&Gs-{$xA7xbF`^psn=(=azH#e81n{@2{Ib=8>7q zOlCeapZBm}US|XcNte5GMFgDg$dAYP!ki%VeX)1{Jey zM=^;Q+7f%{YmaDoMbdPe+tIel&(m2Y*yTZ5c9&yb(F^#~U+Y7f(|YIvDYuErnteTl1OWh{JQkS$JjJbB_M|vS~xqxOY%lhl3 z;P}H3SG1!?=V$Wft)q?!JqN9-*b8&wU!=;ANbS#A*`mD3TgMM?g;u@`X?XJdN{L_& zr>u-Ij0D7OlG(-ni-^zNS$xDdq3Z>44eyv^ArhC^HWC|DYWBqA)pq~#WEu`M)Uou7 z(0>Is@t3r2q1@-1T;*g4u12TR9SUX7+ct_2SyDQlmGg^tD({Tg?R6eI zpzEl81YL@V?kg>`%L95S;)z^ootT(&fI{i%9YOA}R-a!9hIaN&uC2}aULCO(HM(+e zda|_Kl;*j<+NZ!wNU>3_@#gfI+MUnhcR%k9+krLmNoy^fv~`;IeDXV)e3Ds%HhcZ# zmEnFGO)(snn|U{#s?`YdY#dg4Q(>&=eIvwtJp<|jsC?WxS{})*Uce@>-0>PwwlLvyF!ug9zyt?TnlXP|lOol<^qH8s?M`w4lVwIrP~ z)6XA}qhUK=#pgCwzX_!RAcyq0GZb45A8>4YMWZJ5S#EP|rr22ey@{-W=Lb7{#SauY zB3{xp!uah6UZcv*MEW8SBKsz%m@+2tS$ysh!*&7krVYd@j8j4Pue3J+gXH06=98-? zt-w6|DV0eg$_JM!JjnqAj1eDy^G?d@PHYp?E$usV3R?UJPooRF~W z0~L;X9~Ra}d&Z6Icbax*Oz5d-ma=4ut~o|t*MP29Hs*Jt|*{B&I2Xr$^9YA>h0pC^=QepDI(8J6)b#o zf6>0^@}y}EY;bp5aqyea(l?>2zlg=dM)S%HyQSKnDJ4S8U;!TS#P*ft$%uJv{M=sN z!EADje92whL%DIc+Bz;~1q%*A*#2%U*jl58@0>qfdev1*VtcmH-sJ|{WmZ405{@yn zX(_*Gg27^>Lrms%nx&M~KU;WzT~rykyz#|&#+kpfzH$z+f-(fTVidQa8gUwBhZ)52N|X?jkD6^(#NdLMLq>3-oA0)3Sd z$8F!+$4Y!E^~+b3%1*{B*aUYiPGwSUr+lBykdqsO7oE@+$Q*Z85RN;fFE+%k9_jz` znuGlyD#=-yDKQS|(*`lh5D`iaEv1T|{bEQoJoTY;(X}7E{M>FJNu3C{n|*<@T@T7U z4Q37zf1cdl2*X{m1 zxu`#Gt}FFy)Ei@K%trP*x)O&Pqx8x@!IbM<}86_lZb_GOAuNqH}RC(jlYAT&n#1Wm$2;`8~gLR zp)PaYv_;WKb`d#A_I`MkgLaWdFzShkoBz?ng00|2j+wz^C8jOkf7}hshqAj2!B1Uu zt-kV@F)f+vB>RWaDabJe4bEnCPA#^_(9!&&Dnoz(-QxRiTnoz*GDySS>(69KH4%dh z&Ua?SFJLA%2OLHgD(hu~29Dcc5Zod6>uMf4Q;Om@m&`MEz~QICfY9FLmOd2a$3o~M z{qx_6FUqrAdW7}3R!Dzh(4Yn*vj<)-lSsZ_4yUOgF#c5^Y3r@O_gLy6x{x~GkzzV8 z`tQBaf4yV&la)=My`$T?nZ@kYIwST4u!+Ih|b z0t>XLwV0SNW^0i-_{_hFD*e^L3b(Cmo98Qc9C8I1kAJUCRfj}-JgQbwzX_GYl#-)F z20nA5sX5Zb-pT`=RuK<|0!)`(@lysnkNj*IZl3EjQQ2JgHx(m(>HCxy(@?0`N!;lp zbFPH%%488Cg2z&i#-@wKOS{+xrGReP^Puduvtg?U5VBN%825Jb)ObXdvtJjZr1%aP z?!RKrwAOoBf9qgAzw_CSlv|8`%6h_s*M>>OW34HIfQ#bEABP&ARu79cyMr;O6;bs- z?E)1Y4i{WnBiz{0&n>@s>}aa?2g1?GL)!)S(I}&H0WZZ*zd5;OaiIFz@4No*z2yD< z$-D^c83o|m2e01a1y#CXF z{Sp8Dh#!1>bG~=_kgQ{UC)PJs{Gc4w<5Eof?|f*OV7B`;F|O8 zx%e?ppv?ogwfRWsm`f;a*qS7eu|Z&keCx%qrSjA9quJT8bjzC!TDi>k&mE$=y@~GN z{A&=I(oKy{la}$A{??bRg-7ZJ6gk&~{$xFc{xUzMvRxW=!Z}94Zc9_&`B(#r-^)z{ z&r)V)Pngj(mrMgnr{gAR>+q?LKaywZhc-Jr(`u{`5Iywd3pGO|SQGUKJ`b6C!)RC` z-Q70iex1l2R*QQ$N;i!u0^%d;%eQ_SWtx5yDguxZU}ay-D9k>lh}W2vvUPV!fnwCR z;MQfa-SiJf9J&ASvbT*6V#CD1H;FBLmGnJ64R2{Kf&^ zl$mS|9|=(>&3G5yQgIAHU}(w)S-)rak+v+N`z5VH2&5d+k9`FtlLIuxELuD-BTRk% z%3$Msa)fFJm@KwG=%bT%HQHeGW{Y-e?C(NCKXQvQ&RC?Kya*)Gs}Yy6xbMg>usQIh z%+l}!?bkcC#TeS{-_>wZ65K=#s}l_dUxix#DkQYa{kI;Au(um75dPe;xAhAv(MJS3 zbEqY&2-TTzmCKTUo@STTQ;jCf(CknevrYA2;m5$w*&q?I_VC-{0JU(ra<0>Fw`*C6 zrty2T#qpTX_TlmJi_-xOGg~G1-)b#GWWik{pEo|$whi?}l40rgI=}sSQX$@KM8-it zRZ&YmBoe@LmNfsi`F~vkOsQXG!xz(`Ilc$$x~+PiHN&X9QF8fhb!B3OuAVF_%|QCI zgRQux=WQ7FM=;hD9Id5?(~pGyc6S{pzR`RHKo=q_zW$9Z6#DDAL$BZFYD^g{!hkWO zfBZ|kSzD*w9;~uKtJM;b-Wct%gE3ls{OQt#Ek0&xEF*@LxRw7u{;=oe-`A2XYvO#s zlT>Q-*HwdF^Lh6Oz!!?Y0EDaj?RHs$y_iBO+eMkED${3g+I3KsXr_l#whPF-w|rKV zkOzo@-%6>Rf|@sD7masUiNBh+b-4xq72c{`CP+MKKN)Z->xjCA3c+d^e>T)Xf56 zVMFn+h68fu7s*Td%;$jmbS8TKxKCgKNs(A0f=m3#v8ujvs)L88;KD?_9*h^e!R$FW zud7Z^@EJDia8qV25Saga#DcRd7qr%y@u?4*pK`7}1H{7uV<# z*S`sgDk38iKfxCSq1tmcc5X3^T)-_5d^uU?Ox!8T;9xvJTrP8Q@Fio0_grPT({CIF zBoY-*(%FCxRX0@uNjSl_UY)4kCE`=%=;D)~Uj=LXPT`f7s?oRO7UX>g7b&4e!nbZS zTR#_Ohc~=xTp$O6vY|VMJg7egMNeCA!(gYW--$s zs;1s#nkLpmJNX37bL@BNwC$u>e7i~ZvO_P8?4ZDh|IO^;=dJa$h8ICmhx@bN!Zp?J zqw|v@!C)-h$Su0okiUADp=kvKy)}Pitf=g49W?UC5Ywa-fr)d=j1Mjw((Zbl?xo9! z&$sQQq7;9lajKq_q#nB@xuV%+X3aPKAfng8Z9BwTUgZy;{zK)2lIcJ1#MC8Pb@Tf3Wom z%NE{Bgd??caB3XKyp4Mhx&T5`P)PF{FQCD#ivw+@Zb`=F1_`BUzf}B~k#PMH!iO}` z^gVb~)rg*9Y4_7x^y{PMlL0=6VP1p&GH1d^Z$2``-Yz5|uEI)9J6fwT z3iSHPXqO1%XRuktfhfBpPx=-chZQhhGS_s|E=I|bKf1?sS~EZ*G{NH`2%~b@O$t<& zV~I~+a~CPEN9M`1tgL)ve&=bgC~c=4THBpsHJ`IeRp5 zT^D^y7oZcvZktDTj9xAO%}L|PQ~;edU}YBg-AP0CWq#yU*cOcEpRgXB@R2xQjgY;j z7ei=vng5(zQh*uqFV3&91L-f1JdMZF%v33@D|Qel0wFv!*e-8>rXcqQJe6Cs(zzF- z`I}P}kujOz$U<}6Kqi+jgszoTPK7q~zHs-cV>2zp*3dFH(MvReGzd0AogJ~TV1mz4 z6fx9aZeJPv<$hpd2NX-mNkia*hy91h^QpSmBKy_!IH~O{C6lHixfQo-IxdM39{sYK zmK^xoW*@eXgdbQHW*E;^Ymi(`;B={F7=~w8)w|H7jOacs|+90;Doo7go#-S}I`$*%2CMcm$m@fe1s zo{i$HTnj=ZSyPXTOqP$kek-rBL%{6J-kc4uV3Fp0-^m2@R(!SMb~;$dnz0hxL4a>3 z-e3pa?DdXi&=aHJ&&4Tyl;4QZa2Kka&?1BRkR!v2D7~Igv4sBxU+Je@u=HKZ~9w zBIv1*30iD@eRHnMj(^4JG`F2+eB9H8Lk+WAE;ZNRsC8tB)Ds}VDet*rx+r(g$h%O7 zN?LNxMTOJTHV)e@jl8(~2s{4^UR>0XE%F)5c!K(AOL}eV?V)*yKu}bFPz4hvr+9 zc>;^Q{>Ei`xc;5(*tHKKUiR&avb2$8obx(Sl-5YYK}$fNGs2@gg2X$|0_CV zC*QJ$Jsdv~d=|Qek4wuZqT}yjaS`BW2`>#FD|{3BMP>GOErtjFfMK%TXy9|%76=so zdQg3bbO4R5hUeJEjUwe^+;$|!8VxenIq&y+9uFWnnu$n$IYsGbQHcoG*jT$6EeLRE zZ@8JoZlc=b4z1Gy={y{eW(T;3{ppnAY{1$|`fjv_Yb!f2{?n*5stP_Fx_2e73`0#; zT7PdkK-1FlOqRYGCb;LciS2Q@{_e+;XEm^foWkTcw1Kdq1Nz@9KEX5ijFJwLraa?c z)wUmJ7&!ozQMq z|5|e6>%H9K-yIzfN&z#a=+rb4l4J9Tp>q~i-(HubZ;@@LBsT925XFw){zx{!1CP?d>}xPI9t0t4N%a- zw?ELyISFJ`WOeSLWgXP#(P16>FeN;M5nn$W5|UPf?qh|!=re||sf6K>FXfFf;>lc^ ze?=8QaGHpGx*4kP$kHb2g0CX=i7>^4Sls(YjEfc7TSJh;>Hx_qqS{WI)n!@BkJsjT zVCmhY+vN4K5*wd_D7znX7+P|x$w1MZqz@8CjwEXrhHN_))>e){l=NXb3DIkdj#c{y zNYYF|3rW6D_odzJm9fCWw1ed>zj8&lL}#6 z@wj`|jEGym)~N8r`EKQUj=A`}I96)1=2FrU)|3*v6JML#V%v1P&Y910!AyfsOH7iJ zdJKAKfUa6l8$u+>dv@t*l-?JwZ$c*b6mOjH3Lx=DEa03dHJuwt^q$uoMAH@sDl&Q@ zvf?{Lzeykl!kTTY*E&u+Y?aOHyy7tcqL zXSM?<%$3SJcstUWmkNVC%9DYsI&4^k4sD8twU&z7Ulb~_RxXmHz+{j(b@<%VX{slKh9@P6c0v`aoFqiRe8De9cilJhQ2vU&59G)7<-4b%VyObc zeT04QoQLI4dSt5$KL?l<;;CRDeY8;)W?SApoE$M`)TmWjtHH+A`CbLE>O6t?c?px zU$aj3Bd#B|mZ3k`DR)O!HFkXbVm|ax1~`1>^WyO;=@- zOAW7Daxj?peR^pn_>+$KKfx!IOgKbUxWntuMRBhM2JwJ19OPGnO%fOC(dhUV(K2aq zoMF=L9>JcN5P+}RmNY{85ni*MW*W;d=&!U#zLeU&5IgN=)J6^$q* zcLuB+mHWcZ%(B(`CM447^F5w7fApzw+v3Ze$J5`0WC0{}m)_=S`|S>ln17BHAFX8o zc+O0Qr$_}hK2zpgH4FMwumUK5TJZ&&@-WAJ2 z04-MOq8Kkg9V@Ifl-&!BP@EB)WZ$g)3JKHMR7lA6rbB$_b|Aejj@T4Dm8v80TX~ODXz?fKE3coX z$yt}G7x<(3!g?ZUK?{05tS2+1ne#l?0b|DrxKqkOfo6$Y1;f8eoX%>AoV(XWqIm`w z%zZb8*~J+;{a%akH9qG0O9NR6){$2X0bb)H(!rc*p3(~+vk=+8@G{AOVjP-Ku~ks& zR5E&LVoY20(W28<7`=oHwyYlm?#|VAZIyF6i_6O0ve!A+dOr4* z4^RhEIJOasFXXs9EvA*dvu8E@a(RDMgu+nv=P%L>0gf1Gxw93N-%ll>`qK>?5JNeD zZwhV`5<_7UHy=!H&J}c=Uz~`Idr(?Wz=WMaUZ~bO5IcL=nx}cq#qq=^MC4ku>ilR+ zRI_1-uPABmaF^~DdhW2Xj^{};c?>x0>8W_7$tg1H81nMW*^&nk3n%4UJJG?eIlVg- zL-VZlkgZD(PR@)?U@gw?>%#nLiG@LzHb6FeXei`!;Q%e zlI?k|5NJ>xv=`b^W~Wi&gUT^LvJHxSOniKDl5*^%he^qZHx<28p&g^y9Sbr@CA#f% z%OOzmoj#~yxEoCTY{AsP0fJqXjF~YXwByE#_FWo)J!ge0=Db?qiu=>@z#WmS@6&?)n~zNaP}pHIJn2} zp{5y~>S`7|6)Q(#JnM@+1hZ4@K!RmDjkXc0$}QojIvO9LLCa zn_`3XxzNI=6!cbB`zv<0M}VgqQ=l;g$bq@e(9J2OsNPW0v#;en;@PorgRy&`T>;$3 z8p&9~#@E0ztc-H#(bQJk9_>r_LXPsm|a16pxIXewqv`}AX zhid51N&k9#hf!~_Sb}A))&3q30wg+`q1x!DTUqu_siwofpsc4IpDe2uF$;9cV`FnB zUZ2XoH~h|qA&aN(MEM>iFtDpgzEpR!uz7SD%g%!{AP!E`p}os0Mpy@wA`lF_YtPz> zNR|=fr2SHoJ?vAhEI{&ano^iz!-~5>#v2%4t#aXicknUssY8Gm)_|AUNG_xwtx*{L zH2Egg%NyFB1xL42vL%+_b|TE@%_h9VTDafR*KKc<4}=>)4~3Mef%QVJI6D0C#2=PN z6O4bDzZe<4gt%Gn0pi+Px-0QYhTHL=mgL#NNWcg#*YeaWzR*$ zoT66B>J^7n5G@4_01h6p*rcKY5k0j&eQ8&TT?+q}Jq9(afR3G|-6|DbCdOyo0wITy zq&r;b^*Zj*+}U8+JCFd4wUkmI5^*xZ3p{&H@fUhN@h#`_1nv}fl6$#hYq5b$(x?S& zrlQb(FFj*S1dDX!Jbd4rSB=F?m;}WIDw6Max%0ep8)4F4m2GzeRp5Emq0%7cAiCN~ zf$tMVf3$NCrdvWC7#LjA+9FEGqShQurzeIz^umK`2u)Ls)ZLXxxU=H>l04!y>g{QP zfgVU`-tRyT$}cGD`kP?M*|zH@@D|jRVp7yJ<;tdOLZr>z(GjRbzUBR7uMKv9`XLjy zcsRd}#i7L|vb{6?Tzo|K>kTC1h_puUkJWw-HB;op%38u9tObhpC{0IOV)q4(X}&h{ zViN;_`*r-&hBq?1R6MFdiFnr*%84DF2Wa{g0@AAJ*2bZY!Xe(o2?k-3JTy8eMrpQB;+UpBPMYwP%7 z>O%umX`WWaRyjQ{_qBd}+mlKEuwERQ z$P0}k^>(;4;dACLiTjtPC`z1w&O#=Vbwtb;mhj+mXHpM6Rr zKfv5zZKZ(o>H$s1_#UX?IsZzZ62xyr?{`6z?dEW)y$(UZAcmVB3HLm3Ui*8c58k@o zaoFTm`6Iw%7^G+aJe&0dEJ_IMO{*a-F%h;60^0UdTv(Bv&CR!3eo%wsAxO-_(z3H6 zk|tO^CT8;-kg*h2EJ`#9-Hx1=tvjuf!k+i-j$M7!9bkxQ=+zPu`tK60|GO^N|DUA8 zo<5IAa=e8bhVBoAcO0+iIOy;rtE-(&?*;>|=hEQ{L$DxPjt2aC8GFGjw28)4@A}_e z@;}-3AHm}P$?5(}fBNeLz<=rA{j`(1(*jexr#_N6;C8$Chw?BZm+0N!7e7A# zv2$=c^z-5G3&4p<+kJiD{htfKtm*sSzyH5O{$Gmy*Ss*N6HTQf810|M{(Q;*W6r^U zEr0yy_xj6GLI3%``q$6!|7Fr0VUYR;Y;1y12W3@Kx%nq-y3RUHlGWJQys&2zHTH1c zqNavs&nFsn;!5F-6}`~c)$n$3-q!CczyIs^@efD*>t&jd(EiFc4WCdl)zUJ(M)$zY z&+7-eY=fatVd^lAK2R&07{l|sYcfnO_dcu%gjSu6Y?d`9isL6HDEL@L7|=1m?Q?8m z`JGDRmff0O!{cj|cavR{YR{Emw{-J9PdxS$aUbyZCnBTHm!c`)(b;=($V077U95Nkdnl4%&Tdn)RwkL5xDmR!Py^<9$r`4ehT z_%%MWosFigPVPaARwnm_9p3%yDz~PFz#0%Q@S%ENH^FL0sP*R~(^iVIX!RmRhBfB| z?;vh)o{e$gw3HVhqo8^WLZA=TOuVeq`N4cw-;m?oyuJ>|t-iYX@I(GA5PC3+TK5F; z6)A_%7aa_Xw#vPy-Pq~0`QG8T%^$}rqEw&y6VsfJWUUOmP<)4g$q)$LDT)mlBZx3j zg~9A${E){938imBu-6NKYKVnjQ2^)-DXh5#%aoI*6)DrIfk>M@lA=-Fz7>wB87~cT zW@XvH!88qAW^Jm>6)QPP;mS|0N9Bx2RZf{FguICp79OHyE`$jSM<}o8 zot)nLNoNS)>+{=6m8Itwc zB;5dmK}7q9Ku(i0AviQ^SZ6di3W?iZr z;M}sbxk8GeM{t#+)_297Pt=;FLuZc1DjlwxrXALEx)kVTvgWuvzx5htXe3HXLG7Vl(A52S*vG@anUm&PzZB-lH?GzRH8Lg z3-Cb#=s=!>ZqwVe!_W|x8YC%|c!ZGX%D|||y7^x~r-p5vYW6-+Gh;tQE}`6aIB;|2 z5rJ0S?n%;CxQ|^0L=xo9Ojc`ch-f>F1vMNL{7aN zqwE%G5y9zTYdYV+vUR`o2F6@IZTNH4(IU+sQ+XZya<}t=LrX{8a}Rx@1?cT3IyUZO zn@rWPAtnw<{U;6(R4H>MAbdh{_Gh1-3yByqYGN5;+;O!QafNw2UH2zV7QEX+jA2d2 z5%m(oN;&JBm5b?$bnhXDBrVx1PBr*g`h9r&>XhJ&CYmTeqXXQa5d z-orinbaoAA82tg+iv%l+6jcrQ4qXRvKlO}NjIjNV>4;5S3pnqGn0qKEl`wzi`0q-G z>!xdz6ru~L6|FCxR;wd_Z!Ar&d~O-Ue#&fQi4287676FBH-gXd1W2H@lJHpLj5zJ zdDP69B!t3+^Q84c)dXyjee{~z*Yh5cD$mb0%5@i*{R!#DF6hl1h3-VD%qb{;(Tb8b zT0I%#bg#(SM@jFQreBw$X2L<@qIZ8}O?#1afq!^*b(c%Ve2X6Tsr{MFYu$(&Lt05S ztx)d=3G3#ugrsVO`!K#3YqYPHRswfqhSz zS~V69vHA!7dE`>s(#Qp>I9dHZ+@(K{CI?mHce4vCAA0Z(camZpXJ*g!GgV*c-}nWNKxvpDvhEP_&)uID(YWa`Rgr+ z3XB$13Pd}2ztzD3+SlGs)4hTa2DPPHFNJEO1iyBasH6^sD#=<)w2#Wz(^C|<2GVeZ zP<6vH*RApKH=$bQ#=}uv`o_xXjnJJ>dD(lLHxg*uUmtFd#MBx7eV_Zsr)79smKFud zpX@svc|}&c>>B$0$CZPF9x-XAmf!F6sE1TpL#2EN#25iR9-a9yuG>_w3TUb#ZF~%j z$;4xwA(T5Z=ue37aW{cf>el{*ruF6%>1;=5rX`^g9S-$?x0|fBN3_k-lto_{6d|GW zh7Fi>M{n^>@Ep3Gd7qhn#0OalsDY;p!61nQi-pT-e*Q+)}-Wj@RU&Hn`GzH~Qs z4mf<~05Kk*CrFl58++=#+m^_6RQ&bLc$*hc$?%_Q;3koP$rxoo#a}xKYIzsswM9-S zxs$f?Sf;$CO#R;W+DMmP{qF!dtKVu?_~E{AMO$l=c99H0LI6(Pzb2K3^B>R(w{WKv zE^3KuR<>?3$qD-mC?~CIJxmRyydiRWP3(g{ZGBX?2tQL7HP! zdn-dJk=EYcC3@7bjY!=V$zDN!{iS{f(>i9Cf>WYrU-tfmE^vY?SB?1$S>mo}!~&$1Dy}?JRaX zSjKT{hhpjXOl-NXH$OQTM1K=fM#t!+tfy8s*pMEK`LjoqME5)GNk2~b4L4Gg5FDQ5 zl(Rm{f8X$jMnCy=#kO@Sws&MH#iTYdO$Q{#u&wKP1KE&%rdNrWneB|7jR+w&kjDbv zbMl{2BH6>X;vQH=_<4VU;Ah?KJF_Q7+(3t2_M{%*59e4&4xA0{h^VD^zm!e!FLn0Q zJH>I-lMM;qPo>hF5E|$W+6e=`wMS4JDyz13Wv}XUxzZt6#Zn$P|AymK{=LpVc+^8rJ<|eHb1^D)Z!d%O@zD(ZZ=<@e(qgdpn&|5@Z$-2OL!$*uuhB2EyD=;RXJy6LGn|KOcoOBwKE7VlG}N zhg%M2OW?>Z$>_NQTPi%c|B;^vm_~Y>Z+&&FvWlfzUOKlTe#Pco;(&* zSo}~D72%|=KFWpIy^T6tvbKI|@O=N)VjAOimH5qSEbPtvRneUB(oP1XOH>R^I2B2u zXmRtB^RlJM+{*r|Z@RSWb6`^^Y3hSn`YSM z9)w-xO{Um#5$^)pIpyfc`maMGTy1280a9Mp?ONFNnOTZSM1paR&ezG5vfliC6jQQB3MH)?d z<&(qmTW9^EcO#c)2?0U*ah%TZT_^M+gxjBV0R|B67ctW7ZFYV?IW#(A+ULrjlr}3S zJ#VRZl7JnC2*ydk+h zY^E)<h=a-#X88<{H(?5WzW|c}SDAXAx%O%(0_HyU*F0Uop8o!U?U=vx8yJ(+)+L zZL5$%`PReHDMrL&5fG*gqN(<6z?;~$>Dta~MT2-q*g^XAc`jF=H}_7eY?>2r4DwR! zEBI>1U;?BCcj4}xA_Mrq0p1mv2P8(G*!8ZjLG6>Q;nGgwh!rKP#ipg@;oQ~Z9Aesx zRQ>V9^q@}6+NgE#vh~!wZu*vK?mKcf5ThXH%plR~rE2I0U8l8EBsvk+tP)$VI-<+F zVM&z~&)&1Z>___g6O*v)wTF~%LWb$e4U6Hn*KBs!^QX=5+-}MCT=(WxJ<;w;i;Z zFw*5y7Qg(fVG?iTrRQ6W?oC!2`!N=7*vRMJG&3D4;~NC`!&dZi|Fl3UmpUH@R6<8&fkQh!QX^PUVs1UZM3I#iBQlO{dC6| zr~;yvE7F%Zr(eeieqO4(-?;IS`GuVL?_XZ*AF^2-iQ=pgwE#_+KiaigrdXg06Sx7% zKDIrx&A)f8SoiXVov|HE)=IX>g4?Sp3`KNVV|NsefxpvvKP*f*^VY(UUyF!&FV~- zAcl5@koZn8Cp9_Y^QNCBw3W@#?~XEs3}cC?PfJ*>=m6Z91+ol1w`Rl z*1zs1*W6jD$g!-#-RT{d1W}_WjpDOT=|;BGZs?@0tDCe2JC;E(DhY>$4yX3WtgD*L zoIGYQ%}s{9bOHuZ2JaVzze_0XQ`gG3L~xKj)-7z7)=a;)GK@u;rvX5N>K%%VZVYPH zz~C6fvz7~MicgJ?I4!PH%4mBgB`(^dWbc<`JiHR zub$ey{=LsZ%PZ1=>n3o*TVWA;Ol{esmWx*K?Xd@e*xSZTd!gM?QZ6>f+cc$2I4ebF zuh!ig&Up!i_=|S1+A`B63ZtA2oMul;N715D%y8$+=4qMPp$9Ao--P5Sx10xOvboml zupFT-r_&nDG|w7~Q?x0A4j@VhfmnbE2}wzWt}OUoD;o-7C&GcX>vE47~zGSK95 zLsyo4XhqbN0TLwgD|}Vx$5|A0deWDZ(y^y=`g2YR7WZ~6s20t@HR_-h6}6*IpUdcV ze+e#%{h=YUo%+!i#_5!?nd~=&z-_zC$-fKDK;4#({~YM$KXAY+C_z$F#XWjo(H-Ej z5|V+-l4J*Gu@P>VCqf2cL&Q)xOm`<*rO zBLmREjZQ0#$L*tug!Tb>z?d;=Wg#d|y4po?HsAJ9&a)-EJJf%5dg*&f6jCvwxz}SQ zA!i9*k*a1Mcm8$8h~4aBx2Hn~FZ#{9OIE85PY5oi{nDMbT%@9~NFPQ}g!vj7LWy_2 zzGdnR%PV&)L^@~HKJVgp?;dQNjZQq01Tt5aD)!}{@vR!sIi{P@_KHF#sI!L}-bAZJ zhK1U?D7^nyt@)Ja1m!)Xcxwlga%j3(#xYZV$j=|bNyDLVS2SncTPifKg7r*^0}4NR z)j~>c?JfVE+aBBTmyvHtx-ZC7q~8nevQr_iJapc$L5Ty z$;5KKM-$j3#nJ58tmm9xAol+mTKpfXoB!j=KS7C){l|bw6JK6*pQeZekjb4-A3c?-yNYJQ{sE~xAavmS_5J0+`&}Pv{Z3oCu5ByPmV9fh4xMXu z6)$B6J3pGjy#gAlMKdlsewGJa7_;pmk&Q0-B z$J7s~+%>AR7rGR){qJjr+i6ClbLlT~N6!hJd9q*(@5Rh57=#`Yc_|6+yzi_ghk$mW zR}lQEUyl4u*gVzVE0YlYn8JaV_gYxk)xV09TvQB4<|++`px37%9qzwXT^vJ(^sPS$ zH3$IrO% z(W?mcM)ULF0^K{a1r)%a?Q_war~LJz(Qis*fJ1 zEVGU=#JW9MQL!MYy-ydC0beDLT!$gWxhq^Gj0F`ds zf-~w$J)oeV)Gqa$r@@zG2R0pTe!=ijz1jsa{o>CyCqY=s_8PpIvA5Kmw zt2R#M1^6$y*W%LANliG|Z`EdHkv=tJV5xv)SH}_V2iDC5|KR!rYOYxY0Ng4BaNt~r zVa>y`&)(LH_uKZpFV)3Zd1=A~=irhxGCNj`43dY~abA)}?*+Ce_Kb(3iqNM3W`-Ej?TGMNFoLo8fZP4t(sMIml%_G$?@*9=<@zS zMvC)dU@<~C)S3^{1BtSZ6WPMe(>(~ zjc*rzs||CO!eWBSiQP>Cet7xzv3iacreiM;7up=#9-E#cs)SY1fV*<+by_yXG7bFn zRS>3aXRlPx-A5mDIr_{$lGWR0S=!fQQd^N?Lkd6KBv z(9$N%9z7>Z^MEvH8_KKqd<9I(TmL%_L7enH#n=>#H5{;25I!rRLaBW+mCwr($zeYY$?o7XqvQ2 zDaevGD00~IDMR-f3Cv9spYz4X@}7vE!`G{2XDm(T^y+!A97h-J-#T9n;i@sjIsAJI zfT6tYC;c-Ou($njYUow$ru88itcC$eY2uRm6^5+9(8VXtV#(Yi&3 z`0Q80e^`f{h>d3C)!KkCeQlTf>F(w?LF>GuTTwDxMlD*L~&_ug?$ z<%_yE&e#S=9n^?|;7BJC7?6lG(J`SHL$U}Zz$irsJv1Sd85I=}kP?uRprIuJLJbhA zqDT{v5_%CK^aP~}2)=9fzW1E-o_Fu_?z``~f86sq`6H~gveqJ5<+pz2d7f`u8kCl0 z5E5B9PRK0reUfspGzayi9u<+aJFw1>N% zZjIM{ndipoENc71Brr4Ls|^uemBzY@$^zxop!d6fWIH--XvYo z;|1%+n^bgAC-B;Gd_bM}6D7BG(m*TO7VGBJu32yR4Co5Xj^6zV`aXc=oK`hY%#6oP z;nmT>>Sx%FG0Vt3X0C?4Db63>Dp!b-9FMHsWP7)ajB~#U3@7=#!ppQu`u3Bma|bK0 zQj8vS66+L}=SX{5CD{3qtU1M}pJwo05>#%HmqS_|?YeBu+)k+YJCEvEc}8+|F2Z4O zR_)+>@?SRA-&;S7mV> zU#YH^Dm72?4Rv=CIyuZRDnEFSUgBmH+7>&h=T`xjc9_izc8V%tpOh!vZ zNp^0?e0)gB&hk^ zlPAGvto~VoNA3^u<)#Vu0$?&xst&^qQD9;k{WGlYd^R()>R^Ni=#Bb)%-uhL_;HxuVd$E+!Roa0&}D4v z*U@8dS=ub6z+%KqfTO2+dwV4cdX(9^5;8$MPiM~~U9>N`Rk+$-*F)0g_&J?S)b?&k_SI{$O4_)g9rLPF6b6V7Y$Sms?lfs55=WlPU=x9xbGQlsSoPzi1ur9m9Su@c4j>3SCk3a04M%9jJ`~Ek zHtE#gt@R1~Gjh2@X2xS+v+UoRqgreDD6TCEr7rCbTCF+C5SYT;-#*k zxY!HZ-H#712TpsfG#%$o?K;ZZ_|{&o0cjYfw50yQ1Sz**$pOJ7VkLSv;fV7$fr~fy z>~#9C-)q^rv3DBmp-?d!)_Qi+)s3Xv;!#nj1BWXheb1uMQboRjX=~${b`Ckr)do{9dz|3#^+VO9E0k)xFCVeVGd3+g1t+%5qFk>cf=a8sVQVD)66FWbgBB7w*=nKbw zwq)ul>Lf~pmm@0b?)nEbFf7FjoeuI;Ex!VH;LSURAGG^<$}dOI3OJq73kBB6)U`>f zkLbp&W0(6CGc^|5vtw}0-+xWbSZ&Qn*|uq4R(wo0oRN8dGNQAMHWR+{X0;*TZ>!Ww z>|#*@606O}s$*H0ACK5O2wNXdQ>qY&dV8~fpv3%#(W|sqSxlz@utu!eS7b4j37-w7|Ruxaej^R7N+dmb`J! z)`Ov_L7hA`5FC6L?>bogQEs&9t#1Spm}BJS8IP@dFlUb{R7~*0TJ{hku#AP@~$Tve3Tj z5YP0eCZpC>WCI<9hy&+;q@5s>23_6z+3UGflQwH-Mvro92Y)1ywkRS?Og=$!MNej-U+-X&JsdC!3-y{nPOMY%#7>5XgZI$6$e88*bCk}|oRAr9 zdJ4RS()axw`FfToY52RYTfMzKJulzFIk-FQ3}mCyN0x62Xwi7gj~xoj?Hx|IH$A*_ zTklllTX#Zh%G>%xA9e(nQdf1^jbRw$R@M%gX>CBkPeRIGpI4Ky?hBV%pcR_hwkOca zhM1et_vL8v3@0+^dxT1cGVR`gxxJy4sAhwr1@cK09d*iA{bWC;v8W+X>hL@s1^@}4 z(VkW6v9v!%m=-!g!pZ0(AL?p5U4cv8@ao*s5v#nlcQ&6e++KD80t1R9W{`8wjQy82 zRYfoModU{N3p=~@&?kpCSnH;GR4P{kZo2UxDre+$&2Hy%2!G4Az9js~bYYvUXW*qc zaim*bdELxTP*j?rM$D#uhLxj3d`WH)yRBijC32umI>u<1J{MWP0N{&C#H)6pehQpk zn>{mg6C>DUYNwwE2!naN@I*=HBjd%#EhP2px{FSadtHqyx_z@e45jlRprJ!} zy$?3Rlno;f9i&v;RKLBl0q<3>CAAbz5QwAGn=*nmMpzTizJRvsJ20o%7ttKFd~GVS z`F5VPf(`dv^?>7Ld^-Ac@W}1U>bLHwDJ}-$9+SW5)`&IvXZdm4c%TtvGMlDb5uo9E z8ve#uPX@>4n|8jBjB7jpi7~d75LVMhV};%PCJ0FSh?ruVjGxAt7EyVUR)h$lRGgPO zdo=_&xBSPUJ`Do*c8$pmtkLCFJ6dxV#75#yVp<*h@KSE2UyV+p@c^pOFsu7LpyBBw zp$<727)a5YUIfFPTIyrhQ>N{AgoT26M^;TAY^|)KrKSCfbbGtiBU8BOvpnQm?5m%W z+@t4~t-_FC9miW}_tA(%)gmpMOq=O$Tu4H$$D<0rz{>d2X#>t_ndS?I?a_<84BOU}YR=TYzSI9H$Y|so+((0C#|NP!g*(=f%W$5W32rHvQVm1} zzVCZC|00!b!BF?D&L2;GD^RYw5m#+h5lXwbxe#TY7IYybY5vG4Uj*dXWcM{^#9%h2 zbJw)T*IjFsMn?@4LXGaqji1nGq;?8vYRJB9i!+$OFF`6FM0~#4s}^LdSrvldjdkI| z=Thwu6Vq!gVt#e%YFc&WJbkqq5k^h>=h5#|6x;n3aB+-kz{XWJwQW-Yz-BON2hvgD z2GHTqdg>#`%fW6>x-pzyGts-L6irhqv%ce~zgQ@;V+E=$CYr&; z?ZuTH$SJqM^m{z9R#-nj^&weNBAAH@W)Mj}PqijwTYJ(b=y?QKf=uH&g4d{@qho_b z*#ZhCj$I&H5|$}B0Ocx>Bfbedg@;dDZ6_psxmg-;#51bQ`YP2qt2Q>Zrv!3omXU=D zJ7!*SQS8y!Bx}*R$$Z+;zqp!wu1*LnJ-+1oEFXU21}onzH1R2<6Em2p9k-#gK93nO zc%sm;rZ}7W`^eF9u7_y8>q7jfaKT9J0nCyKr%f_m>`9*1CdD>1*KNrVk5Z@GpHcwXOUOw$zY3Q|Q2^AF_Jy`7$27qc_ohh2v+&LCwo9ln0u%Qf9jn_G?AL^B&jxgV>13D*ZA0Ucm%aO6*L}^9}ROSo3Hq>ER(r9HEfm-Ma6Pm!HNT-5%LA5u}@1i1UYcpaKi-m7UnU7bzquhA;8 zb5CDCiBh)FQeGuE73!^+GNSspqwMeP{^JMQ6`Px7$jbxr^Jn_pJXO%k5`>v`UFdG= zh08?v_-Ah?$N{)<8QYs4f$(Cr4^lX49JGb2w(U88Ml;7|K{DP#ZCQfPQ2(XjSnW%) z$M!7L&c(f0-Qh)Uo_mJO3Z4z1)QKXIaS(OlkwQwQ-F355QG@u~@ug)YpwmgV4stRx zE@*7TBY{{@v1{ZKv+j4Y7akX)&m5a!3_xXXXO-~PM~)Wbq&Ov`6DCp^Oi6=^eiOns zW311OI6w&<|Nb7Vyy*4F?5v_BBE{AIQDs*rPWK+uR`b%@c;lj?pV8~WqWG2swI1V! zM+1W~gglmEfVyL>MZ4(ekTr-ORfGsPXmejvvvvvw*W1l{)$_fxg6rit-i-9Gx?izD zIkslEYA`tSdoX`%PJZPToHP=-v3Zw<%&TZibyIatV$d*ty~s26s=AP{iV~vHp(qGGLXxDZ#-?P&clgftKA2Ub=7X-Us|T%5 zKvUeUmp3)*R%^Hc+f93y5-R1$?9ooGpnb2N4>>=l7Uq#X1EMsZQ3hB=41zX9SiYM< ztVdnM+w;s2CY$E@AOdi`dEWfU-+jKfF1INN&*uNS5v19>ea)#0u? zhyz3Qa&g`tjID{2Y5i(xi~=)+SujcRLx>8CrQCV~x~d09`Hb{I8>7Yw1Ma&}9jKAX z49pv)unRHXk12|cG9ZIZYnaLpXr)j8@h(}!b#zlnwUGfssVjLB&16R9 znTjkcD<{X!;>#CduJJ%>qoP!Ln>r_{_o(LYvtm}%0z>(jx#$kG z=;+QTAzL^7)MLf%P4# z3fhGcss*~=WG~lgW^ktS55-WQ4eLf?mr=RHs4c3w<0**O@7+1`_p*q@T}TdWS6vQ_;8+Jt}7HTO}XP`1mi4& ziP&vvuMlb4u71u>!cr0UXEqfBIr(o86XRpZPUXxyu4e`I^%mQEYJ{<&Jh)k2`k4TA zR_o}ro#CTj_BpgyXq#?_iJDJ{e%^x@D{c;R&5HlP@gafM^KOdp6BtYj+b<3WDmrfy zkIVg>U{auXVy?xn{Y8%9VRG$|5cA_~sC`FZ2?txeHdjaV>ro}0+8C6}aa=Yy$NJHi z1UY%Gcv|Ax5uN1$Q2y|~@O+2CH-TT`+_XdeIDLL=19EcI*t1t+tOL{;`A>Y?!SGDY zU87ymF zeOF#0*t4P4vcNYH9KojZUme&lwu{bJOd}^n28Ng`sHjW&I_TC=E|GY14<~8_zX?<| zuqj8Co_Z~RRQKhNdk%)U8z7?p~hF=tqS{Tk-bA3_Pg{#21$ZjG^e^9`*z{b3HDdtrEMci+A58ULFmb*Kawq|Bj~=AJ5OUoMMX!L zMm_rQU8u|O+q{aN(nBU@mi;m!3p5Zh{27U~57XbEb5df=E}aixU`us=JuS23zCo4s zVg+UVe7&dGUh(vi`)&$w#e04x9L!9tDHl0SGh&L`a3D!;-#g{}dF>S=XzQB{ruHE0 z)BcB_^Z5hO5b4~>i@xSP`4(z0tu+fO)oBcykWqu|spzs7^C*3>wtjJLL+K774{3Ir z@-xaoanS$Qo=@>Es4w3H&OHqRqZ?On?5avC1P5 z1N2_J7%B~A+;Y4J(F?)l$AM9ZdL1xsDnBF+@Xg@1v9S!sGXd!|wEaYRlMa}8c&O?w7-@yFi# z4tW{kiIik!s7bB`%h7WwzOLq7aDbCe5xvZEfw604dlcwMT1t@n;^BVOf-2)XoImcp z57BPSle{^TJ9yZ@(Z$A5cQ|M^S*D;_!!3@mMz)qooI;2~-B zum$KE-XE(}A_NI-n!j7+GWHaCv^uJ8bsF4@4RHiHezpw*-)lRxQ7+PM(yM2n!Vl6l$HZxajed+7UJ+gOke7Jp^St18@CX`=Gpo^pV7@ciqr{rd;uZuSNmk5==-Z_cs(cH2#O$sF731Xj~^;zR9? zRuPH1lT_{4Mkp1UYh5=#P?}gWrYL%~ztptl=Y;wRsdBy}jnRaI2~(+GPADbzPJ8y! z614c$XXC%{M}b|rM@k+?9Rf1L^-F5r;+dM97+{BRU{kOB$b%iRbgV`)2{tvp+bT3F z{icR|i9#9A>(p2T`rTl|IQ~!QS1O3Xz~7L%lMt*H2dmZY$=P#bW%9lWtjpcJabT-R zr=!dCZtq@xRGIUzsjU;kl@~+I%0pP;rSZOwu1}_h8MH{AdGA2x%xs>P>o)S-g(IvpF{{E)jCmut+e!jnkNE>UPL=SRrjbnn}+| zs1UjG^GdKTQ&fu?@yh`57tmW%6MU*-%<2>oU6(EQII2Xwg;M&a$-cb!@`qWOmNj1# z*8i>`eVlFzj-MQ?Q?9TU*~x+D&gRnO3*jd&pVBX$j)pzJ!fhf<_cHr#Y%J%2 z!r_G6*kOu?W}<>`JY!b9b>sn}(GN#H-;m)>x4v7>613VZc{Pq8R8H1pIv=)cuegt* zLKiHzo%?N|pAui@N3`8JhTojTCalYNWev{eCx~ z{dO z0U{29^dvo5EhiNkjS+qCu)u%5s`4Mr`V`Z2&a4@e?*eJzNTHp4TyDZFaxB5g6>7QlB^46Si|))|Wb%eeLHI?##;L2Etq&7=R_Gfl zCP)-qi{AZN+j2STC!9w3Ej87A_iajjRKm!KWusT|xJ|gmDh7ck0zrY|M=KYjj=Xj$ z=V$m@ba=hBKmS59Exk~8lSOaWW^YK4>fY`IuuIMos7BNcF6sn(g%q)zRzqgYXLXeT0pYXe{<{QOp`k!HaR%C)NxhsaPo?;*~Q zcK~s*N_qJCp@wyI`2|CQ$;hJ3R(qOhfnZ8P$T@ z?SAvx^$cpVBlpccB&gYEL)O;=MItDZfHr2~nU!q3-BO@a&>umIU8}ym6xDA6jF0T0 z(?!m)oNj>QrEuWJX-b~I3h3j9s-GOBMNcLuqk^k3HV&12xO}JQ(V|IRWPaqc`G?R+ znv^x9ebgeS9|H2G zu4qU{Q7E!r&^u{azVqrqvWohtv+rJb3=l~r*qMQ!PJmxK+{2i2 zqG;;fI_b;9()eETiaxRrOij*4+4wJqycTR((b<(z9)kJVEK zn0PKkSTKs{Ss6_p`&`fSDPa1!(3{T5>qmi+*J_F+GjiLFVaN(7!#-kwzlg&II7 z(8gc_W-kT)0mjw8R(t+~DGh7Sw0osndS-zPYs0frh-61&e%^ZEoie%;PsiE)dCt~^ zu`V;qO~^S9pHF%mInN@ZSz+25*HM2}dH*l8K)$>FTX!YzI%1&)S%-HWUXQU$D5)Yy zKy%Mc+eJmhl#(+?3%4IqZBb5t^cfk>lt8bb)gtSgOs(wh>iJ$s({xGkkaY_^P)&Jg zX%BtJB*$T$P$x@T+dF>k8ghQmudcj_xjE`OcRNZ0Ya0E%cAKd#5=bra&Lb zigShZoo=&y;YL0p@lb{pGw$a;{rDN8t!UU`v!wrgEgvjUqj#E%+yJGl;X)6LJ}2Q& zZ%bbw(&BBC@z-N16Gf=-7uMlviF?1DTYtZE-C9Y~8qdid8~YICtEN0w)9^%GsV=s7 zcQ$b6js3W0Ls%VhFT!;XZ?<(G?00T>U#Rn!Zs)M5HkZsFdyr7i7LYTO+6hsY|D%F& z2h-QC*)6nEXGbHeX{Xo(s1~3Bwy?I|KNQ8Z+-NC5yVV74SQ{zg=DSwm*-lZg}$jZ0Nt!Y z!nxWsaPavkhSMGrDLkaJ_kd`!NaSZc4;7;pKJBeWlB|nM_6Sr7CZHLAtZ2dp8}_An zk9U4kKi2FWCqTg;>B06?Fa~aV@oR##xNeD=_g?sUcp(QCTs&e*n+%)s{Gw%cDlgU5 z#CC?B%BI$G?EG^eor{<3QPRdRsdU<#*8qK}Rb4{(bpJZXBn@_ZKAj9B!?F#kLe$wZ ze17m0I>5~Q`I>~i7fw>`49hIF`3Ed)xK=@%MoJV^e%oZ+#f7nM@yS{<=4YYy<%Y9Jn$ zBp|R)>v{Lac1*Ixf?hj@^vR&AUo!nQX2LBfmmjQ7G&}ZAbxfSz$E+N6q+BsS)c)*T zOaKa9C!C^k__0QcQ3wX?MI95R6yLDi4#+2|RiYO90PcWzqaMpc{(5%NDZh##yko4( zmA?wPN$R&wrbs-#_Sb87f4*~Ca3Xpkz=nC8a^h^E#zknxCLP8!HP7rmD9@$9bt3uoNyKbKs{` zPtQZ8B0xlyMW7^^G9~Tz8!y2v_l@>w+@54GRZzoU8i=SL3HU5 z_WfJu7sXyRqW->fK;4ZWSw=R1q~(Tshh?{LIMqQXiv0W<__Z`H6Q$c$=56rQGTW?FhHc z2@1)l*>GN21o?7LglTNIS%%AoH%6z-xNY;ag<-MnRz3_5Wb%it9&C23+w^NPsV$FB zA4Xt0>EssLd=AFWFp4`9u=aO+4gM^ab0m$0=#cfu&tV$G+ihj6BjfysOuYiN_!|Ld zT++tChDSl~wzI+)C}DoIUjix_FXSt+W-Ozz`NxEF>m-fP+l{XQkbfFS-V*+59GUpj zIFjAJlL3PuY|-jS(vJKs2a{F)?F#6^o2B4#Wh(CQE7OAWgZNd_FETF9nZrXYr0Lw1|u~ zk;<}s+LiFyXt5#S`>b0-G5$_W7-?qGz==u{vRojol7y*~fY2o#F4cbHmdy><q_ zJ$5csB(^%U2Vu&197U_*HBPVh4X)R}f00{MrE-QYc{^Lhlz!xw+Rb=+R}JNdw=HWC z%_hKTNa>^}Bb#!W+GKPo`}Jxo00AG6zlW?@Cfg?W_u1Sos>jhUmiJ6rabdHQ!A>CE z; zOC5jHj{K#KN?WjMHsI!lR!YvyN7tdDQ53-{wI$h4+e#hW-NU|@!wa)}!fb!+i?+Xk z6=b13JVzz>DzDIgc!wEm|3!@h}U&D$$Kd2igO+Gd+i zNcp$g8(B8_%IH_QL_OoiaF;MR%F#nPTu$Y5rJhcrvZPd&c#BYI0c#SWNdG@aIxr0162L5Zs;sLR9<^|pz7sLDpzj}=E_3LF%Iw<;X zklcMaK&Jr-AYvb^9xcJ$VGdNvO(!7lrFJ}&8(qp05=m_&VqweQE2LV&JgXTMQ6oWV z1KL_jguY*!fMChtu~EsygjsFk;g}xT>%RF94QydksY`yb3+sANrx2s_)2m+`Y`5-z z6L39yhILcVZy4w2o4ckmni1zkyc@;H-2Z)kUVxLm4@+g%pP5tkUjK1HsNq&8a;~aP zLSn^7NbR&oEMRgTe?HW zX5r_U&EcwMKCg%PrI~u8P-%TJtdSNm0sT2p2&+Zz4$4!VAKvdAl^i@=)!iJ1c6y@BD*EuVKlyKEg8MJoxVYseP8wlKFgM>- z#e!xgAb1j8;`2>lH0mU*4s5}Ws5Z`Pb{x5#YYaP`D=Srq2w3r~x$-v1Z?5XXi)+W2 zNiUqb(AG3i%@0AL&w1@Z*_Sp>8LV}7C<@!yc&3Q7xgpteQLT7vdVH{=x`b?0uE{1Z z#lqZrx@t%flfuxC2lmWbTOv6qAwXDcsH=FLw=l0dOck}k6=Y@o5jB|JTGMGfD}Khz zM`UQDI8$%Tk?v?p&r60oCVe$|L)cIRBP3q4^dHw)GYN1SgNu`pb#=4}Kx3zcfCW(& zgp{y-kqnx_+o%#dmlAc`=<;4fQ^N}1#IesjhT@_6UG)SA*a>oGh&1_wP+;Zir2sfhtbuNn)=7%0UoESMdT3pk7H-(X zzT)S7*{TwoNRH_*ipy=_?fb4L4bNpLUe$ApJXdG}5|1b2^N1Fd!?=3w&5OHeEZQKEB~6 z@iWABOfrWV%&6GX-gnIHRWe;c|d%{YDQCQ6m)DBQ@C@v#(0Q}V~27*0yUvE8EwIB0Sv(jp4)rxo$c16o%cRFypHBN7&?@`LZm*ES+ z`76p{k@`^~&v0w@Pv=C$Ot?FHn6*2J-aa)X!W|+pGiz-4Mte9qbl+gZ#fjdZg~U$2 z;BqXY>zuT$o8d!Kd6WkVQ*XyZq=KOvN*Nhn zg-k16Go-S6(wMmi5Xq;do&d&^=4yC~Q<&8t|=pe``qgUBJiTz1ozCiDRB z?tSdnP)AwFPvJ@truBkk+TFE|dkyBcXRo_Ohux7`&EWnIPhzr$V74 z*u0?wu$#usZp%8joPu1giyor6Wli?IGJmVhMmFxv4@W3pEH%26L*j=p16C>AHpV6+ z<#JtN+GL670TZLjT)(HRPLM+nv)%xI1@^4H3nU<=w^Gc6UXt)#&96%XWrj@+aD3Q&jR-7W2y&^mK1ya~b%XrSowC>D zw&2E7^$pff1fc4|4T+e;$@JdJqnG^rkM^j#KO1)#{?l}cPYG)&=T@{WKB(BeVc;K% zP|l8T?5Dow1WhiE*Nq+Cn|Pj}c{O!~KRZ&;b#>`Q)-*WCgKG%W)#;gh zb+A7zVC0q|oV6fg(a{a_dL;ApvNRVJz z=ktjtk=Gr3Uv~^vet_~iFE1zOk0)-7sxIFv(GTpK@Uw~lu(@v^eMqa!yj}VpmD8TTN>vRQV$y9ZBf=Fb<*I+cYcJoT(`kL39AXz7rrjMlm!oMx}2g(QH7L`eui`N z#nceDc{R~zc2pDdh>rf7q}-xW*mR)2fP- zmA-+SMzkqig!)eVK=b@fFug60<46melEF?`t;aD)&I<{8Koj$ufN#x&W{ z271ZJ@1~mL4as#%48`i(^i^x9g`&&}{n7MyVPNOQ+PdJ#KPEK>{gEsMqd=-ZEbm@| zZ0_P$QdG0LW+{U~SvqI(cLbDwoIaZQb)4UjY(HE)wRGgu{5OHr)@^Iuc~(SYrXI#M z_l9BlrJB$lnC%{dbEX&0NvlZX z!c|M&cRTCpZ{(vo8?&ra7udKj7v6EO8nBtZX+|o~pS50sm<_mSVo`#9g@T06wD0x7 zr57UB5PlS!Ez}mbt|Dn} zK$$-mV=My@s!Tsx&+A|4V;UW$sb4=GZVb-O?Qy@rR+aCdb_BS z6}sTNMxo~SYzB@$YmAvC_8rPFyiHZT`dRF;gzhrcJ#I&W3eX^r3eYy}ByycljcZX~&z}o(zYpcqX>TG;)}5#34?cJm{5ZCV85<4sAW`SlfuXMGac zgG+ZAmcU6NE$8`)#LokPkJ&paF5T6+l?J#RW+l?(V(G8N4ATMi8pgC;lW7?KTyWbX z4yOwx0Y86fOftSs2)qCfmjz7x#t2229&RRiBNPEhUt#6@b(K1r~*(7pc$I~ zj_k5E)k~D9$3Y!$8~OXv=cl^Vf*9|1O1YoRT#%-Ie!N!-*d71i+*Mb`S!iCcWIS;7 z&=rV9{m2pk!5VrXJz_W(3)KN$Mkl-^L52Ag73y&0qwdTsa z9E5@a=%EZD@tCwRm-4-CD#4Ulsl8&Jy=*edYJbQkva@_gFBj%47K$+I^IE4vk2*3b z^3OOH+;$QO?t~U8e*Dp7ZLfD-IypY6r0)i>-?jQJN7=5>bQ*)f5nm=QS+pv=7&wXw za-hrooagJ{N3W?PfgD-k5+P+CFbz~j4d9sTAf8bpH}D;Otd?uPsA-*#vxjTB9&Mwa zNEHtHIiluCS8yJ%$%ub?^InfoM@odO*eZz3W z!kXcjOY>5g*~t5gx;!z^BO0Tic2w(+^KqZ)-z|q_g#M0XS){Y)+4Hw*vP7jmOBK2~ z>cP+;SO_AV5*T1~+oUK7;Ume3 zFM3yB8xBL&;GgSa2>Oi2)5eUN^H1u2*L`^a=qiZ_|eXlv6N zc3Y(v@zrVbhy@@`Q1{P_KuZpjaF@ZztJ#l8=R(dOqrQwJ_eEJyi%KjVz?f#W=oD_l z>ND(cqJ=G7Od6uy{aL>afr;$64R1|C;EKEumtPeq{xmnV)_x(!`Ui6TJcRjCVp)={ zxS>G}*e9we7Ng7&5w%3WauNI_&J7R^W!RB>`B1g#)f#k!sO0iA>|=m8Fi|}M|6P=w$L~4h<{YLJP1c5)yfq9==Ju> zyZc>#+L5jVl6?bf=V|Vh$9{a@tRx&eO?bEI)ZPIMesx+}x)!<7NVf%zqDU!E-THuR z6RaQ7S3K1|5`(ew^xHh}BJExd>oKoW_$bg;-?`E~ST-ge(|;U{+pD+;vgM&lN58(0 z8>y&d$jzTP)NX_+G?q$y1A2?1pm!#oN)4AD=+QirRSM+W_!PxMKF>la;X)HX!t;4( z-BvNN%^P?Hr8jQSY(10HSmr9~(uY_#->DQTd!j_p$y{Vf)RVp33G4Mnoq--DV5_o&1wAIM}o_R%*xP92ND|YUlcNW zJH=3UCYEwnGnkFrMnj|fzX>>?T6jOu$L?T^J}zH|n;D#ZG~=*a_?2kbnfxlW#?~S= z2~uWJ_a$5?woM7o#^yz(`SG^7d2U}zd(G!$-Y&A8icDuERxr|m+64JJlbaL%c9FB+mW?EkB?}<&1U`=RwyZxz z+jctA&k1%So#^xP?aH3Fz9f6#xXj=7nq1@DDYIP`K8U)B#~%q~(meuZd^^0_FhwUj5Can6Xs=48|ARY?>S0(7idN^LR z@T`H&Fj&}g#WeP8LQtt_#LT8=jrl^Xf^FjcJ&lq}T`aYfv|RO_U&w-+CLXdV*KJ*M zbRQCBbY2E&-m!K&P=ZRG8cGy1Di1}{ANb<)yJuD|KnrAe@%I7|O!=?d7wr={(hh zCIh9T=cQuD1EprFE}WM=pZze*RcT#JEX%zaI}UI9IO$pLLx;*@n`M(4$EF*78|xt2 zbJrKX2^`j}T$Vn*8V9Yj4M{Oy&={%xRQdxxBi7Fyz<7#8wqMQt*ezxH!DQd_MHXvCXB#jkmd9%*lA`CRL_3h!9QC;F!%bw3U-yRG zWg$Hp^~G4Ne!)0xN}v6X!C4Kg3?|GW3T&>`So@RhaxJa{-VC?lHL_UMW3H+GV|urc zkoKeZ!bhKv%2#Nc<=Yq-E^Gy&AJmLW0V)1LceGD7(7IRdYNmCW1Ne^X+T+!7wukJ&<1+c}H9+zznwwD9aJLWb+gg!`1^Z`{`R6HX$5G_Z0X6&UY$e#ZF=F583 zy?%vY#{7Xyi~06o6u`dVSZFdGgH3RPsLKE#5?O;Z)4xXa^DIn=O~ozzFqqg2O98zr z7z@TGm74cZAmaP^7|8>a@Wc*5Fat1|hFUd_XZ}Czy?0nsTe~mH(&bV?aiJngu~0R% zr4SG>Vj=WmOhTk9ozOHi>57673@l1eKtMuEW=N1gfPjFabOC{c5}K6I14;=>^JcB@ z?!C`_?%Df%XFvO%KlXF^2cBdyGsei6W4z;ie?^kZegOjOJ}2T97gz`I_1>u(n=w7z z9w1QYC6$S0i6q&PWd}{cy2kIk$Hsf6hjw4kZ}+gl$}tD#=BTH=p2yV`W5_`RYvzrJ z&ah)_{R)Nr3*f+Ff7F%gsAQEZNz#*8hMqo0Tj5H{vvR@jV)O+sM0Ergj_$2?eVjIL zCADW+exkLy3aN+YW5s;Tv^Nut7nF0T01ay5S<{f&(?jp@%-6Hk0+>xm?YtzdB8R(3 zw(t*&!gafSeHC$hR|rPiEJ|{PrHANJB2eM)hRPw=%>b~SU0+P}`P_8zxHC!sL0a*J zB(2Ez?oR<@p6=F9fdNCWq2-=}vF~pp8Xut^F?cq4ixnL*S1bD-$TA;rJV@E;o=IW> zl9X*16h_b9j=abosd_h`sPMx0mU7PJFEUBCxLxz*n$uq|{9&7E-5nTQ&QdAw{$$9d zOOUS|G6iS0KgB3ajE|y=^lY8Vr^2!#lKDAnjm-6H#gEs7Ygief*XFlU18WYU6xUXM zK#@r6l{mhlrju3>NL49dnmpLwTPdM3SkCE57)M88xu(n!d=;kDHolrybD*lt&zFb2 z>h>F^X|^ED*A$Xq354F-JUt_o;uK=uFXQ!fxMZDJ)B7o|wI!Ci`lgC6jVUd!#(o_( zf8_OA#phz1o5cZ%R(EF4`Y#3v7D&c25vG&D!KL@(qoKUxO%-L(qII)|A(Pu}fV=&x z3ooZITG-g>RdcI`@o?hn`|tMsFBido@`8WXeL>*YjQ6<#qMJAnx1zr;J^a*ZUQ@!? zQA8J3N`J8krI>VLL|nQAyz=r#q+yiUKK1njoT!5?`m2~y?Bc^OJ>e!OJM`xhTgnxJ z24B%d-fEWWdUl)!#gWn4OE)XZ$}(zd`?ALiiFy7sdMkn%Dq}X)B-H`W;P`z3oquC4 zMXR-sC9gV(NyWj%He#cjVt^qlWx24UaDF8`3?nZpSn|i;9TqZ}nq2#{t3z%#&$X96 zO{`p4V96bxrwY2e4JTY4j_o5zW6bsIQxbb_>Z-rkOyGUynEqv57?dQ}pm zHxDHk$2y)RI+Y1!t>{l*<(Ju}wfG>fxel>my>`uInd16xz8J=+^-UkP{hTLDsA%T# z8UHb7kK^WJ?b)#uJ%{~gPq?)bZ1PGrKzH(tloFqSJroH2{;m*m$HkHCDb27#IR=g6 zUT|kfD*DYb{C=&NQTHH^;B;`fj-_4Q7ZRT4)s|y6by>0_hFZJr|$Csv!7C_QE$$rCuBAE1s$C3%lr7x#E z2b~ys8AM{uD#l^P{PfT|CC7?oEl(@Bp{yqBOjdXA zXj3R(K^SYpw>A>${wdIu_fudl`vy;v@56@UcS6YmazmRHd zkLo##P=k=)p-sU{e1H?e{I2ED{!J7=c{jG(aMN`x0#~H+rV&#&OK0ue-R-oW0_`uxI`Vd06h|oGvh!y;vv#1-D}2FI z6$f37#P!ts@XC^vj>ws#5wx_991orlFT0S&J<%&?*$yDV&@;gJ{Mb-U*JJkq$SGPD)V*umm33YEeji4lO>8VFPj6Yikr#uzec+2OOb zL=t~le_{Wr0u|h=oss=hfQTlYKR*lW3Y3SUT!2^o1kIc)BpYv^9V6m8w{zUbZXRmc zDDf>p`XX^;c^^Lo{>QIZ{_Dtk|13%GjPZg5$3A6{MWcQmu#u7Y)o}3CTPC`!=E88a z)5auor;jf$DYo@-!^pD(0?d)t*86Zyp_5*w3ArNQ+K4Y3RGn(-8e6TrQ3jeuqc6)d zJ1ba(I;kaeBXqjae%J-lW@?ZL^WL)>8NPR+=Y=|4_S^kQ%O9-?Z+{fYl>OTWG~VAs zJuE?&x*o*RXb-oB1w-^&B{rlHwyKz^mF?UKrvON`K3!xv{{vp<{>I&Bi@jfY%|F^r zP~O@a--sZ-XzfcYoeQrmugV6>B1e%^S@n`nZ#~-aLnc@1*1??oR>LjRb+c^&1{QiX-C}@?=<$_D5HZ2|8XSDyM-OQso2r}lz;NNxKB`BVQPzN(CKj6t&u z>io2d=#}H|e(5b{ie<1P_PMafVZUAckXEql`yRFb4*~5nkdX-VkCGYf zD=u$sEptweE5kP~j=z$8Bd~5m6|HDXB~`GPFs8KP?f-E+HId9N8@L?Qd4-8Etm#Ta zk%u|UNPBdMGAa~LFgk;-vb;K`NMxQ@cQ>JHA!Ks3g zmAGWtULG+kK?lHpS{~8o%ac)bPHY9@2X}L#w&bLnm6A$N*lJrz&S>gT+E%)*E_;}v z+d1v-;)UQszE5r>rl7zL?CF;sG0Jk7zacFvWI0x9^NHD!Osh*sLz;d~QMKF>g}?Y9 z>u_gQqA$(uG)>+Dnrd<3aZpD5Fo^}3dqlOzVJp00-U%$73kzA7WG~g$bU4+B!eEit z*V2++vk(EI!pse<^>@+OB-%+Bw%-i^z_r;5T{i055I&!&=l#&m=(rIkH@AS2Ep9Z2 zcfo3s`DkD*c(yhU>Q^qFVcg;QFngF{?ZyPrJ7zgt#5dcLw~7B=;`rbH+W+_85rR8! zX$6dQ+pCr;dQ34`HO+g-{Z$JK^8CvsRfF zZ0CUTdbPP=WVw%{EK3>cvjPe^Z2oc4;hA}UGmL~FZOEJ=X@paUX-Ysct zJ8@1W$^MMYg~EvnG59!AN5rm{_a%nATb)g|y#3xn#yK2(>5?6*a{=F0L*_PJl@(!Q z*QUvK532xGIVcdRTRyGTHlz58&pvGf)^F(w{LLx*^@+ZRG1tZcB3EJUo*$#4n3mg# z2Duzd6A50uQeF7os<7nJkk1uxwz_8}0coivdI^gq#xKqd+&S}ae4kAF7zi|faTCDD zCc1je!*s~MXvSxNj0z@BH2~6U z_LHgR8og_Y{<})Lw0k9ni#!fdc>$9a5*k2J`zc^VvXyzmPbMt!0KzXq`_fUgxI#ik zxK4QGsMJxH?vKO-2S<=rYmVp*dQw7y!6kjNJn%6K;@B4vPLa`|3}o(bIRWBlr?_-;Oc3A)&pi~MC@T2|C(Iax z6vT--Ozox}loG+sFbzf8#*h!-kBcgF#x02R zRZc@`*wDZ@$AaDv8Zda{M~=m@I~4ym?6OuYzbAU!C4~%6H~0`32~47^Kab4L&0Cjg z@0ZqAUq-YQLZ+O&a||nwOhC6Zl77EsQf~jG8vQb#6y1H>W)f+DG_UeisWKl9gXBkr zrx?L3WQxV3h5|Cw+jE_g$m6MgDSTg@kEdVsT+N;+$IouM(aa@@(?7ab8#s`s@$Mmz z&8w3p7MwU6QN1gcZAtfkaQIp_4vsUB0+DNGf#I(U`_zLjOct*=$c=cpm-^;Y5y+;X zl#>lX>*stuUv6A%Ax=|8#Y(Sj|5{_`&g?<-6=c-6fygpeXZG*+d-BwLgwq8^%w`GK zXjt6`XoH>4vyF2{EA zF+iAL=ePl+t~x}!r4JN)y0oQn5wXXUQXXE{oaev8PPQ&$4$I(rDp24%*xBy(>R6^d znGIZve}W#<7gt;PWB0vayZHcBJZ8F00wJWdWD3k2b_%uSEM7~vtE^iqb8@XP6Jb4w zu{SbflSevQ>buWwmY5Eg*M4YKNs>XfzT`M^k`?%dOx4j3eqr@m3dQ6zDq>hij=Gr5 zXrcabs~aJi3emDFRsnXD7fMS%K=)9P(PY)NYvk)}Rt9>qU?~FXha+;#W~cb~(ySHR ztj?*nFtZJScATdq(UHeOjO?qNy_b=@)B?#Hp%nG-x>kb!&IG@2h~ZNb1zrj0Qqk^Z zm3bLh$Oe#QT$`+wUX;&FYh7~B;S+x@w$--IfVN@675$`wyg_P8A<41%iZWK~WUcko zGr^qW(ia{HH~mT5=|*cSqEdr3Q^_gujQW}waQM4=d*X7TbKxvavU*TZ*nS~>&%-jEt9-c!6kZ@?*Q!EtxHskQUUT~`Mwa9 zh5TupBD>Y2z}5hM6ew*US#)sBCdH=u|UnxC;+T2-a7Z? z*kQg*ps|fN9ASHzg)bfp(A*ys{S<0j6YSUVpj-QbLCkpGQn|QwGM^$e5EfGa6URl^ z$1)lKC&J|0gp{H6sBx1rMQD#F2MRsZ1D2^|0MFx)ry0;u#G?Jjd8j`(x#rf0Oy!XD zg!!%~{=*&S{EVvO?|uh4?S?jU6*kvNGHE2wqNJ14ikTnuMll#uw{P(B1*TWxqqx26 z;L=Jtb41rFrZzU7kHFVkTrlxQ<~SZdF8b~3{A;3qErYLLKLH6TDuvy+@JFcf33g3w zlemPxa5mub;m%l3ZA%vC`ABlU%^SXH5G`$D$`r05im6RUN$)&Hy6aTeo7k39-e2(X z@;Qy2EyQ73j}6o_gZw)?=L1e8WqiACNhNs=pSXR$7TgZk*&0si>@t6M_H~x0S$4hg zpbUjr-OEH2Q{vXT)jqd@W9H`fZ`{$e`Z8Xe!;_bs>cZw)w=_4()w|{ zU|b*Ub7IZrJa5G0rF;SO+bE(EFBe2`( zX&${+MqiVl8>IM^FYS_=f?#`Z2Cj$qOLW&AJfA6RX5J6@A$B!R@#XNXj6Ge~FZFRX z`jNLdVZDet14gJVk%QbD!H!5;%4vMYGlitho4HZTcckjFPsnAb1eDgo&f$=xr!%a9 zukhxIocz$DZ})am4puNp3nOJampiTuX1N=}6WIQ?AVrBPs@~#S8TXcMpi8a_+m8<& z19$d)f9UJ{8P3$|!YHB+;+W><4eN`Fb_Ewp7uqN9#Kv*VgKC3I*!g9S+URMn)jqbh z)!p1er&ZZv%e>)fGK|t?&)5+0E+Q1x*SYPSLoHrYQcd0G|82;B=QyVJjq#u?KDNq$ z49jFJH`#rY-V!3ENrfJQ)wW80Z5b4gZ*9SiTF`%A+J3z`*VV$;FMgEZb#w0MvlYxP zAwgLF^T~k#*0+;P{Q~ENCM~}}Bip`k-*RJPW1wXm8*`#$-^#?vueC(xbglN6#09| zN;I~^Q@M6UrXoZaa#RtO|EK4Duc zLcX?guC1#YTzC7%CRT7@fUIQn7KZ^#L^nGUqzSyN!;kJP{b5h`p&zH+)GH_NJtKpx zSwfYD4X^9zmt1^*xZK96#@nnMd3ZF&BsE?p+%<2QV-a%B-COepTG=Kf!P>YzEO0lT8mQBum>uG^5rgawsfNDYE))|+%WEQA>L#LU z;002B73p=))vl_4H2_7z279f|cZgL>ucP!o2#GFbxGA1$g&fDMwEYy=Xb@?Z&clmk z%;a2oyd`nMNc`hGNAJe?P!^n`(SG_>E~Z(pB`CXZWlVpZ()AwIXdIAW38DHTZ29^X z=rN7r-as=m*Ep7vAyUEF=xs5yVT~`N)-rYLY>hlMBi_N>?U+%kag{^s3LZYz%~Km0~`U3HBKr4+5QbWe5Ix_zNoJgB$Rnb}G5sA*7HTQfHf z4(&~w0KSvO>sCKhW_}diEEE)XlgYd;c6dJPU(k>#xirH}lDaFQr`PZ~O3^J2Rd#vi zOM+9x*#Q6A?rI4GkMm|gn%?O((5jCjP+oOs$O`u&CO}>Pj^N5Z$C`#CYZ6BRxg_) z(OZ{r-;Z;fsK%R{8I1l*OyfEGx_%YZT3TKA?z8e|J6(&mzsk5GFZFs3DdvX0EFpUN z@P&&Qps?d}T$)CiJAR(2HAp9c3uI!F^ zzWrSH%}wPT-K8Ms7YpkkX;d4ZDi}z=(x-V^kOP+@ho5xsYWcABtHi}S>++o?Y$Oeg zAD)hutV>pzFM8iGv+cYmH!Lw3Wh65^Ct1_R;Ul~vOt9l>bp3{&idswK1!p1-KRmps zCD9Ryo$3n7MRvI`-ru)AJH$1v5$ZItmzFkK*DYoI8Gof!6I9wd9PyAXZ|9)g=7^j=G>B(rBLXwwq+erQR9Rz z26@wb_d_1aBkR_J&vKXI=|#o%^EC;>8@>@y^n}KUYP%6rK7FqLZv^FN*WU=r$8Ox7 zgN{8J_Y`HFSq~J1jjS04Ix!<0s67!#2fOp>pMe*G-@>RIZ#^s%uU+(NU?}kr1O9QS zVqRfr&GIk%hS0gvy{|^G6x^{F?|REBYi+) z)5+gNmW$8KGyMi&^fduQ?3=dGov*MIb1Nlh{tj;?a@;6Y$9iOhMAA7y&o>sMt zhH({M(F3*|Of2 zS*Fw5?PaQ`c(bdZy0K+s*<7TNtqqH>7m_hlR~!Aw-;X`;hEN0!S#t=H^;m{aw>pQ> z>Wn0*p)A`ORdfk*&uHJ7>*7Hx-Meu|M*f`Wcum*O#S!VSygWX0si8D>YQtsjt1kH2 zXZ@`0@<_r>splP{V?jI5pzWxJ83h(@GO zn6Qg|bAXwhDP2@k!nUl96FNFqfaH75I}CSh{Z5Xm?j@=-d-CCyA7}JxXNxU$PBT|g zlfx)fJb1M0K{B?E5567X^4WBWd~av?JE2gIKy;g3(&xsIG*CRg&QiKV0^dReDCX$c zI&9^Y>fEW%8wa_eN8Yk}w5)Vw@x>SwQGnaTfskGml~b0rZ?Aj2>9JFJpmcm))lx(2 zOx&?F7&5*qNB7wkj6${F>~BAQ1VxnJMX35^bNq&>7%CEp0SZ`N*u#FbYN5l~b%LG+ z$1-T}7VfD0(nb&%+NN{a-u?E|m5tYhxjlU&*PVcj$wOR&wj}7Y>#00YiCmj%pg>zf zcX414<_9Zn#^+*!=V(rK!!!_~F|lGk`fxh-W(n$)&jy+teN#FCoqocgLKfk(vfcn9 zxqA7mljZ}I)G_T=C=W}9g7Nd(QUdrsku%qIFk$#lT<&`V6H3YNLoi%{^E}Q|8+<>r zCHrIGtpl_FaL@jaPa^-BL&(A3{v+CHVSt%wZ>`7~Mb>;=FNiUP7BE)P9_8NeL9jz2 z_9tDhj^)vZ4d{#!&li^w=5|@J57u_rp;FUn9K)6zh)(6pK^PLKLd--%xyxE#gsDY! zh3WQaoWU{B5U`Z)kmK*-ke5NCLS4~=<-{Xry+Crb-t zqWFnsOIs3I%ChBcR@>}&+{;7|_EqmU2HyxoMsusyjJE)8s(;2&a|(CznO6}xcKNHG z)|Z3wrG28CWi53NV$xJ)onChCwH+o*Qf+Hn_$sTLWHWn5#^>fkDNX%giA$i$PE%D` zg#GC1p8~v5D%@^~6(pF}Gzd6b*$;HXh~yDGviU9EgIk2wkvim`WVR1^#iK?7eTQnP zz&8sbOn!VSAfLc?EeaS{QM4YF)}PHhF>ybGg+P7?{ho>B)|xB(C7GWs$TF*I=C0`gKOfQ?Ud4OY38 z>6HU>pkK|UR@=KZR;1y(>Ea_T85>&UWrThSxbr|j2^x>f#irb1&z3xZ&Q|A&oLS~W zyB&Nk1qbN&s)&W!Py*+*J2v+q7}m%FAhRZ)wW9gn7BDPt z`b_`rRiU@Cl!<)4Z{FdXLF_SMY}{h!fmptN@e2>}_*=L6PFn;}2x=t^@oCtg2m|mr z3+d-x3&>_zSdW&|AZcwe6rKLy&!!_h(8eU78kH*{V7{aSXVO{zWmnM;C!;Jo-o;`2 zAl3}MAPC38)v%9L$3mMh`?Y7s69@>j)*C_|Jb)PY6p371);cS^^nw+HyQnti0o)nb z_5It@g23B?r%P9RyCG0L)xVjU877!2&JiqY!{^C~fd>Ts($@MnRYwGO?y#9%fl5M2 zMTJ<~Y@fmUkdOa>HZ|-LOT#NHwIP?pqa##zBGn z?LXeo5a12^s9CEpUkJK3&5`oGioIe17Q%iQqym{CBOT`H%F{MkVfPKL;5?CU=&&li zh5)2P`4oR7pWLxkt&ngjq>5X@ZPdC56fAL)t{NPJfgE`8~#*&J*cF%zm7I{dlUN%GaAHaIr!`G&V3Q^ zFBWT$j{3rGH#2#ibu0j>m6yVR1%r+*#PqF0UBurFdUzn=LP{ba$lpUMxc$4KuXr8z zVd-sapQFuSWf0v7Q`QAVgXHa4!52Q!M##l44lH;F94R8X8EDdV@Y8OhRH}x3K#DodRebNgMgP%)0C-Li^ z|1nw`=n*Snj2J)zRw6H~xM03kZfMe2zP2jb8^7FJP>zh~wdSo`T))+N;o);cRBjE| zcgcIML-dM>VPG+zdkotHA67`vM%XR=gM6F;Y%!lI|;J~scrz|EGKE_bwMoeBNk^W ztswjNJbeueTBw(=HiNGuV%>)moCc~a0e{5pNGP8PYG(Rp?OK3}UW1YP)u1(`NW{K} z0t;ve)Snp;2M+@Lm(lld4d7z)DKzG0vcOU64mW>t_Zr&=@ucN00ucfpHjoAZVE)8& z(b4%GC-7@Br&vYoupG3gFa}?aNbnrsfm@{knQAIe_f)8`Ty-@tWFu<#>vB|~+^F+& zThN7Rfjf!pT~dI;zB77*7ZbFv&+KN6y%`??H?rpn2A!yPTp!bl+jWxK)m~Bk7*w7% zBUC50>g@->>{Po?xCgXoU zO2z-NN9@1f(CuY;S6YsM!mK?vK^zics&(HwdBKf?DqeVZSnawL!7@Sj!bZSv0+M0_!9p z#Xp66`;Bt+=@e4{;7R`y3+b2OYtzA`AZ;U723O+J3}Acn{8BErNfSV;8pr|PK8m8&CAWC^c%oVcgdni2@qT-VPHr z0tsvA;+x+pPQg4RWIX!+VzU2dPNe?^&nM{6$MPxHf3)pBm38_;q!YDv zPFq^Z?RXS57v`>RoNfUgWTl+5;i+4Ubd(9 zWRn6)3w0pp=++NtOS?}(4^J-CP92?qfG#GeyN1s%zc~=mP;RGERLktQ8|Y_qDV}2z zFAgUyyppMB=|PbjL0NXNN+Gh3Z&A;i+c5TPdr6(od08^>WEE|&PSUZY!2#4#rgGmO zy{lw<6%p;#Cx^FS#s`k%DX}WJbPw%$lL9%2N`9`ZQ?`1u?Gi(+D*%;TsW>)*J<)w^ zZe8lQ8rViQz{@G47MrMtP(G8g*V)d)FU0L>+J;OGjb@PeIbVABdp?`;er-PfSc&Ri z(dDG-HC0MEy3Onq(zoiijR%^3SkD4`aAr06Kn4-0Vax+dcbe|M{c-oXSEXl==Pog- zl}O0X&SpqS$U55q{XW1roPU6o(lK{yOZmD z$~r7`R2yp9aqL*02$!!eF@Oc)7UgU%GQ}+nJU}%cmtx8+bwVGTejj<+dl@ZLc|U!c zgRq@U>inS>E{}}xJu;tVwwIuE-1^*Rm(4fV+_oPOSv09GbBIWeh9Xh^ri6Q!>fWt( zd-o!teqQSyP>4Lw!Sv6i!exw(7W5lZ+u?lX;R@NZc^zBjCo{5$bc)YBBnZ|Kq4SlT zy?t)nj_;WoFy8Go(epheujmfHycFiSiAdSblXuaN>sd7ry6c>1P$9pDJE4w1`_FwA zKHachyBQ95?!~Z5y|~U4!-q=H4^E*eolPM{!9OaMA4EJsI;amLpPOjY2$U zbIT5o&Cf$|v&U}+CXeKOA>@^JGw#JbJpWb?+62><2~GNaaXal*)8~0p`L)>ZqSjf+ zKei*SngW^Xa9bib%OO7m?M@1J}m_+S6b z|EFBP{vXsuT)eI7CY2KywQO`q^!7Iqn*L&CvWu8xV%GcppHGm2=w%pB<06Y6qrk|bSu^5)Q3LP$~q7CbGg z4PpJ)cDo_-H-UJ*0Ra2Pw{n0WZzz4@%CA%}TDL zv{*0zSVxZC%oeD*iimt_>Cj=fId#POj8RDBYR)$3=DQOPZ8CG0$g2+{5$Rq$?%lr3 zOJ-srRF}HyUq|AWF`A)AMx8RVjY+Q*L$-r&T@#`Q8r9UM`@QeW_m@A{(G(flck z53pHXLW8#8B-W4X!<3DY!%ig?$m@!ixhYQvCF+{$z=(FfD$arUVIXW%0KZ7xuqdMM z5IELTE=v6K93uo=;*?&yG}={FuF>^6x7y6VFI5xxE2Yv>Dlb(xT`C^iFFlTljXofd z>Gi@oyb|hJxiGdEzeL)9(wN-C@zNB5GM1$94fZIEe z7dz@35ggv}vwJ4rdK?zZT^kAY?bom`VTq8>M&Y-}LN%g)kQ2g@Bd?MK@AD1L{_D4* zf0{|ZSN>uTq}U1Bg&7x*!z1O^B)}~EXW4GHgM?U-xV}E;r+`aaJnt9b{pg*7WP>3w!LcN8#pP=63nLSV%gqaO=aPd*tGpi2`N{v%-w&V7fG>VPS?n<$x?L)I z377nky&P3@VOiz{S*o*H`(|KnHhy9Dn?&oVPwJT#Ip(WTawj&FiZAy;7@^l zyG}cSAQ$pi#AQxO<%}NJPh3Ac)P2%O7x$L4BN**Ei5b018(Ha?JS9)cZnRhM`<08FQ*RNIoI;!~B{ySdt+``$Onh5cQASEUope}_d zrj$ZRspp{o@_qgN`-mN31Qy7Ee}>ViL}J9GNc&Udx4kXG39rj6@2Jp*sLGxof<8Hk zYM`JFHIESVd-RW)jRR&F_xtFVfZ7(T6C=s}l;Z)Yg#yuR%{8y~w^QGyW8)WF_o0R4 zmuaC-5l4zkZ9P=~C?nsUTK{SafVR|wN zWxF0C{o3e6vo0g4!}HmTgBdegpXsCz4MlPvvii@7JTj$Q#{PI_G`JjjIoP1{Fz(?M zsm{}Dp}T1-Gg8{;4uc+@DfZ0f(POE8FAl`95J%ow(3?8u%PCQ5)0<4vgjR8edVEk6 z$8>$vwh#utY~H!DM?iu|a-)#38OaJAFvX-|TpS*=B?QWJ4*0#dS#I}-FI`)YBY{6n zT5RK6QwipaK2+GuaTPOx()&Sz8KIQDFx$BcJMwg03CWdg!F++&zi15BMQ@dxL~Tz_ z*&BWMDe&ci=0;_GpQ%$2ch6Db3Np>CC}5upHFwzH+Mx~X!7j%A!H2ILF=X!^ITqfQj#x`MX&Po&kyjDc5l*f6_HdLR>QDGAE#C{7R7{MO z{30MAB;&#M>2>MVMgxO}eB)T{xvVq-p7=mnOOT;ltcyg z=S4nnyl3^6FAp%&X^OFbX)8r}F;`=@V2-vXXGvQ?WNkj{#7-}%O!gB%(yMy&*KPjS z!G#9=gR1bNLjzmGee|T4WxO5BDtk+f0T}( zi(goW;`Vmq|Ii5rNU8RXP;-vC!ROL;a-hX^ZQUxgmbi<$sD&& zu<%4aSX?y@Ld~&jFNS7}S+nd|qgXxj+hwJ#KLzScQd%8rkONEQ8$$lCv}5h~O*UsD zxKMthb$1Waaq{VisJi3d!qu@ko+vI4gvt3BHY!Xlc+)@Eo}Vgq*;31LR*!8%|!NV!K}+# z)99#fM+}-Dl1@EB{`G|OxswXfPui9#J~N#{hr*nW0w&_?XD)z zL@a$Yg772LHOMAvuY;&}bx<$!Bg1pxZSNL*z{e9y_ zBA*$QNF^r#6qwvz5nYPbk%LZNV?jEjH4Ac+kVq}JgV}tu)HYSeR~=6xPFJp4=@Jjv z=jUPvi9Y_nYc0j30e9|{Ao!}z9KFtqT}g_)n^(sk4AO^gb%EiY9=+9Y79%ksS!cG;a`g3C5_)oZfK#saVKVukSX6|WK#76u3Jpw^kM)%B}l5#5jO zoqpPTA}%b)5{)RN1nJ5bHWYgdzFkzk68ihh!bmnHYEX2v+xmsXm6xC5o|i#Hk$ftc zX_3cKX4%y|n4LU6C_foMX@vvp%2H{@3)!FONE#{VOO$4{deFr3MbwKYnK`0+0Gc(h z9Av1>n@zJ$&x$G1Ghat8>Y2MF!Csf+a2*9qcJ8-fB$1ks_4XxsY?={qSz~d}fr*q` zBa%%2^3+AoV*}h7)%kPCex#Y56$AbdD&2pwIZGr3Wy9TLQ`7H6QwD}(j3H#!fM<2H&wE53tbM9RpD31^52_dv2YSu{N zTDTx}bZrUr>on8h*yA@jA7<+{U#y*#Vgz~=DT751Gc)4OZ*U@0t|gbAX&6N~y#ijb z;DS|pH?wHQW=dN)TF3&?QlM8U&SYjLE%oF@VrH46JUvW$3 z#5voBk*+<5F5VtV)yCIcFH`ne7Q)uljA(A4{5EZhP~P8$du=S_LQJN7JZ~qpXLWQi z+cbw>O_gK-{W2N+T5&33Q7Qfei|k17{w@D?Q7x=U=~G;Nq7MuI2&5p-^wVizVNi098lorTFNZmL%%ydQU-YyZ_PN9!ukXCEo@u$tnX?r6O?KR=Z$! zWA6Hb!|5#l3eTo_#@UcyYV#%of!GBRcwdSEWx7VVVcO=j52BX|PBiZ%bwIE$5#RQF zVumWw1{Ve)cHh=cAaI)bBtHkGWDcfJS{lS~HzM9|t;D54>)E*|;3L~5)Mfx%fpfW3{8AkFd_8M zBZ!*IW%jdfIXB1N);)x)6lRG?)@{o}(oI7R&0pq~Rj(% zM)&0b?rW);I;9`{8K|31PI)Uj`Jy-aPs2~qR$ z7UJNEE7S!>kzA7wnHw4Kl|OgeZX7OC{jhLm=E9ECZ#qyKe2!~p<(;1vd@t_g zRM?UM1o_E81u3F~s>(rky|y{Rk>QIOKm+^NaF|_L-7*iS_d%~|UV+UhfBFaiq7Ntk zG{#vjgKC6Vli+D$W#vb5-lKQE`ImeZ9@>tr=isOFsD+Ht`GxcDlevZXmT7oXEDrZa z+|vf>@W+Vx2^XQ$73k(=^RS#mOXK@FXAC&v#fwC@E)Kfx%Di*pMBZt15&QgkA1P zB}gzpB{66lnmtl{65W6`R#aF3TCwPz5FvU&YQp11SXU%NZ!p z_;uBp-2hPw%20f@Y+|N|PF4%B$A$P~q{AS)4GP2)(Z2Z3a)#ZtxmUB7urD2{us>=? zgoDSr1Fa>vebzQvqe7<2(auKGmP?G5>B8k#Q>Np{5Z=D~LeK8fS}ojJWjBb)&BrYq z0k;edfcJd zIzXDJ^Vm8;B{=#p_5SRP`%dnFf2rYiJ-$r<0n+kF6F-+rWw+gjmXb)A2bw2Bym+9= zMT0!lPJxVsVsBT?0^kmEL}26EAvbNaG{sU-JH-kgh2o9C)weT^=L+*~SCrD2NCmW` zrk3As4_zcQqd>IW;RAVj1{0Co^2XunY~cKS>{zXth2)#-U1^inq||SGn}I(eR;KI@ z5~`>Ea|Y*w!+yxI&36kPSy_#cxXKQ=pLgNFwvszBbA``73|AX?iL{?2jAYGit*b8( z*2VD1h7nE?$yBkf3E-bgpK$i^`MEjVik9s*;}AqrX_2gfR;PLPVZF0#=}B;jB^yl| zCQdUiQuQ2+sJ2>=Szy0BErXxACjoi>@P>8g=NLcFl;qq9Pcw~<>jml`7F2$>p)mbh z<)C`x?h_jPMITK)AU{gXE9h5Flhv~ZEKRpVTBvTbXsHoemyExB@?$CjYslWvthG4? z)3u5?8|=;tE>P_@`t2pNE8|NOxl&&5L+uc;idT5wQp_gY448!7v=eWJ?`0+W(shkb zcgWZ&*TC7p7fjwH$<3UJk_Lr#BOz1$zH6oLDR(o}KL8g_ZfqeXr)R#zeck(kvW6E; zfzAyTfT7MQ{rb&@t=TLm%{;z{r3?(7LpKs`jQkaxI2E}vr5i|DyO*acb7b-N?SYan zF{RsyyS9%Isb5=RSMI-SHd0E^zlbv>Gk+x%BSrl-^ik_NA%??i4nwF~VWi!pd&%dJA|JRR>_KgbeP2vT)HxH!lqn2nI@(HKFv?;+WNo6Z99p3F`wpgMRSQL0E=Z_M5yMrp#taWz9EL{{- zdRrezU7M}*@)+Y{cqH+Y0(*#mweX)V0PcsIJ1DNL_kgyX+a*6u5#O?TJbEkpnbI$7 z(Q?Qq=uMB$2=F;-+@)!k%jL`K8MPFLDBh}8h>WXx20EC^P9tOo6k@6+SFjpIMpD_M z!~PcNwS1)w<92WpXu<0*tVp}<*k=+ue%L6W?z2xd^}vN9{hpqS#F7{}Tf^Oc)iTJj zvLR-YR2GqY+U~{=<+Pxy5;svtA`K7^H#7%_hKBL4|6zzJurHH#$4is!-Sa*mDyb6m z)Q<#m!2xUS4`LX>98nS2|JB}mfHjq^ZNqUaqarFoV1}wx0Rd40QY{cV!H|SV3xfzq z=%GnhMx_}rNRci9LJ9#X0RjZZ1_A<7LN6*kG^I-Q+cW3PIdj_kf8YPU-~0X7|D9Y{ zve(|(YpRM%b?0x7fr$U&7aMmU)dxuAr$^evo@(8%t ztoy{M+Zxn|e6d_?<5J!uZ}i`ivlXEh;;jQt@ig7R#f>&o3Y6;;R#iYQg5^;}-?`n0x*Y+Oe8WP?R=GcRK!tP2I3Isvk3HI-aIl(9O2?x|vEfiDbx4 zx=kmlQkA&T{ybF5(0%Aea;VTXwzq|pN+2~`P@2aU@7s_>VXbFNbA~w9bM#gp!fJAf zm7I`-^@iaj;8DU6Yx5Aq^?i^{tvvzs-p!@K^Am2|`y&c_{9BN(Gi2rG5`bu5v1I4XScQY2og?{NpvG5)RHKjgl9zV+lEoLM@ln{CRhi(CVSyK04-?AF#iHp4P5El5cJMG#*Sfj-%gVTM!-i z4P5*+2^!*e_{ILZbihF#-fQUQXiO3-d)QrOniG*XgkD2|sc{-|hPCl$(ooAu7x0gBBTS+;D1 zqtG(b?*1ZdLiL6hYF#K27#p;*DS|}?i;$hv5*hJlqHRH6J;HakBKi=X?W9|RQ;nOp zc$Z}53oKswgt<$}YrcJbNJ~^h3|uQd#o!}vPzw|F5(f)wO!|#>x6l>kyij6phqmk+ zNrPToXpdD>&_$f{6vW$>xTwgVg4)m-VE^3Sme%}kxH=C2O=P3R18ap^yX)c~8&|s6 z+|FZPC*_hz*^vR~6s@B>7w|1)@?w}e>EbXo(7(OSb2T+RJwxhLuQf-s?A65E*dhbp zkrCJ(*c;f57q%jEiX{yLHx22j)=wS04B7B`^Fy^~@0tbJ2&EWk zctP4H>IfR^E2zmwncTT{t|gjAWNI!>CkM~xLD0DDFUUr&I|&TqmNZ(gk2?6N2{BBc z*LLBlkKF3uYYr1t*gP*xze190N>5sUC0Z_9{%!Ia$Pwnuz!TJHXTTyz7m&D5&HAmU{ zMdK{6mN93oXpMU@qj@pVXsQrXN|X>>;=@XZeF*mFCt1^TJIJ!QsV2(-hYp3QMl$|u z9^#X^YA$Jt{f@{Mw2HGk_Bl^FR>unpi;Lg814y~c=FuE`&>_5;m2vl1$&;<=O-{x3 z0FGz}l;hrTZJ4t3dDLqa@UG8Xyg)a9T8xuXH0k7}Qa4WS=8pYHW`w=``2DV3yWywx zQZgR8F|M;>wt}`VdH&fmRed^2{l=zp+$Vr-F7vcv+g10X9!>sk_ww%g;u~T@Il``~ z4)hR#)Kb(@V0~1vV&Ng$X^s#gX3MdfN}C_JJvmD*jC?`!stFf(b(?8o&Xp=JD!4w) z;(1Gs`*7!39r{IxNuB@ERK8^Aad|j83@ulUZ1VCxmBrYVux*GrF+XU`aCxr~wM_U1 z8keJu3|4O7QtD^bM2q$GLNoB5-qA75{{+fxWlWOKiTy%&c`Hsq7#Mcx2&5s#=n7*qqgrG>`M=xN^Q|JGn4FLGn5>y zgx^^f3s*y5uKR!GukY~>3wWH(cIXBhnGRat`c!fx0R_WDN1-o2f+Ggye_VU890De^J|SeXk127XD+)`U5Inzjn6ZnMW_xzI9>Zi0C(-q(W}-G>=wfs%Gt z83{`O`YcwICEL<85ZAp|T{yEz1O3=+1`@e)zKI)s5W&9?EdW+v@Kk%C$wL zGs7(N*k3Nhr(_FA`j|h!QP*&VQIaq!6&1K0)m~M@S;dQc=(Q6{i8wb@kPP#@CyOJ` zT5Gdmyd+WtnQoXRhyY74^T`KX!a{jN~f06*BDVKTs)LlEWE8LQEi@css7a@|6M6y(W8GZ*{r#)nI?} zWpKNvx>78lVYz>rw=}S)D&$Rl{KLr`v$MB5g}A_YvtemMcZgWol z)+c29TB5-s(MvtJm^bmDt)_lib4k&9n;9uR*+NzH*y;ju%u8!rWQNKFM4otdGw1gf z6p01Jb)6%V&V#^ghJ0 zung(Q?pgPd#QK!1MAd`A;&0#J$@4B$;^I!|QP2{5j%&@ntyQ4~>Cd7Ly6E>dmJ*5L zB*Sed}j_JYW;Ay zJNmQkjXeW^3h!EV4abHt*a!8jMtvdAbP62^RaNuhsim~>fP(_5af$`CR%hpAJ^Vut z%L~yhg~4PD@nrf_(Y0_BHE?#<1208>c)-Gh9@TUv7jzYl^DSWi{-6z0AK=KH8*)05+Y!~F!VD{k& ztdiPM5fQ+y>t!HfE@RB<=VZTIv%T{;j}``W!7J%iA@|n|*{#-VudG=!JrgNhn@SP9u~{O*OkpSKi3-FecYbBp?M{L)I`HxMCf={dK> zZO+$Kq;-~a!SatHHH5sZ*X>*7BxSbd+fJXp($1Z%*&iirn<||QekEyGPF+UI_CQg3 zdMMOH^nHU5QYy8V*(enX9Gp^kpOQ3YS=y5G1_j+dyx<`PrC3B5Z^_hx z8`DAacB|~c4$r)jY71|QJdel$iiBta)U=RDi#Hkcy**2!(;`z+Ac;hm%1ixOwt(zy z=#{SH$$L%S-D}MTf%i^REG08yBS2(W_tHAFTBAlOtiR+ z`d6B*#yIWdHNb!IfheTsnPWH}{t2;0vuyIT9+hs(5NSOd*BKJ;aAD{~m-h76&hX~@ zxDFR!PR-AS5?-?)I$5Yr`)jg;W$J$Z<%r0T9@YeUkt)9c=J}KFyt&w4+{BJc!)oPM~|EKE5#EB+B57|BHFjOPTE4HqRJIoKPEwGvLl z8UljUECJ&7-RYm{g?^&c*QW-vunLQ=+v5%N?)GD5*fsJVGGt~2b-&!xL!zKPqOb(S`HiLDZn zyIls26ZVic^NZ@MtovYZnOgHMH8+o`*?_S9KUEl6Q26kPS`NVuWTyGO@6P_s1M#2O z`F|B$Y2(m4c*7>BC%H2pU-`OD1=-G7q-9R5e^g$wQ1%^EEu=H+>8VDu>BX&Q9{33H zmMRY|HDBl-`v!`>jkmcS#f3Tt8}14BdBEP z*uzO=69j=ZrwsVJ$_{(uK#-8Y-CueL0z*Jo}a!}UJJ`*!gbrE(han+pvS#LUV!|8(^RYa;L@S{NU(?(_+(pG{1 z5S`uTqEgIqPlzPVNs>7H<%kvQBaEKwDwyYBXmOvB*$ixv^Qc^C$dp z6K!N!&BkQ67g?>edsg)d-hsQ3dak-FzLh*#~ z9ESDgfAr9plfKB5RBXEME+rIxkL_8Cp_EGq=(XN)v=}J_hT2>nAmF9L+>;6S)Eux!^PAib7y4;r7Z&^b#$n$mw1UvBhFB@ zlxw7HSL}kCa@i&8$wkxy+t$XNvw^ya%O#f%KhD=A?v@h`~AdA z&l;77JdEn899pGWO?(MTG)xQ3?2~f}L4nht9CV1{b0~QYub_YkoE3*=J8!jlxkOTl zA;Eqq~}9R$N;K= z_fN*gR)Puo%cG-uQ*rTjPha%pMlMk+UGLHO(vFxL$^SFRB7%$e<})xESD~nxX|Mo@|5H zmA^l}(LDA!aeA|GPc@Ul+M zuK}@%4p08>6PuC17CMzzXUkJkQkZ;E9KwUT6DL^00)xZy18=69{LG-&KVY*>^a!o7 z{%OOPueQ!iiv(q*L}cWB11SKU$tUsdCbRrwo9YqHAGB&=HN(>EKB_v6Gm+NUmH)(O^2e4bsFo~h5YKkia6uC*0g%Ze#t)RR(m_bz~n3Mg^ z^-^Q4&Q$B%^0knLoVn!@VBKRoors&cR#BW=+^u$Jk-v{<*fSC$(pIL%D29#4gBVms zeRORLqlzVeVOXSXj_Zm4;u6Y0O=r~tQtV%s9-E#$qH6%IX)|TUrfB(M)?SJSlt?Zu zocZMhw7%H-=vHt|pl90wd~)0{-L`NBpwa6~8e~;|Re0X>%EY{GBY&m`t?eDtiB6Ob zmqXvpTl>{M>`fW8>fJ54KJ0OFh{4KI#78}m`+@TMi|u$01AM^}#HCYsJHSnw8>$mk z8ak%6Fye2^iCr%&Pv4~_IB<8WMV|{Ui=4F{xb^URW`|)XcrJyCYas?~WS+h{)>oAt zc9kZ-m}Huqw0883o0Fb_^bfs&>?(q)qe6)9I`ki?q;Q z?@FrZ>qc0>`!xm9649^iU`+`gZNMv}6{=R{V#`z=3;w!UehF8Pv6N}j|Z zG&e?^Qy6mlpIFqD`ZPtsQazrWV$}nLd7ecJc*$UMql{_?P6eex{MRz z-$E(VIqDiX+TRIod5TFxnpdoO9Njb{uX%WMJ$PbZenB7a{x*7I#1TM`qhOR#gME4{ z&gn153Kll{6;HAaw`ThLdk-<-V4m4&r{*~s)5laf%8c6?YJPGj^4e;smlpt>KwSB* zH=xsRZ7=;mZ+c)0^^@n4XqtENwlz8X9aXpuS~APu1W7frw!%KkNm{Z!mnajsN2S&u zymL6FzJ=j*oKV|~vL#Yd)SrEX$PV_&DK0B%G1YNr7#5O@i-yB_m^GZsAn6&(T&PH4 zwk*muo*SK7l(A#h`?>VZ*c>Z6`=xS03Bq-5SEMVAyeT`De6yIR4T;2sbRs7~=d>ee zDvf!$DOjO%t9_JAc4F>!^-KSm#n4*s2mg5j>EEB;KR@cpK0zsIQM z^~ZZbFQ`|P7r|bx++5R8K;p!SlYA%M|K;re*2IY>IS3f#9sT`8g1)ltVl*y4n8lgo zkOnEefCJ#BpUU6djoMXBqOHFdR`80*4bm;jREf?c$Nf^{I?A|xWMchZ*Y`1u`f{D4 zPLoAzw%-?t+AFwFKmL{`|2zHfdieiL8yH_{wc=EtxS<83uUUOTm?m2^RQYS)4Vg?M z$Yj=S*TOrQ+|LODJU;ev>{@oIQHt>5T zVEwg>vm-;8g7$jsRbk5byS2OO7B9cDBqv9+xLt02>9Gh`G5Yjg>&oxFzzj(J!=K*g zi9hEj0QjrRMeCwJeV_k-*ZUCoxj!LiL1rym;u&cA=fA#xuc!aJw)?w{pa2uj(8J)? z_na3l4>Bix^D&FjX8jq=_`^~PM3R@G#0JT>qTLlzwt@!iy?>0Y;&Ao;&E?7#oR&e>(Sx6_bSt;LFo@$3is+PocU5$uke zN?uHD!iZPCqK5ud1^eCDJr3=l3F2MyWlBi*HxToH_kXPUbb#ixj&)nFu6*BD6`tHO zE9p{<$JUA-C8wtmK&?g@E`18gu9;GC8e1 zU?(QIF=*el{j=PTx$eih1!T|CRo?fMfkQ+aKf)1dD}P?M3Abi3rJ;!j^avVNrDI!} zU_gC)+2W>;R}z_R{7^<}Vq&k{ioUZXe0N{vFMof#06=`gzX(L$aA$p8mkrL8N6+jl*1<$s}dQ?UkQb#xW?eXz^suo zv~$}=Wjf8|(=k;Cy|PX%8!3MSy~c-tHERkPCo=I#E3IZ)zTI!XlJIY*wBsIN3!%`H zCIKYb;0PstMm>;Yh5DTu`|L!JC>D z*IHhc{u6aQt2~`9uSvVjv&QdI`(&#IZ2u;roOz|>?w>;p|FLpF73<1*=eCLyXQe*L zVmLt(ZfcU24qyRot)tIb^=3H}+e?!-caKXG#mdfFnKP;#Vw8nF$&PTe79l}Tb?4bP z&=l^r^oQ@6W$bx+h_Gc%&-v;4KMoxv?n^ykZ(v^`YTxXI}OkTZ>N z3j-xB3RhRHbiMt8&#;JsHs7hLV7?r=>8jFxy@|L54I#r)_H0|mY^&W1N9$-T=R#?% z0uqISn&X}cFVN;{6kNci6-b84obfDp*kd$gsJc;L*WyLVH;`Juo|D*uVCZ_-5&J#Q zg`JkmgDNMofBDzHe__=6rdh$mDonU?gLPDOqqJ*jN#?^_R56VleRoYu zTZWYOpgh~Urw37^rfl2G2(*oj%_L~rtS2dwp4niC%=hC*%n_2VAL5bb&wrY_d-T~q zw1^`j*`i=x6rw#Mv!N&C(M^}*(#n^?Jjdc;8o!>)Kl*w#6g3C=Gz)8X&3O7TUVl=U zMSCE3ZD&A~`K%~6zZ9uIX#F;W$-k7Rw0^d8m_88xlv;g(_I89{KrvQ^<`@wh5c1sg z_UunYQhauzwC(}dnnvaGcwO~;_w)5u0>>Ot?U)JOsVx7J;?%iE?+Pg?>BCL(jJ-)(##!ZXd$<+A?fgRs}a#vI*JfWnIRUQJci1}z5gTwuQRyBfRm zD}4EwG`?JKW^9Ie-We3dqBb$(C#H(|cC^v@dGb?wOC4`61i|8;{UbH0&5z!vNjQ#PF0f4};mX2nCa^A{n?VX0^DZ$mjJoip(~S%J@)=~tO#NffWaNaH4AN7|v-svyJ_)h-i+(|WZ!(qjU zER!t`CjQPp{^sA?oj_G{`A$yQPLX}?n^TxE>5k>FjpfqtC|)@40J>W++>*A2aaxYt z(N0*o1o$PIqFnj}-7vl5*_HZl_ETMAFq1DKw((e(f#hpLeYF>&&|^Y4pf8ilSBUd@ zp4Xj7omPi^mbMOJNy&jt9Sy4u(_VMAvkTAb42JKgT z3|5H|w7l0{;@Q6oEfq~!u<^+^*fQD=&KSKEG?D7?yj3DU^*U0=K+$#@K?I00ioYB@ z{)g`7bJ!=>h6vI(P=NW8NLF5xQ9AB z!#~zMHL0r`z2FS!bD8bu_#=MbK(Jtn=>$t`!~-XBunZ$=t{qV{7- zY2zVmp10A}bHkJ`ms$@scY4#Xdp_SlgD%VdJBR7tKow%!mpZa;0ieAl@cub%S+A~2 z&X0>*IE@<=Op|0Xk9rH+EvOjBdaC0yh;mYRWjjJ$ST4g_-2$|z`~uU{?Bh-1oX4(# za~=a=&ex%f=l`VF^?MumZ3{4FfOz?AN!LY#kDRi;l$l*D@J3zq=O@v>ershT(p4o_ zyCyXq_3~L^_{J%~CHe!a4S>VwJnC=EcN&MOMxwB&_e5p5y4S^0{g}4)EbIaUxnQet zBlERAmUQCrA`)Rko6e^?rl}}dsu!F3=`>1M=RvI?5{V{wrQI{UM^sE|c0$lqqozHf zC462f`b|OruLTi8&SaYxsX!`iU{5OHJx&GGt;VB z#jZcnsdJ#(GZx{oWig5H*<4K2(Ux~~lXT-%bk;;^UMF=>0tn>1?sR9O(};B%;pT(5 z!AV&C_+%B}Xiy)qWy1f|k${epW0EKC>X&e3+^>Hp(;xL{03pLqG}08FM1F>3Dfj&| z%*H;*0C;Px5aN|;N4nu!m-S^y0=&aU2otm)FyR#M$ZrC%mY_u^HwL$(4Wm}i12t;jLHS($BQ)QhFzS% zuAp9SQe&_Gbaj<}sLPL=DVnk=QDE@V_VIOQM*nQ4TG|R@>Ndx7t@~wdr-@d=&G8?h zII8G*(7)F2~sP&@egw zRBLgof9QsBGm!!5Y`!?kUVi%IY%$8bQT5fc0Q@nJ_#Zgq_&P#Sbt#Yb(9~FiW#Jah zWkqrC^Y$7xf>IfAVQJ|^kK+b+MK)*Y>yyj%Iv*>ajM#kxopRZDSHEkW!|}*@SG)ac ziA~uf4eYcw)Lk~FJoCm=mPfUZtRqFP9IH{!ue4$ zUJvyO9<&T|;oG-b5Z+lC;o~df?FWI~bf9VP&!(F@+q_*48+32l<^Oq4;2UU0O!GUv zFlF6}wj*8-Za&MZD3z~De%z^zN6ltR1TUN4ThZ-P!WBXhpZEF9(0v>j^Yz3w8_RWD zF|e+M$^%YX`1+EHX+SOv7{-8s?C)RxMUcaNHQy`A_5G+t-_V{S%7j`au_d0fGEhcb6N?hfkIDsT~btd!l&Y@Q_Z}Px~1*RIlW! zLzX?ZoUH3_MjZ*9=#B$0!!WV(xVnut38>YLJ*jKNG9d6@F0PNTTsl@Z$F-Z7(5w(B zd%AEG?I~eB0mZQ3`X9DJEYKPQ4kh%PM4caO*Qx7@>SBtQkz-7a(T4&j@tYf_WYmXL+8+Qh0E9fyb0?5FFN+7 zPlExdUX8yXYt2quAYP#SB8Bq=qW7j&zh?f3l}=WcOuz-oEkjau&^i&lgVY$gTVu%s zi+VHJhn{h({*&4ap}B9!9lcF;qqCEGmdB&cy;sMFBV->SUoKC5e}Df7u6%gG1K+3V z?<91qMHzeAFQk7CKViCdMeFoGx!PY23C-N*6oVBCpOMl@xdS7yKd={VrO}5oxAr{~ znj>>=%h{XRnL{AD$}GNjmfB2H56!c>Hr;6l;XlEn{vz zYtx=vNX_mrnB=h-f3zt7V{H;@$H{aRbJyoDW&ZPoD&mEL9z8Z&rd6Uw84t+OJm&ss zqtD7SiAR#rJ2-f|FK_DJym@Tyhku^^&qt3xsB8|Xf!MiMqAejBbZyNEy|3cg)-A^A zvb};WL6j+2Y0DeOC&SNN|CHf-s*t~Xk43Mp0YXYxK1RkJcB7xwGVh>0o%FBE*>`7w zHq&SF4J4@4PcaZP;mpNnSKYSRzlN&8ZlTL0@dOCidQSl3>2=PfB0V9UtK(O!G#s$T zG-a%wp9t%oullOsy{8*7m6ec`vupe!K4T^P;Xe2*9^&7_E+&c zWH?{q)?n-;wAiCF2)TUKMow47E0`(9Y6klb#yxm@l8SZdv4olqt6E7Kc88>Z(ZbCO z`fnk6XnlBNixHvoi^%n6VZz0964>A|IJBC7vwR1fo*%dCX*JPoN z$!P5qI2)TS_f&(|JrHGZjd4UoXhN^!Nm*#~$L_!dnHtqyJJDB*;I1AsDU=H0%`xhd zHVbdN4IXni){p9C4SsoUgw}f8qbtLc5X=-Nr$MKOk~jyQB*ggQs+%j#FLa;MtN28-@37+SJ{khAI?pAbo5vn7J0L(9DFdipguWr#d z_>jrj2zjXHf`e2;R+~0#-dS z^Xp2b-UjA*iq^rCCZ$7+DhoEEmu=CwnVX`<{OWdoiewwjG*#D97gPVUF7hAS*LOkB zzk%{IyuL{9K8gNOt3RU4tm^BN@aEV0Lhf58wO@6&`A^>T1yr1B8}45&bvSgCk-1>D z5#PQ>n-W{!DGW?_3t{yb(Jsr_951fm)lOdUqt`5Db}5}xP(UWT^VAro`VYk%r4p5pQ}`KVy73&@g}utKtTOUu zvG^~IT(3i|dQ+*B^QJ49=mU|dj=@wc=HigkHqgWAGrJ#0&z zvwWvU;Zw>M`qP0^L*;P`9i|qeGg|3)qIkT?fw&gqVH6uz#sjBKl1m285DvC;3c$Jio zEkSFvRLjvk!Tykw>LR?;$sLx8zOC}D>H8LXH82fEb{SsG8bG9=%d#Gfa!jXEno&H# z#C6f7YMTr{^oU-0C-e#>rE!BP6_``=)NI(--s{?%LAK0DKqA1ZH72MLl=)V;ccb}! z6LW^Lq&4J6i|=%J?rLoi^!W9_Ej?2wx>;i=IwmjTV_;z}*rQ+Kn<4C$ zpq`md>nl`@yVL_)!L-lSgr=||^ULN4uQT*P%hX2{g29_3rDz5Xp_k5U zG(*ZX+7c8fq09!@Q z0bP}=pbUx=c8$TOW-S-uXXfVmF--4~WibG1#oq)VEgjnDv4eP0X~r3n$^84C5sxLj zot7us%V{VJ#e-JBxuhcbLU%xarr-K&t2hyYnmBdg_`hTo6a4s)ANOg z_RQd6nNO&)W(Ly<~biTJ->kr6L*pq zuA5ke)v)mXrpMwBCr5xq>WV+N&mlgA@H*G(+aXgou$uNMKGn2XW|dEluCSKpc#6hP zM=i$+7E!@vBI=Sx2EK`&-<1_A=xYw$M>R2c%=46N6sSc5b`9oTW!)l+2VM9naCq@c zYDxB^Q4EEB_;O2ns@q|pj1It;jGpC)Q!F;hfhvAQP%U{M)c@#@)w zTqeJIvy21~Y-0_o32RA*-=yA5e>rO~DSuFb7p+-DOTMuYQ5H?j-Z|-EbhCQ4wyt;R zjm8T!KiDP-h->_HIN&27Ijue0M*eM{V1=CNV+ZKCL3JmnIRs2>5`M zp4o4y#ONZ-n6?O&JUOq4u3zK`R6dmU zPO)TbpwB73BzjH?QsbvHuWBM`OvhD4)$O@<2el?| zWQ!%Paq2@DpN8P5b3NIxfx%|c;$fVos3$rFKD=}Cs46uwI3&M!cT~aD_nc6vcx}+k z!mt_x$Knznd*0@V)*FnGcvj&1Bk5-GTmE6Ulv)=ey1tez6CJ(P% zO#Q^D-U6~c>&&l zN(Z3gIXY%4w+yTRwYmYKtf9-fo#;DDgDKlrDpr|^hm=ujBag&armE-lIxZya2u z!)Mp=B5iofD_i6=E;JP&Z!z6Qm(-=~01?%PJuau0%u(PVS~)PW&zPM4+O=112-pq@ zvJn6lHTm=5^f}I~M}}o}U-vlP%z7X#JI#{=%<6Aq;~;t1idD3YNN;w^%-cj+*=fw3 zcJB$;Z8Ej46U>);m;X4ulOL~8IO!j@B^S4nJqm_gkjSVVVhv_(+WIx}mE8S@{tg5> zu;KtfK~%>+nQhCQJb!SzWF3X;sVS>mF!9cNDBGoVK`&t0vy?t>?qEOXkv6Q7m8^*J z?1-23n?bRMtp~mWO7sLlsle|yb zrbjp>iD@84!e_`>cV)vT66w{x>2XPbWS3Xf>KVZy{X$YtdTxwounE*a>|mmdkw_-9 zd44AQ69`nR+PdnRUnjmBbF81e{O}dsF?(jc>A?G%1IfD*II5i7hRiva!K-%#EQ(019*Uk4WTZK;1y>4#E=q&1g10~(8B(e*!t~;DS4U}7_xIHopfwl03| z(WJma{=I`^1?#TIlj{)}uZ^uk2n)^F*KSpTYz-&7c+8K@puze)QqSN^Y|AmaB)sx) zHswLEnp|;HY)S0_t$nKsdu`?wcHcYr7ta}S&Hlh9NMX$EUc05w`$D1DzAp{4fjU%M z4wWdn2h9Bpr%AB*wJ&4`;^gFrCHHLh@%U8T;&a6rH(>>(o=a?OY;=cN^#z7#*1ww` z`Z=l+Z)ZUEOiEIXl33_nXVM9jhpf3r=Mf&yO)#is$>55+$)F?BG zbS&%n<&K;FzF=^%-ug%6r@KmKHbi>u)oQ72Q_Reel&(-yENVfo5yhKZ z>C0O=YImguEeusiJ+~9NQT-rbOgcYdk4rkee}+37=$gU{zfgAdMU@f@Hv@JzD5TdhZsRqFUsZC>}9=>m*($-KVt zdYI_66@UeT4ad>gN9ZX_!~ipw>Q<+^i`VV38P)8#qoY>jkEh>r%Py&!5vIHH}w~@-O5pz+p$Q3~y5-V@E}QEX<} zq$yR#lhZ{Ww{!}z40;Os9pEJjl$UT07N-K51smeBTHiq5 zo7rrSJM4vfZC*iu0N(!>-IjY_`y+BP_OAc&%WFKQ-%bDVfbNb6#A^v3wYk|ac^G=0 zE9VdUCqDbJ8|qcXrl)|B>I~m6iV=T2aQP2K-2Zo(UYq~>>Na_9;b7={_VqU*u~M;q z7_i=PbmNY{9!d|1Ttr^`m;R>z%C>)ScNyDfsR=F{zr|v|x_XWwir`ye3N}sBu?*Wf z0wPdryQgk%(J%Eg32^M4jM{E`@rye?Z^Tqzt?B>zI^sp zu-VEml;fOk&|Anl4C3WF{IQ`WhE)yzzP<~154<&v6!ySlyzt8yJh5kSO zGyie*%I$3$&(TBx`OY#cxsN3&atX32cZA~|&-XzD|V40 literal 0 HcmV?d00001 From 4a966e5e5290f28b2b453851aaa0c91c1300e63d Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:23:38 +0700 Subject: [PATCH 006/145] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f26b1ef2..fbf8eb7d 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ A: Some countries have restricted access to certain streaming service APIs. If d _If this software is useful and brings you value, consider supporting the project by buying me a coffee. Your support helps keep development going._ -[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/zarzet) Buy Me A Coffee +[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/zarzet) ## Disclaimer From 7cc1fef989552e689519a9776ef3e89760f850c7 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 10 Feb 2026 09:07:18 +0700 Subject: [PATCH 007/145] feat: primary artist only folders, fix notifications v20, fix SAF duplicate dirs - Add 'Use Primary Artist Only' setting to strip featured artists from folder names - Fix flutter_local_notifications v20 breaking changes (positional params) - Fix SAF duplicate folder bug: synchronized ensureDocumentDir to prevent race condition creating empty folders with (1), (2) suffixes during concurrent downloads --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 42 ++++++++---- lib/l10n/app_localizations.dart | 18 ++++++ lib/l10n/app_localizations_de.dart | 11 ++++ lib/l10n/app_localizations_en.dart | 11 ++++ lib/l10n/app_localizations_es.dart | 11 ++++ lib/l10n/app_localizations_fr.dart | 11 ++++ lib/l10n/app_localizations_hi.dart | 11 ++++ lib/l10n/app_localizations_id.dart | 11 ++++ lib/l10n/app_localizations_ja.dart | 11 ++++ lib/l10n/app_localizations_ko.dart | 11 ++++ lib/l10n/app_localizations_nl.dart | 11 ++++ lib/l10n/app_localizations_pt.dart | 11 ++++ lib/l10n/app_localizations_ru.dart | 11 ++++ lib/l10n/app_localizations_tr.dart | 11 ++++ lib/l10n/app_localizations_zh.dart | 11 ++++ lib/l10n/arb/app_en.arb | 6 ++ lib/l10n/arb/app_id.arb | 12 ++++ lib/models/settings.dart | 5 ++ lib/models/settings.g.dart | 2 + lib/providers/download_queue_provider.dart | 28 +++++++- lib/providers/settings_provider.dart | 5 ++ .../settings/download_settings_page.dart | 12 ++++ lib/services/notification_service.dart | 62 +++++++++--------- pubspec.lock | 64 +++++++++++-------- 24 files changed, 326 insertions(+), 73 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 30bfdd77..d0f27b65 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -33,6 +33,7 @@ class MainActivity: FlutterFragmentActivity() { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var pendingSafTreeResult: MethodChannel.Result? = null private val safScanLock = Any() + private val safDirLock = Any() private var safScanProgress = SafScanProgress() @Volatile private var safScanCancel = false @Volatile private var safScanActive = false @@ -300,19 +301,36 @@ class MainActivity: FlutterFragmentActivity() { } private fun ensureDocumentDir(treeUri: Uri, relativeDir: String): DocumentFile? { - var current = DocumentFile.fromTreeUri(this, treeUri) ?: return null - if (relativeDir.isBlank()) return current - - val parts = relativeDir.split("/").filter { it.isNotBlank() } - for (part in parts) { - val existing = current.findFile(part) - current = if (existing != null && existing.isDirectory) { - existing - } else { - current.createDirectory(part) ?: return null - } + if (relativeDir.isBlank()) { + return DocumentFile.fromTreeUri(this, treeUri) + } + + // Synchronize to prevent concurrent downloads from creating duplicate + // directories with (1), (2) suffixes via SAF's auto-rename behavior. + synchronized(safDirLock) { + var current = DocumentFile.fromTreeUri(this, treeUri) ?: return null + + val parts = relativeDir.split("/").filter { it.isNotBlank() } + for (part in parts) { + val existing = current.findFile(part) + current = if (existing != null && existing.isDirectory) { + existing + } else { + val created = current.createDirectory(part) ?: return null + // SAF may auto-rename to "part (1)" if another thread just created it. + // Re-check: if the created name differs, delete it and use the original. + val createdName = created.name ?: part + if (createdName != part) { + // Another thread won the race; delete the duplicate and use theirs. + created.delete() + current.findFile(part) ?: return null + } else { + created + } + } + } + return current } - return current } private fun findDocumentDir(treeUri: Uri, relativeDir: String): DocumentFile? { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index abe371bc..d7713832 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3550,6 +3550,24 @@ abstract class AppLocalizations { /// **'Artist folders use Track Artist only'** String get downloadUseAlbumArtistForFoldersTrackSubtitle; + /// Setting - strip featured artists from folder name + /// + /// In en, this message translates to: + /// **'Primary artist only for folders'** + String get downloadUsePrimaryArtistOnly; + + /// Subtitle when primary artist only is enabled + /// + /// In en, this message translates to: + /// **'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'** + String get downloadUsePrimaryArtistOnlyEnabled; + + /// Subtitle when primary artist only is disabled + /// + /// In en, this message translates to: + /// **'Full artist string used for folder name'** + String get downloadUsePrimaryArtistOnlyDisabled; + /// Setting - output file format /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ffeb6845..e11c2238 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1960,6 +1960,17 @@ class AppLocalizationsDe extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 25a79292..784777f1 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1945,6 +1945,17 @@ class AppLocalizationsEn extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 2ee6b9a5..d1eda587 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1945,6 +1945,17 @@ class AppLocalizationsEs extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d2b7fe4d..600c9468 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1945,6 +1945,17 @@ class AppLocalizationsFr extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 5e635a9f..88daceea 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1945,6 +1945,17 @@ class AppLocalizationsHi extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 6885b02b..532d79e1 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1958,6 +1958,17 @@ class AppLocalizationsId extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Folder artis hanya memakai Track Artist'; + @override + String get downloadUsePrimaryArtistOnly => 'Hanya artis utama untuk folder'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artist dihapus dari nama folder (misal Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Nama artis lengkap dipakai untuk folder'; + @override String get downloadSaveFormat => 'Simpan Format'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 8a46307c..41f762b1 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1933,6 +1933,17 @@ class AppLocalizationsJa extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => '形式を保存'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 63c65cba..f71686e0 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1945,6 +1945,17 @@ class AppLocalizationsKo extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e144e991..453c445f 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1945,6 +1945,17 @@ class AppLocalizationsNl extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 6ffcf11c..722e3afe 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1945,6 +1945,17 @@ class AppLocalizationsPt extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 14844d15..331eaea1 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1983,6 +1983,17 @@ class AppLocalizationsRu extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Формат сохранения'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 79eca336..dd794add 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1960,6 +1960,17 @@ class AppLocalizationsTr extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 938b5f94..b3478881 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1945,6 +1945,17 @@ class AppLocalizationsZh extends AppLocalizations { String get downloadUseAlbumArtistForFoldersTrackSubtitle => 'Artist folders use Track Artist only'; + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 8187756d..6b7d627f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1431,6 +1431,12 @@ "@downloadUseAlbumArtistForFoldersAlbumSubtitle": {"description": "Subtitle when Album Artist is used for folder naming"}, "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", "@downloadUseAlbumArtistForFoldersTrackSubtitle": {"description": "Subtitle when Track Artist is used for folder naming"}, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": {"description": "Setting - strip featured artists from folder name"}, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": {"description": "Subtitle when primary artist only is enabled"}, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": {"description": "Subtitle when primary artist only is disabled"}, "downloadSaveFormat": "Save Format", "@downloadSaveFormat": {"description": "Setting - output file format"}, "downloadSelectService": "Select Service", diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 84ee93e9..648e367f 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -2489,6 +2489,18 @@ "@downloadUseAlbumArtistForFoldersTrackSubtitle": { "description": "Subtitle when Track Artist is used for folder naming" }, + "downloadUsePrimaryArtistOnly": "Hanya artis utama untuk folder", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artist dihapus dari nama folder (misal Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Nama artis lengkap dipakai untuk folder", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Simpan Format", "@downloadSaveFormat": { "description": "Setting - output file format" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index fac29ac8..11c904c7 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -20,6 +20,7 @@ class AppSettings { final bool hasSearchedBefore; final String folderOrganization; final bool useAlbumArtistForFolders; + final bool usePrimaryArtistOnly; // Strip featured artists from folder name final String historyViewMode; final String historyFilterMode; final bool askQualityBeforeDownload; @@ -65,6 +66,7 @@ class AppSettings { this.hasSearchedBefore = false, this.folderOrganization = 'none', this.useAlbumArtistForFolders = true, + this.usePrimaryArtistOnly = false, this.historyViewMode = 'grid', this.historyFilterMode = 'all', this.askQualityBeforeDownload = true, @@ -109,6 +111,7 @@ class AppSettings { bool? hasSearchedBefore, String? folderOrganization, bool? useAlbumArtistForFolders, + bool? usePrimaryArtistOnly, String? historyViewMode, String? historyFilterMode, bool? askQualityBeforeDownload, @@ -154,6 +157,8 @@ class AppSettings { folderOrganization: folderOrganization ?? this.folderOrganization, useAlbumArtistForFolders: useAlbumArtistForFolders ?? this.useAlbumArtistForFolders, + usePrimaryArtistOnly: + usePrimaryArtistOnly ?? this.usePrimaryArtistOnly, historyViewMode: historyViewMode ?? this.historyViewMode, historyFilterMode: historyFilterMode ?? this.historyFilterMode, askQualityBeforeDownload: askQualityBeforeDownload ?? this.askQualityBeforeDownload, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 9805762e..51c83220 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -23,6 +23,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( hasSearchedBefore: json['hasSearchedBefore'] as bool? ?? false, folderOrganization: json['folderOrganization'] as String? ?? 'none', useAlbumArtistForFolders: json['useAlbumArtistForFolders'] as bool? ?? true, + usePrimaryArtistOnly: json['usePrimaryArtistOnly'] as bool? ?? false, historyViewMode: json['historyViewMode'] as String? ?? 'grid', historyFilterMode: json['historyFilterMode'] as String? ?? 'all', askQualityBeforeDownload: json['askQualityBeforeDownload'] as bool? ?? true, @@ -70,6 +71,7 @@ Map _$AppSettingsToJson(AppSettings instance) => 'hasSearchedBefore': instance.hasSearchedBefore, 'folderOrganization': instance.folderOrganization, 'useAlbumArtistForFolders': instance.useAlbumArtistForFolders, + 'usePrimaryArtistOnly': instance.usePrimaryArtistOnly, 'historyViewMode': instance.historyViewMode, 'historyFilterMode': instance.historyFilterMode, 'askQualityBeforeDownload': instance.askQualityBeforeDownload, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 6e7fdf75..0a634496 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1033,11 +1033,15 @@ class DownloadQueueNotifier extends Notifier { bool separateSingles = false, String albumFolderStructure = 'artist_album', bool useAlbumArtistForFolders = true, + bool usePrimaryArtistOnly = false, }) async { String baseDir = state.outputDir; - final folderArtist = useAlbumArtistForFolders + var folderArtist = useAlbumArtistForFolders ? _normalizeOptionalString(track.albumArtist) ?? track.artistName : track.artistName; + if (usePrimaryArtistOnly) { + folderArtist = _extractPrimaryArtist(folderArtist); + } if (separateSingles) { final isSingle = track.isSingle; @@ -1129,6 +1133,19 @@ class DownloadQueueNotifier extends Notifier { .trim(); } + static final _featuredArtistPattern = RegExp( + r'\s*[,;&]\s*|\s+(?:feat\.?|ft\.?|featuring|with|x)\s+', + caseSensitive: false, + ); + + String _extractPrimaryArtist(String artist) { + final match = _featuredArtistPattern.firstMatch(artist); + if (match != null && match.start > 0) { + return artist.substring(0, match.start).trim(); + } + return artist; + } + bool _isSafMode(AppSettings settings) { return Platform.isAndroid && settings.storageMode == 'saf' && @@ -1152,10 +1169,14 @@ class DownloadQueueNotifier extends Notifier { bool separateSingles = false, String albumFolderStructure = 'artist_album', bool useAlbumArtistForFolders = true, + bool usePrimaryArtistOnly = false, }) async { - final folderArtist = useAlbumArtistForFolders + var folderArtist = useAlbumArtistForFolders ? _normalizeOptionalString(track.albumArtist) ?? track.artistName : track.artistName; + if (usePrimaryArtistOnly) { + folderArtist = _extractPrimaryArtist(folderArtist); + } if (separateSingles) { final isSingle = track.isSingle; @@ -2565,6 +2586,7 @@ class DownloadQueueNotifier extends Notifier { separateSingles: settings.separateSingles, albumFolderStructure: settings.albumFolderStructure, useAlbumArtistForFolders: settings.useAlbumArtistForFolders, + usePrimaryArtistOnly: settings.usePrimaryArtistOnly, ) : ''; String? appOutputDir; @@ -2576,6 +2598,7 @@ class DownloadQueueNotifier extends Notifier { separateSingles: settings.separateSingles, albumFolderStructure: settings.albumFolderStructure, useAlbumArtistForFolders: settings.useAlbumArtistForFolders, + usePrimaryArtistOnly: settings.usePrimaryArtistOnly, ); var effectiveOutputDir = initialOutputDir; var effectiveSafMode = isSafMode; @@ -2873,6 +2896,7 @@ class DownloadQueueNotifier extends Notifier { separateSingles: settings.separateSingles, albumFolderStructure: settings.albumFolderStructure, useAlbumArtistForFolders: settings.useAlbumArtistForFolders, + usePrimaryArtistOnly: settings.usePrimaryArtistOnly, ); final fallbackResult = await runDownload( useSaf: false, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 7a572567..fb684bb7 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -231,6 +231,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setUsePrimaryArtistOnly(bool enabled) { + state = state.copyWith(usePrimaryArtistOnly: enabled); + _saveSettings(); + } + void setHistoryViewMode(String mode) { state = state.copyWith(historyViewMode: mode); _saveSettings(); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index ca18ed54..0638e847 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -365,6 +365,18 @@ class _DownloadSettingsPageState extends ConsumerState { .setUseAlbumArtistForFolders(value), showDivider: false, ), + SettingsSwitchItem( + icon: Icons.person_outline, + title: context.l10n.downloadUsePrimaryArtistOnly, + subtitle: settings.usePrimaryArtistOnly + ? context.l10n.downloadUsePrimaryArtistOnlyEnabled + : context.l10n.downloadUsePrimaryArtistOnlyDisabled, + value: settings.usePrimaryArtistOnly, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setUsePrimaryArtistOnly(value), + showDivider: false, + ), ], ), ), diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 430c779c..13d84e72 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -30,7 +30,7 @@ class NotificationService { iOS: iosSettings, ); - await _notifications.initialize(settings: initSettings); + await _notifications.initialize(initSettings); if (Platform.isAndroid) { await _notifications @@ -90,10 +90,10 @@ class NotificationService { ); await _notifications.show( - id: downloadProgressId, - title: 'Downloading $trackName', - body: '$artistName • $percentage%', - notificationDetails: details, + downloadProgressId, + 'Downloading $trackName', + '$artistName • $percentage%', + details, ); } @@ -133,10 +133,10 @@ class NotificationService { ); await _notifications.show( - id: downloadProgressId, - title: 'Finalizing $trackName', - body: '$artistName • Embedding metadata...', - notificationDetails: details, + downloadProgressId, + 'Finalizing $trackName', + '$artistName • Embedding metadata...', + details, ); } @@ -183,10 +183,10 @@ class NotificationService { ); await _notifications.show( - id: downloadProgressId, - title: title, - body: '$trackName - $artistName', - notificationDetails: details, + downloadProgressId, + title, + '$trackName - $artistName', + details, ); } @@ -223,15 +223,15 @@ class NotificationService { ); await _notifications.show( - id: downloadProgressId, - title: title, - body: '$completedCount tracks downloaded successfully', - notificationDetails: details, + downloadProgressId, + title, + '$completedCount tracks downloaded successfully', + details, ); } Future cancelDownloadNotification() async { - await _notifications.cancel(id: downloadProgressId); + await _notifications.cancel(downloadProgressId); } Future showUpdateDownloadProgress({ @@ -274,10 +274,10 @@ class NotificationService { ); await _notifications.show( - id: updateDownloadId, - title: 'Downloading SpotiFLAC v$version', - body: '$receivedMB / $totalMB MB • $percentage%', - notificationDetails: details, + updateDownloadId, + 'Downloading SpotiFLAC v$version', + '$receivedMB / $totalMB MB • $percentage%', + details, ); } @@ -307,10 +307,10 @@ class NotificationService { ); await _notifications.show( - id: updateDownloadId, - title: 'Update Ready', - body: 'SpotiFLAC v$version downloaded. Tap to install.', - notificationDetails: details, + updateDownloadId, + 'Update Ready', + 'SpotiFLAC v$version downloaded. Tap to install.', + details, ); } @@ -339,14 +339,14 @@ class NotificationService { ); await _notifications.show( - id: updateDownloadId, - title: 'Update Failed', - body: 'Could not download update. Try again later.', - notificationDetails: details, + updateDownloadId, + 'Update Failed', + 'Could not download update. Try again later.', + details, ); } Future cancelUpdateNotification() async { - await _notifications.cancel(id: updateDownloadId); + await _notifications.cancel(updateDownloadId); } } diff --git a/pubspec.lock b/pubspec.lock index a250b404..59901618 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -189,10 +189,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "33bae12a398f841c6cda09d1064212957265869104c478e5ad51e2fb26c3973c" + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "6.1.5" connectivity_plus_platform_interface: dependency: transitive description: @@ -386,34 +386,34 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "76cd20bcfa72fabe50ea27eeaf165527f446f55d3033021462084b87805b4cac" + sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875" url: "https://pub.dev" source: hosted - version: "20.0.0" + version: "19.5.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 + sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "6.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" + sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "9.1.0" flutter_local_notifications_windows: dependency: transitive description: name: flutter_local_notifications_windows - sha256: "7ddd964fa85b6a23e96956c5b63ef55cdb9e5947b71b95712204db42ad46da61" + sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.3" flutter_localizations: dependency: "direct main" description: flutter @@ -439,50 +439,50 @@ packages: dependency: "direct main" description: name: flutter_secure_storage - sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" url: "https://pub.dev" source: hosted - version: "10.0.0" - flutter_secure_storage_darwin: - dependency: transitive - description: - name: flutter_secure_storage_darwin - sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" - url: "https://pub.dev" - source: hosted - version: "0.2.0" + version: "9.2.4" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "1.1.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "1.2.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "3.1.2" flutter_svg: dependency: "direct main" description: @@ -741,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + palette_generator: + dependency: "direct main" + description: + name: palette_generator + sha256: "4420f7ccc3f0a4a906144e73f8b6267cd940b64f57a7262e95cb8cec3a8ae0ed" + url: "https://pub.dev" + source: hosted + version: "0.3.3+7" path: dependency: "direct main" description: From 05d25d4d7ca278f81154bfd1a852bbd8aeea1c0f Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 10 Feb 2026 10:11:02 +0700 Subject: [PATCH 008/145] v3.6.1: fix lyrics_mode, notification v20, SAF duplicate, primary artist setting, unified download strategy --- CHANGELOG.md | 16 + .../kotlin/com/zarz/spotiflac/MainActivity.kt | 9 + go_backend/exports.go | 205 +- lib/constants/app_info.dart | 4 +- lib/providers/download_queue_provider.dart | 118 +- lib/services/download_request_payload.dart | 154 + lib/services/notification_service.dart | 62 +- lib/services/platform_bridge.dart | 2656 +++++++++-------- pubspec.lock | 64 +- pubspec.yaml | 2 +- 10 files changed, 1811 insertions(+), 1479 deletions(-) create mode 100644 lib/services/download_request_payload.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index cdb01dd3..de083e93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [3.6.1] - 2026-02-10 + +### Added + +- "Use Primary Artist Only" setting: strips featured artists from folder names (e.g. "Justin Bieber, Quavo" becomes "Justin Bieber") for cleaner folder organization + - Supports separators: `, ` `;` `&` `feat.` `ft.` `featuring` `with` `x` + - Available in Settings > Download > below "Use Album Artist for folders" + +### Fixed + +- Fixed lyrics mode "External .LRC" still embedding lyrics into metadata - `lyrics_mode` was not being sent to Go backend for single-service downloads and YouTube provider, causing Go to default to "embed" +- Fixed `flutter_local_notifications` v20 breaking changes - migrated all `initialize()`, `show()`, and `cancel()` calls from positional parameters to named parameters +- Fixed SAF duplicate folder bug: concurrent batch downloads creating empty folders with `(1)`, `(2)`, `(3)` suffixes - added synchronized lock to `ensureDocumentDir` in Kotlin with duplicate detection and cleanup + +--- + ## [3.6.0] - 2026-02-09 ### Highlights diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index d0f27b65..52615fbe 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -1312,6 +1312,15 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } + "downloadByStrategy" -> { + val requestJson = call.arguments as String + val response = withContext(Dispatchers.IO) { + handleSafDownload(requestJson) { json -> + Gobackend.downloadByStrategy(json) + } + } + result.success(response) + } "getDownloadProgress" -> { val response = withContext(Dispatchers.IO) { Gobackend.getDownloadProgress() diff --git a/go_backend/exports.go b/go_backend/exports.go index a30e1628..b6b7a630 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -150,6 +150,8 @@ type DownloadRequest struct { QobuzID string `json:"qobuz_id,omitempty"` DeezerID string `json:"deezer_id,omitempty"` LyricsMode string `json:"lyrics_mode,omitempty"` + UseExtensions bool `json:"use_extensions,omitempty"` + UseFallback bool `json:"use_fallback,omitempty"` } type DownloadResponse struct { @@ -192,6 +194,73 @@ type DownloadResult struct { LyricsLRC string } +func buildDownloadSuccessResponse( + req DownloadRequest, + result DownloadResult, + service string, + message string, + filePath string, + alreadyExists bool, +) DownloadResponse { + title := result.Title + if title == "" { + title = req.TrackName + } + + artist := result.Artist + if artist == "" { + artist = req.ArtistName + } + + album := result.Album + if album == "" { + album = req.AlbumName + } + + releaseDate := result.ReleaseDate + if releaseDate == "" { + releaseDate = req.ReleaseDate + } + + trackNumber := result.TrackNumber + if trackNumber == 0 { + trackNumber = req.TrackNumber + } + + discNumber := result.DiscNumber + if discNumber == 0 { + discNumber = req.DiscNumber + } + + isrc := result.ISRC + if isrc == "" { + isrc = req.ISRC + } + + return DownloadResponse{ + Success: true, + Message: message, + FilePath: filePath, + AlreadyExists: alreadyExists, + ActualBitDepth: result.BitDepth, + ActualSampleRate: result.SampleRate, + Service: service, + Title: title, + Artist: artist, + Album: album, + AlbumArtist: req.AlbumArtist, + ReleaseDate: releaseDate, + TrackNumber: trackNumber, + DiscNumber: discNumber, + ISRC: isrc, + CoverURL: req.CoverURL, + Genre: req.Genre, + Label: req.Label, + Copyright: req.Copyright, + LyricsLRC: result.LyricsLRC, + } +} + func DownloadTrack(requestJSON string) (string, error) { var req DownloadRequest if err := json.Unmarshal([]byte(requestJSON), &req); err != nil { @@ -301,22 +370,14 @@ func DownloadTrack(requestJSON string) (string, error) { result.BitDepth = quality.BitDepth result.SampleRate = quality.SampleRate } - resp := DownloadResponse{ - Success: true, - Message: "File already exists", - FilePath: actualPath, - AlreadyExists: true, - ActualBitDepth: result.BitDepth, - ActualSampleRate: result.SampleRate, - Service: req.Service, - Title: result.Title, - Artist: result.Artist, - Album: result.Album, - ReleaseDate: result.ReleaseDate, - TrackNumber: result.TrackNumber, - DiscNumber: result.DiscNumber, - ISRC: result.ISRC, - } + resp := buildDownloadSuccessResponse( + req, + result, + req.Service, + "File already exists", + actualPath, + true, + ) jsonBytes, _ := json.Marshal(resp) return string(jsonBytes), nil } @@ -330,27 +391,54 @@ func DownloadTrack(requestJSON string) (string, error) { GoLog("[Download] Could not read quality from file: %v\n", qErr) } - resp := DownloadResponse{ - Success: true, - Message: "Download complete", - FilePath: result.FilePath, - ActualBitDepth: result.BitDepth, - ActualSampleRate: result.SampleRate, - Service: req.Service, - Title: result.Title, - Artist: result.Artist, - Album: result.Album, - ReleaseDate: result.ReleaseDate, - TrackNumber: result.TrackNumber, - DiscNumber: result.DiscNumber, - ISRC: result.ISRC, - LyricsLRC: result.LyricsLRC, - } + resp := buildDownloadSuccessResponse( + req, + result, + req.Service, + "Download complete", + result.FilePath, + false, + ) jsonBytes, _ := json.Marshal(resp) return string(jsonBytes), nil } +// DownloadByStrategy routes a unified download request to the appropriate flow. +// Routing priority: YouTube service > extension fallback > built-in fallback > direct service. +func DownloadByStrategy(requestJSON string) (string, error) { + var req DownloadRequest + if err := json.Unmarshal([]byte(requestJSON), &req); err != nil { + return errorResponse("Invalid request: " + err.Error()) + } + + service := strings.TrimSpace(strings.ToLower(req.Service)) + req.Service = service + normalizedBytes, err := json.Marshal(req) + if err != nil { + return errorResponse("Invalid request: " + err.Error()) + } + normalizedJSON := string(normalizedBytes) + + if service == "youtube" { + return DownloadFromYouTube(normalizedJSON) + } + + if req.UseExtensions { + resp, err := DownloadWithExtensionsJSON(normalizedJSON) + if err != nil { + return errorResponse(err.Error()) + } + return resp, nil + } + + if req.UseFallback { + return DownloadWithFallback(normalizedJSON) + } + + return DownloadTrack(normalizedJSON) +} + func DownloadWithFallback(requestJSON string) (string, error) { var req DownloadRequest if err := json.Unmarshal([]byte(requestJSON), &req); err != nil { @@ -470,23 +558,14 @@ func DownloadWithFallback(requestJSON string) (string, error) { result.BitDepth = quality.BitDepth result.SampleRate = quality.SampleRate } - resp := DownloadResponse{ - Success: true, - Message: "File already exists", - FilePath: actualPath, - AlreadyExists: true, - ActualBitDepth: result.BitDepth, - ActualSampleRate: result.SampleRate, - Service: service, - Title: result.Title, - Artist: result.Artist, - Album: result.Album, - ReleaseDate: result.ReleaseDate, - TrackNumber: result.TrackNumber, - DiscNumber: result.DiscNumber, - ISRC: result.ISRC, - LyricsLRC: result.LyricsLRC, - } + resp := buildDownloadSuccessResponse( + req, + result, + service, + "File already exists", + actualPath, + true, + ) jsonBytes, _ := json.Marshal(resp) return string(jsonBytes), nil } @@ -500,22 +579,14 @@ func DownloadWithFallback(requestJSON string) (string, error) { GoLog("[Download] Could not read quality from file: %v\n", qErr) } - resp := DownloadResponse{ - Success: true, - Message: "Downloaded from " + service, - FilePath: result.FilePath, - ActualBitDepth: result.BitDepth, - ActualSampleRate: result.SampleRate, - Service: service, - Title: result.Title, - Artist: result.Artist, - Album: result.Album, - ReleaseDate: result.ReleaseDate, - TrackNumber: result.TrackNumber, - DiscNumber: result.DiscNumber, - ISRC: result.ISRC, - LyricsLRC: result.LyricsLRC, - } + resp := buildDownloadSuccessResponse( + req, + result, + service, + "Downloaded from "+service, + result.FilePath, + false, + ) jsonBytes, _ := json.Marshal(resp) return string(jsonBytes), nil } @@ -1266,6 +1337,10 @@ func DownloadFromYouTube(requestJSON string) (string, error) { DiscNumber: youtubeResult.DiscNumber, ISRC: youtubeResult.ISRC, LyricsLRC: youtubeResult.LyricsLRC, + CoverURL: req.CoverURL, + Genre: req.Genre, + Label: req.Label, + Copyright: req.Copyright, } jsonBytes, _ := json.Marshal(resp) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 7dbe289b..75306c04 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -1,8 +1,8 @@ /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.6.0'; - static const String buildNumber = '77'; + static const String version = '3.6.1'; + static const String buildNumber = '78'; static const String fullVersion = '$version+$buildNumber'; diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 0a634496..b65f77ac 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -12,6 +12,7 @@ import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/services/download_request_payload.dart'; import 'package:spotiflac_android/services/ffmpeg_service.dart'; import 'package:spotiflac_android/services/notification_service.dart'; import 'package:spotiflac_android/services/history_database.dart'; @@ -2756,129 +2757,64 @@ class DownloadQueueNotifier extends Notifier { final relativeDir = useSaf ? outputDir : ''; final fileName = useSaf ? (safFileName ?? '') : ''; final outputExt = useSaf ? safOutputExt : ''; + final isYouTube = item.service == 'youtube'; + final shouldUseExtensions = !isYouTube && useExtensions; + final shouldUseFallback = + !isYouTube && !shouldUseExtensions && state.autoFallback; - // YouTube provider - lossy only, bypasses fallback chain - if (item.service == 'youtube') { + if (isYouTube) { _log.d('Using YouTube/Cobalt provider for download'); _log.d('Quality: $quality (lossy only)'); - _log.d('Output dir: $outputDir'); - return PlatformBridge.downloadFromYouTube( - trackName: trackToDownload.name, - artistName: trackToDownload.artistName, - albumName: trackToDownload.albumName, - albumArtist: normalizedAlbumArtist, - coverUrl: trackToDownload.coverUrl, - outputDir: outputDir, - filenameFormat: state.filenameFormat, - quality: quality, - trackNumber: trackToDownload.trackNumber ?? 1, - discNumber: trackToDownload.discNumber ?? 1, - releaseDate: trackToDownload.releaseDate, - itemId: item.id, - durationMs: trackToDownload.duration, - isrc: trackToDownload.isrc, - spotifyId: trackToDownload.id, - deezerId: deezerTrackId, - storageMode: storageMode, - safTreeUri: treeUri, - safRelativeDir: relativeDir, - safFileName: fileName, - safOutputExt: outputExt, - ); - } - - if (useExtensions) { + } else if (shouldUseExtensions) { _log.d('Using extension providers for download'); _log.d( 'Quality: $quality${item.qualityOverride != null ? ' (override)' : ''}', ); - _log.d('Output dir: $outputDir'); - return PlatformBridge.downloadWithExtensions( - isrc: trackToDownload.isrc ?? '', - spotifyId: trackToDownload.id, - trackName: trackToDownload.name, - artistName: trackToDownload.artistName, - albumName: trackToDownload.albumName, - albumArtist: normalizedAlbumArtist, - coverUrl: trackToDownload.coverUrl, - outputDir: outputDir, - filenameFormat: state.filenameFormat, - quality: quality, - trackNumber: trackToDownload.trackNumber ?? 1, - discNumber: trackToDownload.discNumber ?? 1, - releaseDate: trackToDownload.releaseDate, - itemId: item.id, - durationMs: trackToDownload.duration, - source: trackToDownload.source, - genre: genre, - label: label, - lyricsMode: settings.lyricsMode, - preferredService: item.service, - storageMode: storageMode, - safTreeUri: treeUri, - safRelativeDir: relativeDir, - safFileName: fileName, - safOutputExt: outputExt, - ); - } - - if (state.autoFallback) { + } else if (shouldUseFallback) { _log.d('Using auto-fallback mode'); _log.d( 'Quality: $quality${item.qualityOverride != null ? ' (override)' : ''}', ); - _log.d('Output dir: $outputDir'); - return PlatformBridge.downloadWithFallback( - isrc: trackToDownload.isrc ?? '', - spotifyId: trackToDownload.id, - trackName: trackToDownload.name, - artistName: trackToDownload.artistName, - albumName: trackToDownload.albumName, - albumArtist: normalizedAlbumArtist, - coverUrl: trackToDownload.coverUrl, - outputDir: outputDir, - filenameFormat: state.filenameFormat, - quality: quality, - trackNumber: trackToDownload.trackNumber ?? 1, - discNumber: trackToDownload.discNumber ?? 1, - releaseDate: trackToDownload.releaseDate, - preferredService: item.service, - itemId: item.id, - durationMs: trackToDownload.duration, - genre: genre, - label: label, - lyricsMode: settings.lyricsMode, - storageMode: storageMode, - safTreeUri: treeUri, - safRelativeDir: relativeDir, - safFileName: fileName, - safOutputExt: outputExt, - ); } + _log.d('Output dir: $outputDir'); - return PlatformBridge.downloadTrack( + final payload = DownloadRequestPayload( isrc: trackToDownload.isrc ?? '', service: item.service, spotifyId: trackToDownload.id, trackName: trackToDownload.name, artistName: trackToDownload.artistName, albumName: trackToDownload.albumName, - albumArtist: normalizedAlbumArtist, - coverUrl: trackToDownload.coverUrl, + albumArtist: normalizedAlbumArtist ?? trackToDownload.artistName, + coverUrl: trackToDownload.coverUrl ?? '', outputDir: outputDir, filenameFormat: state.filenameFormat, quality: quality, + // Keep prior behavior: non-YouTube paths were implicitly true. + embedLyrics: isYouTube ? settings.embedLyrics : true, + embedMaxQualityCover: settings.maxQualityCover, trackNumber: trackToDownload.trackNumber ?? 1, discNumber: trackToDownload.discNumber ?? 1, - releaseDate: trackToDownload.releaseDate, + releaseDate: trackToDownload.releaseDate ?? '', itemId: item.id, durationMs: trackToDownload.duration, + source: trackToDownload.source ?? '', + genre: genre ?? '', + label: label ?? '', + deezerId: deezerTrackId ?? '', + lyricsMode: settings.lyricsMode, storageMode: storageMode, safTreeUri: treeUri, safRelativeDir: relativeDir, safFileName: fileName, safOutputExt: outputExt, ); + + return PlatformBridge.downloadByStrategy( + payload: payload, + useExtensions: shouldUseExtensions, + useFallback: shouldUseFallback, + ); } result = await runDownload( diff --git a/lib/services/download_request_payload.dart b/lib/services/download_request_payload.dart new file mode 100644 index 00000000..bff0a941 --- /dev/null +++ b/lib/services/download_request_payload.dart @@ -0,0 +1,154 @@ +class DownloadRequestPayload { + final String isrc; + final String service; + final String spotifyId; + final String trackName; + final String artistName; + final String albumName; + final String albumArtist; + final String coverUrl; + final String outputDir; + final String filenameFormat; + final String quality; + final bool embedLyrics; + final bool embedMaxQualityCover; + final int trackNumber; + final int discNumber; + final int totalTracks; + final String releaseDate; + final String itemId; + final int durationMs; + final String source; + final String genre; + final String label; + final String copyright; + final String tidalId; + final String qobuzId; + final String deezerId; + final String lyricsMode; + final bool useExtensions; + final bool useFallback; + final String storageMode; + final String safTreeUri; + final String safRelativeDir; + final String safFileName; + final String safOutputExt; + + const DownloadRequestPayload({ + this.isrc = '', + this.service = '', + this.spotifyId = '', + required this.trackName, + required this.artistName, + required this.albumName, + this.albumArtist = '', + this.coverUrl = '', + required this.outputDir, + required this.filenameFormat, + this.quality = 'LOSSLESS', + this.embedLyrics = true, + this.embedMaxQualityCover = true, + this.trackNumber = 1, + this.discNumber = 1, + this.totalTracks = 1, + this.releaseDate = '', + this.itemId = '', + this.durationMs = 0, + this.source = '', + this.genre = '', + this.label = '', + this.copyright = '', + this.tidalId = '', + this.qobuzId = '', + this.deezerId = '', + this.lyricsMode = 'embed', + this.useExtensions = false, + this.useFallback = false, + this.storageMode = 'app', + this.safTreeUri = '', + this.safRelativeDir = '', + this.safFileName = '', + this.safOutputExt = '', + }); + + Map toJson() { + return { + 'isrc': isrc, + 'service': service, + 'spotify_id': spotifyId, + 'track_name': trackName, + 'artist_name': artistName, + 'album_name': albumName, + 'album_artist': albumArtist, + 'cover_url': coverUrl, + 'output_dir': outputDir, + 'filename_format': filenameFormat, + 'quality': quality, + 'embed_lyrics': embedLyrics, + 'embed_max_quality_cover': embedMaxQualityCover, + 'track_number': trackNumber, + 'disc_number': discNumber, + 'total_tracks': totalTracks, + 'release_date': releaseDate, + 'item_id': itemId, + 'duration_ms': durationMs, + 'source': source, + 'genre': genre, + 'label': label, + 'copyright': copyright, + 'tidal_id': tidalId, + 'qobuz_id': qobuzId, + 'deezer_id': deezerId, + 'lyrics_mode': lyricsMode, + 'use_extensions': useExtensions, + 'use_fallback': useFallback, + 'storage_mode': storageMode, + 'saf_tree_uri': safTreeUri, + 'saf_relative_dir': safRelativeDir, + 'saf_file_name': safFileName, + 'saf_output_ext': safOutputExt, + }; + } + + DownloadRequestPayload withStrategy({ + bool? useExtensions, + bool? useFallback, + }) { + return DownloadRequestPayload( + isrc: isrc, + service: service, + spotifyId: spotifyId, + trackName: trackName, + artistName: artistName, + albumName: albumName, + albumArtist: albumArtist, + coverUrl: coverUrl, + outputDir: outputDir, + filenameFormat: filenameFormat, + quality: quality, + embedLyrics: embedLyrics, + embedMaxQualityCover: embedMaxQualityCover, + trackNumber: trackNumber, + discNumber: discNumber, + totalTracks: totalTracks, + releaseDate: releaseDate, + itemId: itemId, + durationMs: durationMs, + source: source, + genre: genre, + label: label, + copyright: copyright, + tidalId: tidalId, + qobuzId: qobuzId, + deezerId: deezerId, + lyricsMode: lyricsMode, + useExtensions: useExtensions ?? this.useExtensions, + useFallback: useFallback ?? this.useFallback, + storageMode: storageMode, + safTreeUri: safTreeUri, + safRelativeDir: safRelativeDir, + safFileName: safFileName, + safOutputExt: safOutputExt, + ); + } +} diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 13d84e72..430c779c 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -30,7 +30,7 @@ class NotificationService { iOS: iosSettings, ); - await _notifications.initialize(initSettings); + await _notifications.initialize(settings: initSettings); if (Platform.isAndroid) { await _notifications @@ -90,10 +90,10 @@ class NotificationService { ); await _notifications.show( - downloadProgressId, - 'Downloading $trackName', - '$artistName • $percentage%', - details, + id: downloadProgressId, + title: 'Downloading $trackName', + body: '$artistName • $percentage%', + notificationDetails: details, ); } @@ -133,10 +133,10 @@ class NotificationService { ); await _notifications.show( - downloadProgressId, - 'Finalizing $trackName', - '$artistName • Embedding metadata...', - details, + id: downloadProgressId, + title: 'Finalizing $trackName', + body: '$artistName • Embedding metadata...', + notificationDetails: details, ); } @@ -183,10 +183,10 @@ class NotificationService { ); await _notifications.show( - downloadProgressId, - title, - '$trackName - $artistName', - details, + id: downloadProgressId, + title: title, + body: '$trackName - $artistName', + notificationDetails: details, ); } @@ -223,15 +223,15 @@ class NotificationService { ); await _notifications.show( - downloadProgressId, - title, - '$completedCount tracks downloaded successfully', - details, + id: downloadProgressId, + title: title, + body: '$completedCount tracks downloaded successfully', + notificationDetails: details, ); } Future cancelDownloadNotification() async { - await _notifications.cancel(downloadProgressId); + await _notifications.cancel(id: downloadProgressId); } Future showUpdateDownloadProgress({ @@ -274,10 +274,10 @@ class NotificationService { ); await _notifications.show( - updateDownloadId, - 'Downloading SpotiFLAC v$version', - '$receivedMB / $totalMB MB • $percentage%', - details, + id: updateDownloadId, + title: 'Downloading SpotiFLAC v$version', + body: '$receivedMB / $totalMB MB • $percentage%', + notificationDetails: details, ); } @@ -307,10 +307,10 @@ class NotificationService { ); await _notifications.show( - updateDownloadId, - 'Update Ready', - 'SpotiFLAC v$version downloaded. Tap to install.', - details, + id: updateDownloadId, + title: 'Update Ready', + body: 'SpotiFLAC v$version downloaded. Tap to install.', + notificationDetails: details, ); } @@ -339,14 +339,14 @@ class NotificationService { ); await _notifications.show( - updateDownloadId, - 'Update Failed', - 'Could not download update. Try again later.', - details, + id: updateDownloadId, + title: 'Update Failed', + body: 'Could not download update. Try again later.', + notificationDetails: details, ); } Future cancelUpdateNotification() async { - await _notifications.cancel(updateDownloadId); + await _notifications.cancel(id: updateDownloadId); } } diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 7bcae04a..74bcaed0 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1,1253 +1,1403 @@ -import 'dart:convert'; -import 'package:flutter/services.dart'; -import 'package:spotiflac_android/utils/logger.dart'; - -final _log = AppLogger('PlatformBridge'); - -class PlatformBridge { - static const _channel = MethodChannel('com.zarz.spotiflac/backend'); - - static Future> parseSpotifyUrl(String url) async { - _log.d('parseSpotifyUrl: $url'); - final result = await _channel.invokeMethod('parseSpotifyUrl', {'url': url}); - return jsonDecode(result as String) as Map; - } - - static Future> getSpotifyMetadata(String url) async { - _log.d('getSpotifyMetadata: $url'); - final result = await _channel.invokeMethod('getSpotifyMetadata', {'url': url}); - return jsonDecode(result as String) as Map; - } - - static Future> searchSpotify(String query, {int limit = 10}) async { - _log.d('searchSpotify: "$query" (limit: $limit)'); - final result = await _channel.invokeMethod('searchSpotify', { - 'query': query, - 'limit': limit, - }); - return jsonDecode(result as String) as Map; - } - - static Future> searchSpotifyAll(String query, {int trackLimit = 15, int artistLimit = 3}) async { - _log.d('searchSpotifyAll: "$query"'); - final result = await _channel.invokeMethod('searchSpotifyAll', { - 'query': query, - 'track_limit': trackLimit, - 'artist_limit': artistLimit, - }); - return jsonDecode(result as String) as Map; - } - - static Future> checkAvailability(String spotifyId, String isrc) async { - _log.d('checkAvailability: $spotifyId (ISRC: $isrc)'); - final result = await _channel.invokeMethod('checkAvailability', { - 'spotify_id': spotifyId, - 'isrc': isrc, - }); - return jsonDecode(result as String) as Map; - } - - static Future> downloadTrack({ - required String isrc, - required String service, - required String spotifyId, - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'LOSSLESS', - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int trackNumber = 1, - int discNumber = 1, - int totalTracks = 1, - String? releaseDate, - String? itemId, - int durationMs = 0, - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - _log.i('downloadTrack: "$trackName" by $artistName via $service'); - final request = jsonEncode({ - 'isrc': isrc, - 'service': service, - 'spotify_id': spotifyId, - 'track_name': trackName, - 'artist_name': artistName, - 'album_name': albumName, - 'album_artist': albumArtist ?? artistName, - 'cover_url': coverUrl, - 'output_dir': outputDir, - 'filename_format': filenameFormat, - 'quality': quality, - 'embed_lyrics': embedLyrics, - 'embed_max_quality_cover': embedMaxQualityCover, - 'track_number': trackNumber, - 'disc_number': discNumber, - 'total_tracks': totalTracks, - 'release_date': releaseDate ?? '', - 'item_id': itemId ?? '', - 'duration_ms': durationMs, - 'storage_mode': storageMode, - 'saf_tree_uri': safTreeUri, - 'saf_relative_dir': safRelativeDir, - 'saf_file_name': safFileName, - 'saf_output_ext': safOutputExt, - }); - - final result = await _channel.invokeMethod('downloadTrack', request); - final response = jsonDecode(result as String) as Map; - if (response['success'] == true) { - _log.i('Download success: ${response['file_path']}'); - } else { - _log.w('Download failed: ${response['error']}'); - } - return response; - } - - static Future> downloadWithFallback({ - required String isrc, - required String spotifyId, - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'LOSSLESS', - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int trackNumber = 1, - int discNumber = 1, - int totalTracks = 1, - String? releaseDate, - String preferredService = 'tidal', - String? itemId, - int durationMs = 0, - String? genre, - String? label, - String? copyright, - String lyricsMode = 'embed', - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - _log.i('downloadWithFallback: "$trackName" by $artistName (preferred: $preferredService)'); - final request = jsonEncode({ - 'isrc': isrc, - 'service': preferredService, - 'spotify_id': spotifyId, - 'track_name': trackName, - 'artist_name': artistName, - 'album_name': albumName, - 'album_artist': albumArtist ?? artistName, - 'cover_url': coverUrl, - 'output_dir': outputDir, - 'filename_format': filenameFormat, - 'quality': quality, - 'embed_lyrics': embedLyrics, - 'embed_max_quality_cover': embedMaxQualityCover, - 'track_number': trackNumber, - 'disc_number': discNumber, - 'total_tracks': totalTracks, - 'release_date': releaseDate ?? '', - 'item_id': itemId ?? '', - 'duration_ms': durationMs, - 'genre': genre ?? '', - 'label': label ?? '', - 'copyright': copyright ?? '', - 'lyrics_mode': lyricsMode, - 'storage_mode': storageMode, - 'saf_tree_uri': safTreeUri, - 'saf_relative_dir': safRelativeDir, - 'saf_file_name': safFileName, - 'saf_output_ext': safOutputExt, - }); - - final result = await _channel.invokeMethod('downloadWithFallback', request); - final response = jsonDecode(result as String) as Map; - if (response['success'] == true) { - final service = response['service'] ?? 'unknown'; - final filePath = response['file_path'] ?? ''; - final bitDepth = response['actual_bit_depth']; - final sampleRate = response['actual_sample_rate']; - final qualityStr = bitDepth != null && sampleRate != null - ? ' ($bitDepth-bit/${(sampleRate / 1000).toStringAsFixed(1)}kHz)' - : ''; - _log.i('Download success via $service$qualityStr: $filePath'); - } else { - final error = response['error'] ?? 'Unknown error'; - final errorType = response['error_type'] ?? ''; - _log.e('Download failed: $error (type: $errorType)'); - } - return response; - } - - static Future> getDownloadProgress() async { - final result = await _channel.invokeMethod('getDownloadProgress'); - return jsonDecode(result as String) as Map; - } - - static Future> getAllDownloadProgress() async { - final result = await _channel.invokeMethod('getAllDownloadProgress'); - return jsonDecode(result as String) as Map; - } - - static Future initItemProgress(String itemId) async { - await _channel.invokeMethod('initItemProgress', {'item_id': itemId}); - } - - static Future finishItemProgress(String itemId) async { - await _channel.invokeMethod('finishItemProgress', {'item_id': itemId}); - } - - static Future clearItemProgress(String itemId) async { - await _channel.invokeMethod('clearItemProgress', {'item_id': itemId}); - } - - static Future cancelDownload(String itemId) async { - await _channel.invokeMethod('cancelDownload', {'item_id': itemId}); - } - - static Future setDownloadDirectory(String path) async { - await _channel.invokeMethod('setDownloadDirectory', {'path': path}); - } - - static Future> checkDuplicate(String outputDir, String isrc) async { - final result = await _channel.invokeMethod('checkDuplicate', { - 'output_dir': outputDir, - 'isrc': isrc, - }); - return jsonDecode(result as String) as Map; - } - - static Future buildFilename(String template, Map metadata) async { - final result = await _channel.invokeMethod('buildFilename', { - 'template': template, - 'metadata': jsonEncode(metadata), - }); - return result as String; - } - - static Future sanitizeFilename(String filename) async { - final result = await _channel.invokeMethod('sanitizeFilename', { - 'filename': filename, - }); - return result as String; - } - - static Future?> pickSafTree() async { - final result = await _channel.invokeMethod('pickSafTree'); - if (result == null) return null; - return jsonDecode(result as String) as Map; - } - - static Future safExists(String uri) async { - final result = await _channel.invokeMethod('safExists', {'uri': uri}); - return result as bool; - } - - static Future safDelete(String uri) async { - final result = await _channel.invokeMethod('safDelete', {'uri': uri}); - return result as bool; - } - - static Future> safStat(String uri) async { - final result = await _channel.invokeMethod('safStat', {'uri': uri}); - return jsonDecode(result as String) as Map; - } - - static Future> resolveSafFile({ - required String treeUri, - required String fileName, - String relativeDir = '', - }) async { - final result = await _channel.invokeMethod('resolveSafFile', { - 'tree_uri': treeUri, - 'relative_dir': relativeDir, - 'file_name': fileName, - }); - return jsonDecode(result as String) as Map; - } - - static Future copyContentUriToTemp(String uri) async { - final result = await _channel.invokeMethod('safCopyToTemp', {'uri': uri}); - return result as String?; - } - - static Future replaceContentUriFromPath( - String uri, - String srcPath, - ) async { - final result = await _channel.invokeMethod('safReplaceFromPath', { - 'uri': uri, - 'src_path': srcPath, - }); - return result as bool; - } - - static Future createSafFileFromPath({ - required String treeUri, - required String relativeDir, - required String fileName, - required String mimeType, - required String srcPath, - }) async { - final result = await _channel.invokeMethod('safCreateFromPath', { - 'tree_uri': treeUri, - 'relative_dir': relativeDir, - 'file_name': fileName, - 'mime_type': mimeType, - 'src_path': srcPath, - }); - return result as String?; - } - - static Future openContentUri(String uri, {String mimeType = ''}) async { - await _channel.invokeMethod('openContentUri', { - 'uri': uri, - 'mime_type': mimeType, - }); - } - - static Future shareContentUri(String uri, {String title = ''}) async { - final result = await _channel.invokeMethod('shareContentUri', { - 'uri': uri, - 'title': title, - }); - return result as bool? ?? false; - } - - static Future> fetchLyrics( - String spotifyId, - String trackName, - String artistName, { - int durationMs = 0, - }) async { - final result = await _channel.invokeMethod('fetchLyrics', { - 'spotify_id': spotifyId, - 'track_name': trackName, - 'artist_name': artistName, - 'duration_ms': durationMs, - }); - return jsonDecode(result as String) as Map; - } - - static Future getLyricsLRC( - String spotifyId, - String trackName, - String artistName, { - String? filePath, - int durationMs = 0, - }) async { - final result = await _channel.invokeMethod('getLyricsLRC', { - 'spotify_id': spotifyId, - 'track_name': trackName, - 'artist_name': artistName, - 'file_path': filePath ?? '', - 'duration_ms': durationMs, - }); - return result as String; - } - - static Future> embedLyricsToFile( - String filePath, - String lyrics, - ) async { - final result = await _channel.invokeMethod('embedLyricsToFile', { - 'file_path': filePath, - 'lyrics': lyrics, - }); - return jsonDecode(result as String) as Map; - } - - static Future cleanupConnections() async { - await _channel.invokeMethod('cleanupConnections'); - } - - static Future> downloadCoverToFile( - String coverUrl, - String outputPath, { - bool maxQuality = true, - }) async { - final result = await _channel.invokeMethod('downloadCoverToFile', { - 'cover_url': coverUrl, - 'output_path': outputPath, - 'max_quality': maxQuality, - }); - return jsonDecode(result as String) as Map; - } - - static Future> extractCoverToFile( - String audioPath, - String outputPath, - ) async { - final result = await _channel.invokeMethod('extractCoverToFile', { - 'audio_path': audioPath, - 'output_path': outputPath, - }); - return jsonDecode(result as String) as Map; - } - - static Future> fetchAndSaveLyrics({ - required String trackName, - required String artistName, - required String spotifyId, - required int durationMs, - required String outputPath, - }) async { - final result = await _channel.invokeMethod('fetchAndSaveLyrics', { - 'track_name': trackName, - 'artist_name': artistName, - 'spotify_id': spotifyId, - 'duration_ms': durationMs, - 'output_path': outputPath, - }); - return jsonDecode(result as String) as Map; - } - - static Future> reEnrichFile(Map request) async { - final requestJSON = jsonEncode(request); - final result = await _channel.invokeMethod('reEnrichFile', { - 'request_json': requestJSON, - }); - return jsonDecode(result as String) as Map; - } - - static Future> readFileMetadata(String filePath) async { - final result = await _channel.invokeMethod('readFileMetadata', { - 'file_path': filePath, - }); - return jsonDecode(result as String) as Map; - } - - static Future> editFileMetadata( - String filePath, - Map metadata, - ) async { - final metadataJSON = jsonEncode(metadata); - final result = await _channel.invokeMethod('editFileMetadata', { - 'file_path': filePath, - 'metadata_json': metadataJSON, - }); - return jsonDecode(result as String) as Map; - } - - static Future writeTempToSaf(String tempPath, String safUri) async { - final result = await _channel.invokeMethod('writeTempToSaf', { - 'temp_path': tempPath, - 'saf_uri': safUri, - }); - final map = jsonDecode(result as String) as Map; - return map['success'] == true; - } - - static Future startDownloadService({ - String trackName = '', - String artistName = '', - int queueCount = 0, - }) async { - await _channel.invokeMethod('startDownloadService', { - 'track_name': trackName, - 'artist_name': artistName, - 'queue_count': queueCount, - }); - } - - static Future stopDownloadService() async { - await _channel.invokeMethod('stopDownloadService'); - } - - static Future updateDownloadServiceProgress({ - required String trackName, - required String artistName, - required int progress, - required int total, - required int queueCount, - }) async { - await _channel.invokeMethod('updateDownloadServiceProgress', { - 'track_name': trackName, - 'artist_name': artistName, - 'progress': progress, - 'total': total, - 'queue_count': queueCount, - }); - } - - static Future isDownloadServiceRunning() async { - final result = await _channel.invokeMethod('isDownloadServiceRunning'); - return result as bool; - } - - static Future setSpotifyCredentials(String clientId, String clientSecret) async { - await _channel.invokeMethod('setSpotifyCredentials', { - 'client_id': clientId, - 'client_secret': clientSecret, - }); - } - - static Future hasSpotifyCredentials() async { - final result = await _channel.invokeMethod('hasSpotifyCredentials'); - return result as bool; - } - - static Future preWarmTrackCache(List> tracks) async { - final tracksJson = jsonEncode(tracks); - await _channel.invokeMethod('preWarmTrackCache', {'tracks': tracksJson}); - } - - static Future getTrackCacheSize() async { - final result = await _channel.invokeMethod('getTrackCacheSize'); - return result as int; - } - - static Future clearTrackCache() async { - await _channel.invokeMethod('clearTrackCache'); - } - - static Future> searchDeezerAll(String query, {int trackLimit = 15, int artistLimit = 2, String? filter}) async { - final result = await _channel.invokeMethod('searchDeezerAll', { - 'query': query, - 'track_limit': trackLimit, - 'artist_limit': artistLimit, - 'filter': filter ?? '', - }); - return jsonDecode(result as String) as Map; - } - - static Future> getDeezerMetadata(String resourceType, String resourceId) async { - final result = await _channel.invokeMethod('getDeezerMetadata', { - 'resource_type': resourceType, - 'resource_id': resourceId, - }); - if (result == null) { - throw Exception('getDeezerMetadata returned null for $resourceType:$resourceId'); - } - return jsonDecode(result as String) as Map; - } - - static Future> parseDeezerUrl(String url) async { - final result = await _channel.invokeMethod('parseDeezerUrl', {'url': url}); - return jsonDecode(result as String) as Map; - } - - static Future> parseTidalUrl(String url) async { - final result = await _channel.invokeMethod('parseTidalUrl', {'url': url}); - return jsonDecode(result as String) as Map; - } - - static Future> convertTidalToSpotifyDeezer(String tidalUrl) async { - final result = await _channel.invokeMethod('convertTidalToSpotifyDeezer', {'url': tidalUrl}); - return jsonDecode(result as String) as Map; - } - - static Future> searchDeezerByISRC(String isrc) async { - final result = await _channel.invokeMethod('searchDeezerByISRC', {'isrc': isrc}); - return jsonDecode(result as String) as Map; - } - - static Future?> getDeezerExtendedMetadata(String trackId) async { - try { - final result = await _channel.invokeMethod('getDeezerExtendedMetadata', { - 'track_id': trackId, - }); - if (result == null) return null; - final data = jsonDecode(result as String) as Map; - return { - 'genre': data['genre'] as String? ?? '', - 'label': data['label'] as String? ?? '', - }; - } catch (e) { - _log.w('Failed to get Deezer extended metadata for $trackId: $e'); - return null; - } - } - - static Future> convertSpotifyToDeezer(String resourceType, String spotifyId) async { - final result = await _channel.invokeMethod('convertSpotifyToDeezer', { - 'resource_type': resourceType, - 'spotify_id': spotifyId, - }); - return jsonDecode(result as String) as Map; - } - - static Future> getSpotifyMetadataWithFallback(String url) async { - final result = await _channel.invokeMethod('getSpotifyMetadataWithFallback', {'url': url}); - return jsonDecode(result as String) as Map; - } - - static Future>> getGoLogs() async { - final result = await _channel.invokeMethod('getLogs'); - final logs = jsonDecode(result as String) as List; - return logs.map((e) => e as Map).toList(); - } - - static Future> getGoLogsSince(int index) async { - final result = await _channel.invokeMethod('getLogsSince', {'index': index}); - return jsonDecode(result as String) as Map; - } - - static Future clearGoLogs() async { - await _channel.invokeMethod('clearLogs'); - } - - static Future getGoLogCount() async { - final result = await _channel.invokeMethod('getLogCount'); - return result as int; - } - - static Future setGoLoggingEnabled(bool enabled) async { - await _channel.invokeMethod('setLoggingEnabled', {'enabled': enabled}); - } - - - static Future initExtensionSystem(String extensionsDir, String dataDir) async { - _log.d('initExtensionSystem: $extensionsDir, $dataDir'); - await _channel.invokeMethod('initExtensionSystem', { - 'extensions_dir': extensionsDir, - 'data_dir': dataDir, - }); - } - - static Future> loadExtensionsFromDir(String dirPath) async { - _log.d('loadExtensionsFromDir: $dirPath'); - final result = await _channel.invokeMethod('loadExtensionsFromDir', { - 'dir_path': dirPath, - }); - return jsonDecode(result as String) as Map; - } - - static Future> loadExtensionFromPath(String filePath) async { - _log.d('loadExtensionFromPath: $filePath'); - final result = await _channel.invokeMethod('loadExtensionFromPath', { - 'file_path': filePath, - }); - return jsonDecode(result as String) as Map; - } - - static Future unloadExtension(String extensionId) async { - _log.d('unloadExtension: $extensionId'); - await _channel.invokeMethod('unloadExtension', { - 'extension_id': extensionId, - }); - } - - static Future removeExtension(String extensionId) async { - _log.d('removeExtension: $extensionId'); - await _channel.invokeMethod('removeExtension', { - 'extension_id': extensionId, - }); - } - - static Future> upgradeExtension(String filePath) async { - _log.d('upgradeExtension: $filePath'); - final result = await _channel.invokeMethod('upgradeExtension', { - 'file_path': filePath, - }); - return jsonDecode(result as String) as Map; - } - - static Future> checkExtensionUpgrade(String filePath) async { - _log.d('checkExtensionUpgrade: $filePath'); - final result = await _channel.invokeMethod('checkExtensionUpgrade', { - 'file_path': filePath, - }); - return jsonDecode(result as String) as Map; - } - - static Future>> getInstalledExtensions() async { - final result = await _channel.invokeMethod('getInstalledExtensions'); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - static Future setExtensionEnabled(String extensionId, bool enabled) async { - _log.d('setExtensionEnabled: $extensionId = $enabled'); - await _channel.invokeMethod('setExtensionEnabled', { - 'extension_id': extensionId, - 'enabled': enabled, - }); - } - - static Future setProviderPriority(List providerIds) async { - _log.d('setProviderPriority: $providerIds'); - await _channel.invokeMethod('setProviderPriority', { - 'priority': jsonEncode(providerIds), - }); - } - - static Future> getProviderPriority() async { - final result = await _channel.invokeMethod('getProviderPriority'); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as String).toList(); - } - - static Future setMetadataProviderPriority(List providerIds) async { - _log.d('setMetadataProviderPriority: $providerIds'); - await _channel.invokeMethod('setMetadataProviderPriority', { - 'priority': jsonEncode(providerIds), - }); - } - - static Future> getMetadataProviderPriority() async { - final result = await _channel.invokeMethod('getMetadataProviderPriority'); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as String).toList(); - } - - static Future> getExtensionSettings(String extensionId) async { - final result = await _channel.invokeMethod('getExtensionSettings', { - 'extension_id': extensionId, - }); - return jsonDecode(result as String) as Map; - } - - static Future setExtensionSettings(String extensionId, Map settings) async { - _log.d('setExtensionSettings: $extensionId'); - await _channel.invokeMethod('setExtensionSettings', { - 'extension_id': extensionId, - 'settings': jsonEncode(settings), - }); - } - - static Future> invokeExtensionAction(String extensionId, String actionName) async { - _log.d('invokeExtensionAction: $extensionId.$actionName'); - final result = await _channel.invokeMethod('invokeExtensionAction', { - 'extension_id': extensionId, - 'action': actionName, - }); - if (result == null || (result as String).isEmpty) { - return {'success': true}; - } - return jsonDecode(result) as Map; - } - - static Future>> searchTracksWithExtensions(String query, {int limit = 20}) async { - _log.d('searchTracksWithExtensions: "$query"'); - final result = await _channel.invokeMethod('searchTracksWithExtensions', { - 'query': query, - 'limit': limit, - }); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - -static Future> downloadWithExtensions({ - required String isrc, - required String spotifyId, - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'LOSSLESS', - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int trackNumber = 1, - int discNumber = 1, - int totalTracks = 1, - String? releaseDate, - String? itemId, - int durationMs = 0, - String? source, - String? genre, - String? label, - String lyricsMode = 'embed', - String? preferredService, - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - _log.i('downloadWithExtensions: "$trackName" by $artistName${source != null ? ' (source: $source)' : ''}${preferredService != null ? ' (service: $preferredService)' : ''}'); - final request = jsonEncode({ - 'isrc': isrc, - 'spotify_id': spotifyId, - 'track_name': trackName, - 'artist_name': artistName, - 'album_name': albumName, - 'album_artist': albumArtist ?? artistName, - 'cover_url': coverUrl, - 'output_dir': outputDir, - 'filename_format': filenameFormat, - 'quality': quality, - 'embed_lyrics': embedLyrics, - 'embed_max_quality_cover': embedMaxQualityCover, - 'track_number': trackNumber, - 'disc_number': discNumber, - 'total_tracks': totalTracks, - 'release_date': releaseDate ?? '', - 'item_id': itemId ?? '', - 'duration_ms': durationMs, - 'source': source ?? '', - 'genre': genre ?? '', - 'label': label ?? '', - 'lyrics_mode': lyricsMode, - 'service': preferredService ?? '', - 'storage_mode': storageMode, - 'saf_tree_uri': safTreeUri, - 'saf_relative_dir': safRelativeDir, - 'saf_file_name': safFileName, - 'saf_output_ext': safOutputExt, - }); - - final result = await _channel.invokeMethod('downloadWithExtensions', request); - return jsonDecode(result as String) as Map; - } - - static Future cleanupExtensions() async { - _log.d('cleanupExtensions'); - await _channel.invokeMethod('cleanupExtensions'); - } - - static Future?> getExtensionPendingAuth(String extensionId) async { - final result = await _channel.invokeMethod('getExtensionPendingAuth', { - 'extension_id': extensionId, - }); - if (result == null) return null; - return jsonDecode(result as String) as Map; - } - - static Future setExtensionAuthCode(String extensionId, String authCode) async { - _log.d('setExtensionAuthCode: $extensionId'); - await _channel.invokeMethod('setExtensionAuthCode', { - 'extension_id': extensionId, - 'auth_code': authCode, - }); - } - - static Future setExtensionTokens( - String extensionId, { - required String accessToken, - String? refreshToken, - int? expiresIn, - }) async { - _log.d('setExtensionTokens: $extensionId'); - await _channel.invokeMethod('setExtensionTokens', { - 'extension_id': extensionId, - 'access_token': accessToken, - 'refresh_token': refreshToken ?? '', - 'expires_in': expiresIn ?? 0, - }); - } - - static Future clearExtensionPendingAuth(String extensionId) async { - await _channel.invokeMethod('clearExtensionPendingAuth', { - 'extension_id': extensionId, - }); - } - - static Future isExtensionAuthenticated(String extensionId) async { - final result = await _channel.invokeMethod('isExtensionAuthenticated', { - 'extension_id': extensionId, - }); - return result as bool; - } - - static Future>> getAllPendingAuthRequests() async { - final result = await _channel.invokeMethod('getAllPendingAuthRequests'); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - static Future?> getPendingFFmpegCommand(String commandId) async { - final result = await _channel.invokeMethod('getPendingFFmpegCommand', { - 'command_id': commandId, - }); - if (result == null) return null; - return jsonDecode(result as String) as Map; - } - - static Future setFFmpegCommandResult( - String commandId, { - required bool success, - String output = '', - String error = '', - }) async { - await _channel.invokeMethod('setFFmpegCommandResult', { - 'command_id': commandId, - 'success': success, - 'output': output, - 'error': error, - }); - } - - static Future>> getAllPendingFFmpegCommands() async { - final result = await _channel.invokeMethod('getAllPendingFFmpegCommands'); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - static Future>> customSearchWithExtension( - String extensionId, - String query, { - Map? options, - }) async { - final result = await _channel.invokeMethod('customSearchWithExtension', { - 'extension_id': extensionId, - 'query': query, - 'options': options != null ? jsonEncode(options) : '', - }); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - static Future>> getSearchProviders() async { - final result = await _channel.invokeMethod('getSearchProviders'); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - static Future?> handleURLWithExtension(String url) async { - try { - final result = await _channel.invokeMethod('handleURLWithExtension', { - 'url': url, - }); - if (result == null || result == '') return null; - return jsonDecode(result as String) as Map; - } catch (e) { - return null; - } - } - - static Future findURLHandler(String url) async { - final result = await _channel.invokeMethod('findURLHandler', { - 'url': url, - }); - if (result == null || result == '') return null; - return result as String; - } - - static Future>> getURLHandlers() async { - final result = await _channel.invokeMethod('getURLHandlers'); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - static Future?> getAlbumWithExtension( - String extensionId, - String albumId, - ) async { - try { - final result = await _channel.invokeMethod('getAlbumWithExtension', { - 'extension_id': extensionId, - 'album_id': albumId, - }); - if (result == null || result == '') return null; - return jsonDecode(result as String) as Map; - } catch (e) { - _log.e('getAlbumWithExtension failed: $e'); - return null; - } - } - - static Future?> getPlaylistWithExtension( - String extensionId, - String playlistId, - ) async { - try { - final result = await _channel.invokeMethod('getPlaylistWithExtension', { - 'extension_id': extensionId, - 'playlist_id': playlistId, - }); - if (result == null || result == '') return null; - return jsonDecode(result as String) as Map; - } catch (e) { - _log.e('getPlaylistWithExtension failed: $e'); - return null; - } - } - - static Future?> getArtistWithExtension( - String extensionId, - String artistId, - ) async { - try { - final result = await _channel.invokeMethod('getArtistWithExtension', { - 'extension_id': extensionId, - 'artist_id': artistId, - }); - if (result == null || result == '') return null; - return jsonDecode(result as String) as Map; - } catch (e) { - _log.e('getArtistWithExtension failed: $e'); - return null; - } - } - - static Future?> getExtensionHomeFeed(String extensionId) async { - try { - final result = await _channel.invokeMethod('getExtensionHomeFeed', { - 'extension_id': extensionId, - }); - if (result == null || result == '') return null; - return jsonDecode(result as String) as Map; - } catch (e) { - _log.e('getExtensionHomeFeed failed: $e'); - return null; - } - } - - static Future?> getExtensionBrowseCategories(String extensionId) async { - try { - final result = await _channel.invokeMethod('getExtensionBrowseCategories', { - 'extension_id': extensionId, - }); - if (result == null || result == '') return null; - return jsonDecode(result as String) as Map; - } catch (e) { - _log.e('getExtensionBrowseCategories failed: $e'); - return null; - } - } - - // ==================== LOCAL LIBRARY SCANNING ==================== - - /// Set the directory for caching extracted cover art - static Future setLibraryCoverCacheDir(String cacheDir) async { - _log.i('setLibraryCoverCacheDir: $cacheDir'); - await _channel.invokeMethod('setLibraryCoverCacheDir', { - 'cache_dir': cacheDir, - }); - } - -/// Scan a folder for audio files and read their metadata - /// Returns a list of track metadata - static Future>> scanLibraryFolder(String folderPath) async { - _log.i('scanLibraryFolder: $folderPath'); - final result = await _channel.invokeMethod('scanLibraryFolder', { - 'folder_path': folderPath, - }); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - /// Perform an incremental scan of the library folder - /// Only scans files that are new or have changed since last scan - /// [existingFiles] is a map of filePath -> modTime (unix millis) - /// Returns IncrementalScanResult with scanned items, deleted paths, and skip count - static Future> scanLibraryFolderIncremental( - String folderPath, - Map existingFiles, - ) async { - _log.i('scanLibraryFolderIncremental: $folderPath (${existingFiles.length} existing files)'); - final result = await _channel.invokeMethod('scanLibraryFolderIncremental', { - 'folder_path': folderPath, - 'existing_files': jsonEncode(existingFiles), - }); - return jsonDecode(result as String) as Map; - } - - static Future>> scanSafTree(String treeUri) async { - _log.i('scanSafTree: $treeUri'); - final result = await _channel.invokeMethod('scanSafTree', { - 'tree_uri': treeUri, - }); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - /// Incremental SAF tree scan - only scans new or modified files - /// Returns a map with 'files' (new/changed) and 'removedUris' (deleted files) - static Future> scanSafTreeIncremental( - String treeUri, - Map existingFiles, - ) async { - _log.i('scanSafTreeIncremental: $treeUri (${existingFiles.length} existing files)'); - final result = await _channel.invokeMethod('scanSafTreeIncremental', { - 'tree_uri': treeUri, - 'existing_files': jsonEncode(existingFiles), - }); - return jsonDecode(result as String) as Map; - } - - /// Get last-modified timestamps for a list of SAF file URIs. - /// Returns map uri -> modTime (unix millis), only for files that still exist. - static Future> getSafFileModTimes(List uris) async { - final result = await _channel.invokeMethod('getSafFileModTimes', { - 'uris': jsonEncode(uris), - }); - final map = jsonDecode(result as String) as Map; - return map.map((key, value) => MapEntry(key, (value as num).toInt())); - } - - /// Get current library scan progress - static Future> getLibraryScanProgress() async { - final result = await _channel.invokeMethod('getLibraryScanProgress'); - return jsonDecode(result as String) as Map; - } - - /// Cancel ongoing library scan - static Future cancelLibraryScan() async { - await _channel.invokeMethod('cancelLibraryScan'); - } - - /// Read metadata from a single audio file - static Future?> readAudioMetadata(String filePath) async { - try { - final result = await _channel.invokeMethod('readAudioMetadata', { - 'file_path': filePath, - }); - if (result == null || result == '') return null; - return jsonDecode(result as String) as Map; - } catch (e) { - _log.w('Failed to read audio metadata: $e'); - return null; - } - } - - - static Future> runPostProcessing( - String filePath, { - Map? metadata, - }) async { - final result = await _channel.invokeMethod('runPostProcessing', { - 'file_path': filePath, - 'metadata': metadata != null ? jsonEncode(metadata) : '', - }); - return jsonDecode(result as String) as Map; - } - - static Future> runPostProcessingV2( - String filePath, { - Map? metadata, - }) async { - final input = {}; - if (filePath.startsWith('content://')) { - input['uri'] = filePath; - } else { - input['path'] = filePath; - } - final result = await _channel.invokeMethod('runPostProcessingV2', { - 'input': jsonEncode(input), - 'metadata': metadata != null ? jsonEncode(metadata) : '', - }); - return jsonDecode(result as String) as Map; - } - - static Future>> getPostProcessingProviders() async { - final result = await _channel.invokeMethod('getPostProcessingProviders'); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - - static Future initExtensionStore(String cacheDir) async { - _log.d('initExtensionStore: $cacheDir'); - await _channel.invokeMethod('initExtensionStore', {'cache_dir': cacheDir}); - } - - static Future>> getStoreExtensions({bool forceRefresh = false}) async { - _log.d('getStoreExtensions (forceRefresh: $forceRefresh)'); - final result = await _channel.invokeMethod('getStoreExtensions', { - 'force_refresh': forceRefresh, - }); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - static Future>> searchStoreExtensions(String query, {String? category}) async { - _log.d('searchStoreExtensions: "$query" (category: $category)'); - final result = await _channel.invokeMethod('searchStoreExtensions', { - 'query': query, - 'category': category ?? '', - }); - final list = jsonDecode(result as String) as List; - return list.map((e) => e as Map).toList(); - } - - static Future> getStoreCategories() async { - final result = await _channel.invokeMethod('getStoreCategories'); - final list = jsonDecode(result as String) as List; - return list.cast(); - } - - static Future downloadStoreExtension(String extensionId, String destDir) async { - _log.i('downloadStoreExtension: $extensionId to $destDir'); - final result = await _channel.invokeMethod('downloadStoreExtension', { - 'extension_id': extensionId, - 'dest_dir': destDir, - }); - return result as String; - } - - static Future clearStoreCache() async { - _log.d('clearStoreCache'); - await _channel.invokeMethod('clearStoreCache'); - } - - // ==================== YOUTUBE / COBALT ==================== - - /// Download a track from YouTube using the Cobalt API. - /// YouTube is a lossy-only provider (Opus 256kbps or MP3 320kbps). - /// It does NOT participate in the lossless fallback chain. - static Future> downloadFromYouTube({ - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'opus_256', - int trackNumber = 1, - int discNumber = 1, - String? releaseDate, - String? itemId, - int durationMs = 0, - String? isrc, - String? spotifyId, - String? deezerId, - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - _log.i('downloadFromYouTube: "$trackName" by $artistName (quality: $quality)'); - final request = jsonEncode({ - 'track_name': trackName, - 'artist_name': artistName, - 'album_name': albumName, - 'album_artist': albumArtist ?? artistName, - 'cover_url': coverUrl, - 'output_dir': outputDir, - 'filename_format': filenameFormat, - 'quality': quality, - 'track_number': trackNumber, - 'disc_number': discNumber, - 'release_date': releaseDate ?? '', - 'item_id': itemId ?? '', - 'duration_ms': durationMs, - 'isrc': isrc ?? '', - 'spotify_id': spotifyId ?? '', - 'deezer_id': deezerId ?? '', - 'storage_mode': storageMode, - 'saf_tree_uri': safTreeUri, - 'saf_relative_dir': safRelativeDir, - 'saf_file_name': safFileName, - 'saf_output_ext': safOutputExt, - }); - - final result = await _channel.invokeMethod('downloadFromYouTube', request); - final response = jsonDecode(result as String) as Map; - if (response['success'] == true) { - _log.i('YouTube download success: ${response['file_path']}'); - } else { - _log.w('YouTube download failed: ${response['error']}'); - } - return response; - } -} +import 'dart:convert'; +import 'package:flutter/services.dart'; +import 'package:spotiflac_android/services/download_request_payload.dart'; +import 'package:spotiflac_android/utils/logger.dart'; + +final _log = AppLogger('PlatformBridge'); + +class PlatformBridge { + static const _channel = MethodChannel('com.zarz.spotiflac/backend'); + + static Future> parseSpotifyUrl(String url) async { + _log.d('parseSpotifyUrl: $url'); + final result = await _channel.invokeMethod('parseSpotifyUrl', {'url': url}); + return jsonDecode(result as String) as Map; + } + + static Future> getSpotifyMetadata(String url) async { + _log.d('getSpotifyMetadata: $url'); + final result = await _channel.invokeMethod('getSpotifyMetadata', { + 'url': url, + }); + return jsonDecode(result as String) as Map; + } + + static Future> searchSpotify( + String query, { + int limit = 10, + }) async { + _log.d('searchSpotify: "$query" (limit: $limit)'); + final result = await _channel.invokeMethod('searchSpotify', { + 'query': query, + 'limit': limit, + }); + return jsonDecode(result as String) as Map; + } + + static Future> searchSpotifyAll( + String query, { + int trackLimit = 15, + int artistLimit = 3, + }) async { + _log.d('searchSpotifyAll: "$query"'); + final result = await _channel.invokeMethod('searchSpotifyAll', { + 'query': query, + 'track_limit': trackLimit, + 'artist_limit': artistLimit, + }); + return jsonDecode(result as String) as Map; + } + + static Future> checkAvailability( + String spotifyId, + String isrc, + ) async { + _log.d('checkAvailability: $spotifyId (ISRC: $isrc)'); + final result = await _channel.invokeMethod('checkAvailability', { + 'spotify_id': spotifyId, + 'isrc': isrc, + }); + return jsonDecode(result as String) as Map; + } + + static Future> _invokeDownloadMethod( + String method, + DownloadRequestPayload payload, + ) async { + final request = jsonEncode(payload.toJson()); + final result = await _channel.invokeMethod(method, request); + return jsonDecode(result as String) as Map; + } + + static Future> downloadByStrategy({ + required DownloadRequestPayload payload, + bool? useExtensions, + bool? useFallback, + }) async { + final routedPayload = payload.withStrategy( + useExtensions: useExtensions, + useFallback: useFallback, + ); + _log.i( + 'downloadByStrategy: "${payload.trackName}" by ${payload.artistName} ' + '(service: ${payload.service}, ext: ${routedPayload.useExtensions}, fallback: ${routedPayload.useFallback})', + ); + final response = await _invokeDownloadMethod( + 'downloadByStrategy', + routedPayload, + ); + if (response['success'] == true) { + final service = response['service'] ?? payload.service; + final filePath = response['file_path'] ?? ''; + final bitDepth = response['actual_bit_depth']; + final sampleRate = response['actual_sample_rate']; + final qualityStr = bitDepth != null && sampleRate != null + ? ' ($bitDepth-bit/${(sampleRate / 1000).toStringAsFixed(1)}kHz)' + : ''; + _log.i('Download success via $service$qualityStr: $filePath'); + } else { + final error = response['error'] ?? 'Unknown error'; + final errorType = response['error_type'] ?? ''; + _log.e('Download failed: $error (type: $errorType)'); + } + return response; + } + + @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') + static Future> downloadTrack({ + required String isrc, + required String service, + required String spotifyId, + required String trackName, + required String artistName, + required String albumName, + String? albumArtist, + String? coverUrl, + required String outputDir, + required String filenameFormat, + String quality = 'LOSSLESS', + bool embedLyrics = true, + bool embedMaxQualityCover = true, + int trackNumber = 1, + int discNumber = 1, + int totalTracks = 1, + String? releaseDate, + String? itemId, + int durationMs = 0, + String? source, + String? genre, + String? label, + String? copyright, + String lyricsMode = 'embed', + String storageMode = 'app', + String safTreeUri = '', + String safRelativeDir = '', + String safFileName = '', + String safOutputExt = '', + }) async { + final payload = DownloadRequestPayload( + isrc: isrc, + service: service, + spotifyId: spotifyId, + trackName: trackName, + artistName: artistName, + albumName: albumName, + albumArtist: albumArtist ?? artistName, + coverUrl: coverUrl ?? '', + outputDir: outputDir, + filenameFormat: filenameFormat, + quality: quality, + embedLyrics: embedLyrics, + embedMaxQualityCover: embedMaxQualityCover, + trackNumber: trackNumber, + discNumber: discNumber, + totalTracks: totalTracks, + releaseDate: releaseDate ?? '', + itemId: itemId ?? '', + durationMs: durationMs, + source: source ?? '', + genre: genre ?? '', + label: label ?? '', + copyright: copyright ?? '', + lyricsMode: lyricsMode, + storageMode: storageMode, + safTreeUri: safTreeUri, + safRelativeDir: safRelativeDir, + safFileName: safFileName, + safOutputExt: safOutputExt, + ); + + return downloadByStrategy(payload: payload); + } + + @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') + static Future> downloadWithFallback({ + required String isrc, + required String spotifyId, + required String trackName, + required String artistName, + required String albumName, + String? albumArtist, + String? coverUrl, + required String outputDir, + required String filenameFormat, + String quality = 'LOSSLESS', + bool embedLyrics = true, + bool embedMaxQualityCover = true, + int trackNumber = 1, + int discNumber = 1, + int totalTracks = 1, + String? releaseDate, + String preferredService = 'tidal', + String? itemId, + int durationMs = 0, + String? genre, + String? label, + String? copyright, + String lyricsMode = 'embed', + String storageMode = 'app', + String safTreeUri = '', + String safRelativeDir = '', + String safFileName = '', + String safOutputExt = '', + }) async { + final payload = DownloadRequestPayload( + isrc: isrc, + service: preferredService, + spotifyId: spotifyId, + trackName: trackName, + artistName: artistName, + albumName: albumName, + albumArtist: albumArtist ?? artistName, + coverUrl: coverUrl ?? '', + outputDir: outputDir, + filenameFormat: filenameFormat, + quality: quality, + embedLyrics: embedLyrics, + embedMaxQualityCover: embedMaxQualityCover, + trackNumber: trackNumber, + discNumber: discNumber, + totalTracks: totalTracks, + releaseDate: releaseDate ?? '', + itemId: itemId ?? '', + durationMs: durationMs, + genre: genre ?? '', + label: label ?? '', + copyright: copyright ?? '', + lyricsMode: lyricsMode, + storageMode: storageMode, + safTreeUri: safTreeUri, + safRelativeDir: safRelativeDir, + safFileName: safFileName, + safOutputExt: safOutputExt, + ); + + return downloadByStrategy(payload: payload, useFallback: true); + } + + static Future> getDownloadProgress() async { + final result = await _channel.invokeMethod('getDownloadProgress'); + return jsonDecode(result as String) as Map; + } + + static Future> getAllDownloadProgress() async { + final result = await _channel.invokeMethod('getAllDownloadProgress'); + return jsonDecode(result as String) as Map; + } + + static Future initItemProgress(String itemId) async { + await _channel.invokeMethod('initItemProgress', {'item_id': itemId}); + } + + static Future finishItemProgress(String itemId) async { + await _channel.invokeMethod('finishItemProgress', {'item_id': itemId}); + } + + static Future clearItemProgress(String itemId) async { + await _channel.invokeMethod('clearItemProgress', {'item_id': itemId}); + } + + static Future cancelDownload(String itemId) async { + await _channel.invokeMethod('cancelDownload', {'item_id': itemId}); + } + + static Future setDownloadDirectory(String path) async { + await _channel.invokeMethod('setDownloadDirectory', {'path': path}); + } + + static Future> checkDuplicate( + String outputDir, + String isrc, + ) async { + final result = await _channel.invokeMethod('checkDuplicate', { + 'output_dir': outputDir, + 'isrc': isrc, + }); + return jsonDecode(result as String) as Map; + } + + static Future buildFilename( + String template, + Map metadata, + ) async { + final result = await _channel.invokeMethod('buildFilename', { + 'template': template, + 'metadata': jsonEncode(metadata), + }); + return result as String; + } + + static Future sanitizeFilename(String filename) async { + final result = await _channel.invokeMethod('sanitizeFilename', { + 'filename': filename, + }); + return result as String; + } + + static Future?> pickSafTree() async { + final result = await _channel.invokeMethod('pickSafTree'); + if (result == null) return null; + return jsonDecode(result as String) as Map; + } + + static Future safExists(String uri) async { + final result = await _channel.invokeMethod('safExists', {'uri': uri}); + return result as bool; + } + + static Future safDelete(String uri) async { + final result = await _channel.invokeMethod('safDelete', {'uri': uri}); + return result as bool; + } + + static Future> safStat(String uri) async { + final result = await _channel.invokeMethod('safStat', {'uri': uri}); + return jsonDecode(result as String) as Map; + } + + static Future> resolveSafFile({ + required String treeUri, + required String fileName, + String relativeDir = '', + }) async { + final result = await _channel.invokeMethod('resolveSafFile', { + 'tree_uri': treeUri, + 'relative_dir': relativeDir, + 'file_name': fileName, + }); + return jsonDecode(result as String) as Map; + } + + static Future copyContentUriToTemp(String uri) async { + final result = await _channel.invokeMethod('safCopyToTemp', {'uri': uri}); + return result as String?; + } + + static Future replaceContentUriFromPath( + String uri, + String srcPath, + ) async { + final result = await _channel.invokeMethod('safReplaceFromPath', { + 'uri': uri, + 'src_path': srcPath, + }); + return result as bool; + } + + static Future createSafFileFromPath({ + required String treeUri, + required String relativeDir, + required String fileName, + required String mimeType, + required String srcPath, + }) async { + final result = await _channel.invokeMethod('safCreateFromPath', { + 'tree_uri': treeUri, + 'relative_dir': relativeDir, + 'file_name': fileName, + 'mime_type': mimeType, + 'src_path': srcPath, + }); + return result as String?; + } + + static Future openContentUri(String uri, {String mimeType = ''}) async { + await _channel.invokeMethod('openContentUri', { + 'uri': uri, + 'mime_type': mimeType, + }); + } + + static Future shareContentUri(String uri, {String title = ''}) async { + final result = await _channel.invokeMethod('shareContentUri', { + 'uri': uri, + 'title': title, + }); + return result as bool? ?? false; + } + + static Future> fetchLyrics( + String spotifyId, + String trackName, + String artistName, { + int durationMs = 0, + }) async { + final result = await _channel.invokeMethod('fetchLyrics', { + 'spotify_id': spotifyId, + 'track_name': trackName, + 'artist_name': artistName, + 'duration_ms': durationMs, + }); + return jsonDecode(result as String) as Map; + } + + static Future getLyricsLRC( + String spotifyId, + String trackName, + String artistName, { + String? filePath, + int durationMs = 0, + }) async { + final result = await _channel.invokeMethod('getLyricsLRC', { + 'spotify_id': spotifyId, + 'track_name': trackName, + 'artist_name': artistName, + 'file_path': filePath ?? '', + 'duration_ms': durationMs, + }); + return result as String; + } + + static Future> embedLyricsToFile( + String filePath, + String lyrics, + ) async { + final result = await _channel.invokeMethod('embedLyricsToFile', { + 'file_path': filePath, + 'lyrics': lyrics, + }); + return jsonDecode(result as String) as Map; + } + + static Future cleanupConnections() async { + await _channel.invokeMethod('cleanupConnections'); + } + + static Future> downloadCoverToFile( + String coverUrl, + String outputPath, { + bool maxQuality = true, + }) async { + final result = await _channel.invokeMethod('downloadCoverToFile', { + 'cover_url': coverUrl, + 'output_path': outputPath, + 'max_quality': maxQuality, + }); + return jsonDecode(result as String) as Map; + } + + static Future> extractCoverToFile( + String audioPath, + String outputPath, + ) async { + final result = await _channel.invokeMethod('extractCoverToFile', { + 'audio_path': audioPath, + 'output_path': outputPath, + }); + return jsonDecode(result as String) as Map; + } + + static Future> fetchAndSaveLyrics({ + required String trackName, + required String artistName, + required String spotifyId, + required int durationMs, + required String outputPath, + }) async { + final result = await _channel.invokeMethod('fetchAndSaveLyrics', { + 'track_name': trackName, + 'artist_name': artistName, + 'spotify_id': spotifyId, + 'duration_ms': durationMs, + 'output_path': outputPath, + }); + return jsonDecode(result as String) as Map; + } + + static Future> reEnrichFile( + Map request, + ) async { + final requestJSON = jsonEncode(request); + final result = await _channel.invokeMethod('reEnrichFile', { + 'request_json': requestJSON, + }); + return jsonDecode(result as String) as Map; + } + + static Future> readFileMetadata(String filePath) async { + final result = await _channel.invokeMethod('readFileMetadata', { + 'file_path': filePath, + }); + return jsonDecode(result as String) as Map; + } + + static Future> editFileMetadata( + String filePath, + Map metadata, + ) async { + final metadataJSON = jsonEncode(metadata); + final result = await _channel.invokeMethod('editFileMetadata', { + 'file_path': filePath, + 'metadata_json': metadataJSON, + }); + return jsonDecode(result as String) as Map; + } + + static Future writeTempToSaf(String tempPath, String safUri) async { + final result = await _channel.invokeMethod('writeTempToSaf', { + 'temp_path': tempPath, + 'saf_uri': safUri, + }); + final map = jsonDecode(result as String) as Map; + return map['success'] == true; + } + + static Future startDownloadService({ + String trackName = '', + String artistName = '', + int queueCount = 0, + }) async { + await _channel.invokeMethod('startDownloadService', { + 'track_name': trackName, + 'artist_name': artistName, + 'queue_count': queueCount, + }); + } + + static Future stopDownloadService() async { + await _channel.invokeMethod('stopDownloadService'); + } + + static Future updateDownloadServiceProgress({ + required String trackName, + required String artistName, + required int progress, + required int total, + required int queueCount, + }) async { + await _channel.invokeMethod('updateDownloadServiceProgress', { + 'track_name': trackName, + 'artist_name': artistName, + 'progress': progress, + 'total': total, + 'queue_count': queueCount, + }); + } + + static Future isDownloadServiceRunning() async { + final result = await _channel.invokeMethod('isDownloadServiceRunning'); + return result as bool; + } + + static Future setSpotifyCredentials( + String clientId, + String clientSecret, + ) async { + await _channel.invokeMethod('setSpotifyCredentials', { + 'client_id': clientId, + 'client_secret': clientSecret, + }); + } + + static Future hasSpotifyCredentials() async { + final result = await _channel.invokeMethod('hasSpotifyCredentials'); + return result as bool; + } + + static Future preWarmTrackCache( + List> tracks, + ) async { + final tracksJson = jsonEncode(tracks); + await _channel.invokeMethod('preWarmTrackCache', {'tracks': tracksJson}); + } + + static Future getTrackCacheSize() async { + final result = await _channel.invokeMethod('getTrackCacheSize'); + return result as int; + } + + static Future clearTrackCache() async { + await _channel.invokeMethod('clearTrackCache'); + } + + static Future> searchDeezerAll( + String query, { + int trackLimit = 15, + int artistLimit = 2, + String? filter, + }) async { + final result = await _channel.invokeMethod('searchDeezerAll', { + 'query': query, + 'track_limit': trackLimit, + 'artist_limit': artistLimit, + 'filter': filter ?? '', + }); + return jsonDecode(result as String) as Map; + } + + static Future> getDeezerMetadata( + String resourceType, + String resourceId, + ) async { + final result = await _channel.invokeMethod('getDeezerMetadata', { + 'resource_type': resourceType, + 'resource_id': resourceId, + }); + if (result == null) { + throw Exception( + 'getDeezerMetadata returned null for $resourceType:$resourceId', + ); + } + return jsonDecode(result as String) as Map; + } + + static Future> parseDeezerUrl(String url) async { + final result = await _channel.invokeMethod('parseDeezerUrl', {'url': url}); + return jsonDecode(result as String) as Map; + } + + static Future> parseTidalUrl(String url) async { + final result = await _channel.invokeMethod('parseTidalUrl', {'url': url}); + return jsonDecode(result as String) as Map; + } + + static Future> convertTidalToSpotifyDeezer( + String tidalUrl, + ) async { + final result = await _channel.invokeMethod('convertTidalToSpotifyDeezer', { + 'url': tidalUrl, + }); + return jsonDecode(result as String) as Map; + } + + static Future> searchDeezerByISRC(String isrc) async { + final result = await _channel.invokeMethod('searchDeezerByISRC', { + 'isrc': isrc, + }); + return jsonDecode(result as String) as Map; + } + + static Future?> getDeezerExtendedMetadata( + String trackId, + ) async { + try { + final result = await _channel.invokeMethod('getDeezerExtendedMetadata', { + 'track_id': trackId, + }); + if (result == null) return null; + final data = jsonDecode(result as String) as Map; + return { + 'genre': data['genre'] as String? ?? '', + 'label': data['label'] as String? ?? '', + }; + } catch (e) { + _log.w('Failed to get Deezer extended metadata for $trackId: $e'); + return null; + } + } + + static Future> convertSpotifyToDeezer( + String resourceType, + String spotifyId, + ) async { + final result = await _channel.invokeMethod('convertSpotifyToDeezer', { + 'resource_type': resourceType, + 'spotify_id': spotifyId, + }); + return jsonDecode(result as String) as Map; + } + + static Future> getSpotifyMetadataWithFallback( + String url, + ) async { + final result = await _channel.invokeMethod( + 'getSpotifyMetadataWithFallback', + {'url': url}, + ); + return jsonDecode(result as String) as Map; + } + + static Future>> getGoLogs() async { + final result = await _channel.invokeMethod('getLogs'); + final logs = jsonDecode(result as String) as List; + return logs.map((e) => e as Map).toList(); + } + + static Future> getGoLogsSince(int index) async { + final result = await _channel.invokeMethod('getLogsSince', { + 'index': index, + }); + return jsonDecode(result as String) as Map; + } + + static Future clearGoLogs() async { + await _channel.invokeMethod('clearLogs'); + } + + static Future getGoLogCount() async { + final result = await _channel.invokeMethod('getLogCount'); + return result as int; + } + + static Future setGoLoggingEnabled(bool enabled) async { + await _channel.invokeMethod('setLoggingEnabled', {'enabled': enabled}); + } + + static Future initExtensionSystem( + String extensionsDir, + String dataDir, + ) async { + _log.d('initExtensionSystem: $extensionsDir, $dataDir'); + await _channel.invokeMethod('initExtensionSystem', { + 'extensions_dir': extensionsDir, + 'data_dir': dataDir, + }); + } + + static Future> loadExtensionsFromDir( + String dirPath, + ) async { + _log.d('loadExtensionsFromDir: $dirPath'); + final result = await _channel.invokeMethod('loadExtensionsFromDir', { + 'dir_path': dirPath, + }); + return jsonDecode(result as String) as Map; + } + + static Future> loadExtensionFromPath( + String filePath, + ) async { + _log.d('loadExtensionFromPath: $filePath'); + final result = await _channel.invokeMethod('loadExtensionFromPath', { + 'file_path': filePath, + }); + return jsonDecode(result as String) as Map; + } + + static Future unloadExtension(String extensionId) async { + _log.d('unloadExtension: $extensionId'); + await _channel.invokeMethod('unloadExtension', { + 'extension_id': extensionId, + }); + } + + static Future removeExtension(String extensionId) async { + _log.d('removeExtension: $extensionId'); + await _channel.invokeMethod('removeExtension', { + 'extension_id': extensionId, + }); + } + + static Future> upgradeExtension(String filePath) async { + _log.d('upgradeExtension: $filePath'); + final result = await _channel.invokeMethod('upgradeExtension', { + 'file_path': filePath, + }); + return jsonDecode(result as String) as Map; + } + + static Future> checkExtensionUpgrade( + String filePath, + ) async { + _log.d('checkExtensionUpgrade: $filePath'); + final result = await _channel.invokeMethod('checkExtensionUpgrade', { + 'file_path': filePath, + }); + return jsonDecode(result as String) as Map; + } + + static Future>> getInstalledExtensions() async { + final result = await _channel.invokeMethod('getInstalledExtensions'); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + static Future setExtensionEnabled( + String extensionId, + bool enabled, + ) async { + _log.d('setExtensionEnabled: $extensionId = $enabled'); + await _channel.invokeMethod('setExtensionEnabled', { + 'extension_id': extensionId, + 'enabled': enabled, + }); + } + + static Future setProviderPriority(List providerIds) async { + _log.d('setProviderPriority: $providerIds'); + await _channel.invokeMethod('setProviderPriority', { + 'priority': jsonEncode(providerIds), + }); + } + + static Future> getProviderPriority() async { + final result = await _channel.invokeMethod('getProviderPriority'); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as String).toList(); + } + + static Future setMetadataProviderPriority( + List providerIds, + ) async { + _log.d('setMetadataProviderPriority: $providerIds'); + await _channel.invokeMethod('setMetadataProviderPriority', { + 'priority': jsonEncode(providerIds), + }); + } + + static Future> getMetadataProviderPriority() async { + final result = await _channel.invokeMethod('getMetadataProviderPriority'); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as String).toList(); + } + + static Future> getExtensionSettings( + String extensionId, + ) async { + final result = await _channel.invokeMethod('getExtensionSettings', { + 'extension_id': extensionId, + }); + return jsonDecode(result as String) as Map; + } + + static Future setExtensionSettings( + String extensionId, + Map settings, + ) async { + _log.d('setExtensionSettings: $extensionId'); + await _channel.invokeMethod('setExtensionSettings', { + 'extension_id': extensionId, + 'settings': jsonEncode(settings), + }); + } + + static Future> invokeExtensionAction( + String extensionId, + String actionName, + ) async { + _log.d('invokeExtensionAction: $extensionId.$actionName'); + final result = await _channel.invokeMethod('invokeExtensionAction', { + 'extension_id': extensionId, + 'action': actionName, + }); + if (result == null || (result as String).isEmpty) { + return {'success': true}; + } + return jsonDecode(result) as Map; + } + + static Future>> searchTracksWithExtensions( + String query, { + int limit = 20, + }) async { + _log.d('searchTracksWithExtensions: "$query"'); + final result = await _channel.invokeMethod('searchTracksWithExtensions', { + 'query': query, + 'limit': limit, + }); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') + static Future> downloadWithExtensions({ + required String isrc, + required String spotifyId, + required String trackName, + required String artistName, + required String albumName, + String? albumArtist, + String? coverUrl, + required String outputDir, + required String filenameFormat, + String quality = 'LOSSLESS', + bool embedLyrics = true, + bool embedMaxQualityCover = true, + int trackNumber = 1, + int discNumber = 1, + int totalTracks = 1, + String? releaseDate, + String? itemId, + int durationMs = 0, + String? source, + String? genre, + String? label, + String? copyright, + String? tidalId, + String? qobuzId, + String? deezerId, + String lyricsMode = 'embed', + String? preferredService, + String storageMode = 'app', + String safTreeUri = '', + String safRelativeDir = '', + String safFileName = '', + String safOutputExt = '', + }) async { + final payload = DownloadRequestPayload( + isrc: isrc, + service: preferredService ?? '', + spotifyId: spotifyId, + trackName: trackName, + artistName: artistName, + albumName: albumName, + albumArtist: albumArtist ?? artistName, + coverUrl: coverUrl ?? '', + outputDir: outputDir, + filenameFormat: filenameFormat, + quality: quality, + embedLyrics: embedLyrics, + embedMaxQualityCover: embedMaxQualityCover, + trackNumber: trackNumber, + discNumber: discNumber, + totalTracks: totalTracks, + releaseDate: releaseDate ?? '', + itemId: itemId ?? '', + durationMs: durationMs, + source: source ?? '', + genre: genre ?? '', + label: label ?? '', + copyright: copyright ?? '', + tidalId: tidalId ?? '', + qobuzId: qobuzId ?? '', + deezerId: deezerId ?? '', + lyricsMode: lyricsMode, + storageMode: storageMode, + safTreeUri: safTreeUri, + safRelativeDir: safRelativeDir, + safFileName: safFileName, + safOutputExt: safOutputExt, + ); + + return downloadByStrategy(payload: payload, useExtensions: true); + } + + static Future cleanupExtensions() async { + _log.d('cleanupExtensions'); + await _channel.invokeMethod('cleanupExtensions'); + } + + static Future?> getExtensionPendingAuth( + String extensionId, + ) async { + final result = await _channel.invokeMethod('getExtensionPendingAuth', { + 'extension_id': extensionId, + }); + if (result == null) return null; + return jsonDecode(result as String) as Map; + } + + static Future setExtensionAuthCode( + String extensionId, + String authCode, + ) async { + _log.d('setExtensionAuthCode: $extensionId'); + await _channel.invokeMethod('setExtensionAuthCode', { + 'extension_id': extensionId, + 'auth_code': authCode, + }); + } + + static Future setExtensionTokens( + String extensionId, { + required String accessToken, + String? refreshToken, + int? expiresIn, + }) async { + _log.d('setExtensionTokens: $extensionId'); + await _channel.invokeMethod('setExtensionTokens', { + 'extension_id': extensionId, + 'access_token': accessToken, + 'refresh_token': refreshToken ?? '', + 'expires_in': expiresIn ?? 0, + }); + } + + static Future clearExtensionPendingAuth(String extensionId) async { + await _channel.invokeMethod('clearExtensionPendingAuth', { + 'extension_id': extensionId, + }); + } + + static Future isExtensionAuthenticated(String extensionId) async { + final result = await _channel.invokeMethod('isExtensionAuthenticated', { + 'extension_id': extensionId, + }); + return result as bool; + } + + static Future>> getAllPendingAuthRequests() async { + final result = await _channel.invokeMethod('getAllPendingAuthRequests'); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + static Future?> getPendingFFmpegCommand( + String commandId, + ) async { + final result = await _channel.invokeMethod('getPendingFFmpegCommand', { + 'command_id': commandId, + }); + if (result == null) return null; + return jsonDecode(result as String) as Map; + } + + static Future setFFmpegCommandResult( + String commandId, { + required bool success, + String output = '', + String error = '', + }) async { + await _channel.invokeMethod('setFFmpegCommandResult', { + 'command_id': commandId, + 'success': success, + 'output': output, + 'error': error, + }); + } + + static Future>> + getAllPendingFFmpegCommands() async { + final result = await _channel.invokeMethod('getAllPendingFFmpegCommands'); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + static Future>> customSearchWithExtension( + String extensionId, + String query, { + Map? options, + }) async { + final result = await _channel.invokeMethod('customSearchWithExtension', { + 'extension_id': extensionId, + 'query': query, + 'options': options != null ? jsonEncode(options) : '', + }); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + static Future>> getSearchProviders() async { + final result = await _channel.invokeMethod('getSearchProviders'); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + static Future?> handleURLWithExtension( + String url, + ) async { + try { + final result = await _channel.invokeMethod('handleURLWithExtension', { + 'url': url, + }); + if (result == null || result == '') return null; + return jsonDecode(result as String) as Map; + } catch (e) { + return null; + } + } + + static Future findURLHandler(String url) async { + final result = await _channel.invokeMethod('findURLHandler', {'url': url}); + if (result == null || result == '') return null; + return result as String; + } + + static Future>> getURLHandlers() async { + final result = await _channel.invokeMethod('getURLHandlers'); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + static Future?> getAlbumWithExtension( + String extensionId, + String albumId, + ) async { + try { + final result = await _channel.invokeMethod('getAlbumWithExtension', { + 'extension_id': extensionId, + 'album_id': albumId, + }); + if (result == null || result == '') return null; + return jsonDecode(result as String) as Map; + } catch (e) { + _log.e('getAlbumWithExtension failed: $e'); + return null; + } + } + + static Future?> getPlaylistWithExtension( + String extensionId, + String playlistId, + ) async { + try { + final result = await _channel.invokeMethod('getPlaylistWithExtension', { + 'extension_id': extensionId, + 'playlist_id': playlistId, + }); + if (result == null || result == '') return null; + return jsonDecode(result as String) as Map; + } catch (e) { + _log.e('getPlaylistWithExtension failed: $e'); + return null; + } + } + + static Future?> getArtistWithExtension( + String extensionId, + String artistId, + ) async { + try { + final result = await _channel.invokeMethod('getArtistWithExtension', { + 'extension_id': extensionId, + 'artist_id': artistId, + }); + if (result == null || result == '') return null; + return jsonDecode(result as String) as Map; + } catch (e) { + _log.e('getArtistWithExtension failed: $e'); + return null; + } + } + + static Future?> getExtensionHomeFeed( + String extensionId, + ) async { + try { + final result = await _channel.invokeMethod('getExtensionHomeFeed', { + 'extension_id': extensionId, + }); + if (result == null || result == '') return null; + return jsonDecode(result as String) as Map; + } catch (e) { + _log.e('getExtensionHomeFeed failed: $e'); + return null; + } + } + + static Future?> getExtensionBrowseCategories( + String extensionId, + ) async { + try { + final result = await _channel.invokeMethod( + 'getExtensionBrowseCategories', + {'extension_id': extensionId}, + ); + if (result == null || result == '') return null; + return jsonDecode(result as String) as Map; + } catch (e) { + _log.e('getExtensionBrowseCategories failed: $e'); + return null; + } + } + + // ==================== LOCAL LIBRARY SCANNING ==================== + + /// Set the directory for caching extracted cover art + static Future setLibraryCoverCacheDir(String cacheDir) async { + _log.i('setLibraryCoverCacheDir: $cacheDir'); + await _channel.invokeMethod('setLibraryCoverCacheDir', { + 'cache_dir': cacheDir, + }); + } + + /// Scan a folder for audio files and read their metadata + /// Returns a list of track metadata + static Future>> scanLibraryFolder( + String folderPath, + ) async { + _log.i('scanLibraryFolder: $folderPath'); + final result = await _channel.invokeMethod('scanLibraryFolder', { + 'folder_path': folderPath, + }); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + /// Perform an incremental scan of the library folder + /// Only scans files that are new or have changed since last scan + /// [existingFiles] is a map of filePath -> modTime (unix millis) + /// Returns IncrementalScanResult with scanned items, deleted paths, and skip count + static Future> scanLibraryFolderIncremental( + String folderPath, + Map existingFiles, + ) async { + _log.i( + 'scanLibraryFolderIncremental: $folderPath (${existingFiles.length} existing files)', + ); + final result = await _channel.invokeMethod('scanLibraryFolderIncremental', { + 'folder_path': folderPath, + 'existing_files': jsonEncode(existingFiles), + }); + return jsonDecode(result as String) as Map; + } + + static Future>> scanSafTree(String treeUri) async { + _log.i('scanSafTree: $treeUri'); + final result = await _channel.invokeMethod('scanSafTree', { + 'tree_uri': treeUri, + }); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + /// Incremental SAF tree scan - only scans new or modified files + /// Returns a map with 'files' (new/changed) and 'removedUris' (deleted files) + static Future> scanSafTreeIncremental( + String treeUri, + Map existingFiles, + ) async { + _log.i( + 'scanSafTreeIncremental: $treeUri (${existingFiles.length} existing files)', + ); + final result = await _channel.invokeMethod('scanSafTreeIncremental', { + 'tree_uri': treeUri, + 'existing_files': jsonEncode(existingFiles), + }); + return jsonDecode(result as String) as Map; + } + + /// Get last-modified timestamps for a list of SAF file URIs. + /// Returns map uri -> modTime (unix millis), only for files that still exist. + static Future> getSafFileModTimes(List uris) async { + final result = await _channel.invokeMethod('getSafFileModTimes', { + 'uris': jsonEncode(uris), + }); + final map = jsonDecode(result as String) as Map; + return map.map((key, value) => MapEntry(key, (value as num).toInt())); + } + + /// Get current library scan progress + static Future> getLibraryScanProgress() async { + final result = await _channel.invokeMethod('getLibraryScanProgress'); + return jsonDecode(result as String) as Map; + } + + /// Cancel ongoing library scan + static Future cancelLibraryScan() async { + await _channel.invokeMethod('cancelLibraryScan'); + } + + /// Read metadata from a single audio file + static Future?> readAudioMetadata( + String filePath, + ) async { + try { + final result = await _channel.invokeMethod('readAudioMetadata', { + 'file_path': filePath, + }); + if (result == null || result == '') return null; + return jsonDecode(result as String) as Map; + } catch (e) { + _log.w('Failed to read audio metadata: $e'); + return null; + } + } + + static Future> runPostProcessing( + String filePath, { + Map? metadata, + }) async { + final result = await _channel.invokeMethod('runPostProcessing', { + 'file_path': filePath, + 'metadata': metadata != null ? jsonEncode(metadata) : '', + }); + return jsonDecode(result as String) as Map; + } + + static Future> runPostProcessingV2( + String filePath, { + Map? metadata, + }) async { + final input = {}; + if (filePath.startsWith('content://')) { + input['uri'] = filePath; + } else { + input['path'] = filePath; + } + final result = await _channel.invokeMethod('runPostProcessingV2', { + 'input': jsonEncode(input), + 'metadata': metadata != null ? jsonEncode(metadata) : '', + }); + return jsonDecode(result as String) as Map; + } + + static Future>> getPostProcessingProviders() async { + final result = await _channel.invokeMethod('getPostProcessingProviders'); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + static Future initExtensionStore(String cacheDir) async { + _log.d('initExtensionStore: $cacheDir'); + await _channel.invokeMethod('initExtensionStore', {'cache_dir': cacheDir}); + } + + static Future>> getStoreExtensions({ + bool forceRefresh = false, + }) async { + _log.d('getStoreExtensions (forceRefresh: $forceRefresh)'); + final result = await _channel.invokeMethod('getStoreExtensions', { + 'force_refresh': forceRefresh, + }); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + static Future>> searchStoreExtensions( + String query, { + String? category, + }) async { + _log.d('searchStoreExtensions: "$query" (category: $category)'); + final result = await _channel.invokeMethod('searchStoreExtensions', { + 'query': query, + 'category': category ?? '', + }); + final list = jsonDecode(result as String) as List; + return list.map((e) => e as Map).toList(); + } + + static Future> getStoreCategories() async { + final result = await _channel.invokeMethod('getStoreCategories'); + final list = jsonDecode(result as String) as List; + return list.cast(); + } + + static Future downloadStoreExtension( + String extensionId, + String destDir, + ) async { + _log.i('downloadStoreExtension: $extensionId to $destDir'); + final result = await _channel.invokeMethod('downloadStoreExtension', { + 'extension_id': extensionId, + 'dest_dir': destDir, + }); + return result as String; + } + + static Future clearStoreCache() async { + _log.d('clearStoreCache'); + await _channel.invokeMethod('clearStoreCache'); + } + + // ==================== YOUTUBE / COBALT ==================== + + /// Download a track from YouTube using the Cobalt API. + /// YouTube is a lossy-only provider (Opus 256kbps or MP3 320kbps). + /// It does NOT participate in the lossless fallback chain. + @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') + static Future> downloadFromYouTube({ + required String trackName, + required String artistName, + required String albumName, + String? albumArtist, + String? coverUrl, + required String outputDir, + required String filenameFormat, + String quality = 'opus_256', + int trackNumber = 1, + int discNumber = 1, + String? releaseDate, + String? itemId, + int durationMs = 0, + String? isrc, + String? spotifyId, + String? deezerId, + bool embedLyrics = true, + bool embedMaxQualityCover = true, + int totalTracks = 1, + String? genre, + String? label, + String? copyright, + String lyricsMode = 'embed', + String storageMode = 'app', + String safTreeUri = '', + String safRelativeDir = '', + String safFileName = '', + String safOutputExt = '', + }) async { + final payload = DownloadRequestPayload( + isrc: isrc ?? '', + service: 'youtube', + spotifyId: spotifyId ?? '', + trackName: trackName, + artistName: artistName, + albumName: albumName, + albumArtist: albumArtist ?? artistName, + coverUrl: coverUrl ?? '', + outputDir: outputDir, + filenameFormat: filenameFormat, + quality: quality, + embedLyrics: embedLyrics, + embedMaxQualityCover: embedMaxQualityCover, + trackNumber: trackNumber, + discNumber: discNumber, + totalTracks: totalTracks, + releaseDate: releaseDate ?? '', + itemId: itemId ?? '', + durationMs: durationMs, + deezerId: deezerId ?? '', + genre: genre ?? '', + label: label ?? '', + copyright: copyright ?? '', + lyricsMode: lyricsMode, + storageMode: storageMode, + safTreeUri: safTreeUri, + safRelativeDir: safRelativeDir, + safFileName: safFileName, + safOutputExt: safOutputExt, + ); + + return downloadByStrategy(payload: payload); + } +} diff --git a/pubspec.lock b/pubspec.lock index 59901618..a250b404 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -189,10 +189,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + sha256: "33bae12a398f841c6cda09d1064212957265869104c478e5ad51e2fb26c3973c" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "7.0.0" connectivity_plus_platform_interface: dependency: transitive description: @@ -386,34 +386,34 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875" + sha256: "76cd20bcfa72fabe50ea27eeaf165527f446f55d3033021462084b87805b4cac" url: "https://pub.dev" source: hosted - version: "19.5.0" + version: "20.0.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5 + sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe" + sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "10.0.0" flutter_local_notifications_windows: dependency: transitive description: name: flutter_local_notifications_windows - sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf" + sha256: "7ddd964fa85b6a23e96956c5b63ef55cdb9e5947b71b95712204db42ad46da61" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -439,50 +439,50 @@ packages: dependency: "direct main" description: name: flutter_secure_storage - sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 url: "https://pub.dev" source: hosted - version: "9.2.4" + version: "10.0.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" + url: "https://pub.dev" + source: hosted + version: "0.2.0" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" url: "https://pub.dev" source: hosted - version: "1.2.3" - flutter_secure_storage_macos: - dependency: transitive - description: - name: flutter_secure_storage_macos - sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" - url: "https://pub.dev" - source: hosted - version: "3.1.3" + version: "3.0.0" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.0.1" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.1.0" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.1.0" flutter_svg: dependency: "direct main" description: @@ -741,14 +741,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - palette_generator: - dependency: "direct main" - description: - name: palette_generator - sha256: "4420f7ccc3f0a4a906144e73f8b6267cd940b64f57a7262e95cb8cec3a8ae0ed" - url: "https://pub.dev" - source: hosted - version: "0.3.3+7" path: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3073f601..c7ce0a98 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music publish_to: "none" -version: 3.6.0+77 +version: 3.6.1+78 environment: sdk: ^3.10.0 From 1a77ded99737ce7f7966d1c01252e410f8b46195 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 10 Feb 2026 10:15:53 +0700 Subject: [PATCH 009/145] refactor: remove deprecated download methods from PlatformBridge and MainActivity --- CHANGELOG.md | 28 ++ .../kotlin/com/zarz/spotiflac/MainActivity.kt | 36 --- lib/services/platform_bridge.dart | 269 ------------------ 3 files changed, 28 insertions(+), 305 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de083e93..19c1ec95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,40 @@ - "Use Primary Artist Only" setting: strips featured artists from folder names (e.g. "Justin Bieber, Quavo" becomes "Justin Bieber") for cleaner folder organization - Supports separators: `, ` `;` `&` `feat.` `ft.` `featuring` `with` `x` - Available in Settings > Download > below "Use Album Artist for folders" +- Unified download request contract (`DownloadRequestPayload`) for all providers/flows + - Includes full superset fields: lyrics mode, genre/label/copyright, provider IDs, SAF params, cover/quality settings + - Added strategy flags in payload: `use_extensions`, `use_fallback` +- New Go unified router entrypoint: `DownloadByStrategy(requestJSON)` + - Routing priority: YouTube service -> extension fallback -> built-in fallback -> direct service +- New Android method channel handler: `"downloadByStrategy"` -> `Gobackend.downloadByStrategy(...)` + +### Changed + +- Download queue execution now builds one payload and uses a single bridge entrypoint (`PlatformBridge.downloadByStrategy`) instead of branching into multiple bridge methods +- Dart `downloadByStrategy` now sends a single request to Go (`downloadByStrategy` channel); routing concern is centralized in Go backend +- Legacy Dart bridge methods (`downloadTrack`, `downloadWithFallback`, `downloadWithExtensions`, `downloadFromYouTube`) are now thin wrappers and marked `@Deprecated` ### Fixed - Fixed lyrics mode "External .LRC" still embedding lyrics into metadata - `lyrics_mode` was not being sent to Go backend for single-service downloads and YouTube provider, causing Go to default to "embed" - Fixed `flutter_local_notifications` v20 breaking changes - migrated all `initialize()`, `show()`, and `cancel()` calls from positional parameters to named parameters - Fixed SAF duplicate folder bug: concurrent batch downloads creating empty folders with `(1)`, `(2)`, `(3)` suffixes - added synchronized lock to `ensureDocumentDir` in Kotlin with duplicate detection and cleanup +- Inconsistent parameter parity across download paths + - `downloadWithExtensions` now carries `copyright` + - YouTube path now carries `embed_max_quality_cover` and metadata parity fields +- Inconsistent success response metadata between direct/fallback flows + - Added shared Go response builder for `DownloadTrack` and `DownloadWithFallback` + - Success responses now consistently include `genre`, `label`, `copyright`, and `lyrics_lrc` +- YouTube success response now also includes extended metadata fields (`cover_url`, `genre`, `label`, `copyright`) for parity with other providers + +### Technical + +- Centralized request serialization in `PlatformBridge` via shared invoke helper and unified payload model +- Go strategy router normalizes incoming service casing before dispatch +- Verified integration after AAR refresh with: + - `flutter analyze` + - `go test -v ./...` + - Android Kotlin compile check (`:app:compileDebugKotlin`) --- diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 52615fbe..365ad757 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -1294,24 +1294,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - "downloadTrack" -> { - val requestJson = call.arguments as String - val response = withContext(Dispatchers.IO) { - handleSafDownload(requestJson) { json -> - Gobackend.downloadTrack(json) - } - } - result.success(response) - } - "downloadWithFallback" -> { - val requestJson = call.arguments as String - val response = withContext(Dispatchers.IO) { - handleSafDownload(requestJson) { json -> - Gobackend.downloadWithFallback(json) - } - } - result.success(response) - } "downloadByStrategy" -> { val requestJson = call.arguments as String val response = withContext(Dispatchers.IO) { @@ -2120,24 +2102,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - "downloadWithExtensions" -> { - val requestJson = call.arguments as String - val response = withContext(Dispatchers.IO) { - handleSafDownload(requestJson) { json -> - Gobackend.downloadWithExtensionsJSON(json) - } - } - result.success(response) - } - "downloadFromYouTube" -> { - val requestJson = call.arguments as String - val response = withContext(Dispatchers.IO) { - handleSafDownload(requestJson) { json -> - Gobackend.downloadFromYouTube(json) - } - } - result.success(response) - } "enrichTrackWithExtension" -> { val extensionId = call.argument("extension_id") ?: "" val trackJson = call.argument("track") ?: "{}" diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 74bcaed0..9e84c4bf 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -103,137 +103,7 @@ class PlatformBridge { return response; } - @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') - static Future> downloadTrack({ - required String isrc, - required String service, - required String spotifyId, - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'LOSSLESS', - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int trackNumber = 1, - int discNumber = 1, - int totalTracks = 1, - String? releaseDate, - String? itemId, - int durationMs = 0, - String? source, - String? genre, - String? label, - String? copyright, - String lyricsMode = 'embed', - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - final payload = DownloadRequestPayload( - isrc: isrc, - service: service, - spotifyId: spotifyId, - trackName: trackName, - artistName: artistName, - albumName: albumName, - albumArtist: albumArtist ?? artistName, - coverUrl: coverUrl ?? '', - outputDir: outputDir, - filenameFormat: filenameFormat, - quality: quality, - embedLyrics: embedLyrics, - embedMaxQualityCover: embedMaxQualityCover, - trackNumber: trackNumber, - discNumber: discNumber, - totalTracks: totalTracks, - releaseDate: releaseDate ?? '', - itemId: itemId ?? '', - durationMs: durationMs, - source: source ?? '', - genre: genre ?? '', - label: label ?? '', - copyright: copyright ?? '', - lyricsMode: lyricsMode, - storageMode: storageMode, - safTreeUri: safTreeUri, - safRelativeDir: safRelativeDir, - safFileName: safFileName, - safOutputExt: safOutputExt, - ); - return downloadByStrategy(payload: payload); - } - - @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') - static Future> downloadWithFallback({ - required String isrc, - required String spotifyId, - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'LOSSLESS', - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int trackNumber = 1, - int discNumber = 1, - int totalTracks = 1, - String? releaseDate, - String preferredService = 'tidal', - String? itemId, - int durationMs = 0, - String? genre, - String? label, - String? copyright, - String lyricsMode = 'embed', - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - final payload = DownloadRequestPayload( - isrc: isrc, - service: preferredService, - spotifyId: spotifyId, - trackName: trackName, - artistName: artistName, - albumName: albumName, - albumArtist: albumArtist ?? artistName, - coverUrl: coverUrl ?? '', - outputDir: outputDir, - filenameFormat: filenameFormat, - quality: quality, - embedLyrics: embedLyrics, - embedMaxQualityCover: embedMaxQualityCover, - trackNumber: trackNumber, - discNumber: discNumber, - totalTracks: totalTracks, - releaseDate: releaseDate ?? '', - itemId: itemId ?? '', - durationMs: durationMs, - genre: genre ?? '', - label: label ?? '', - copyright: copyright ?? '', - lyricsMode: lyricsMode, - storageMode: storageMode, - safTreeUri: safTreeUri, - safRelativeDir: safRelativeDir, - safFileName: safFileName, - safOutputExt: safOutputExt, - ); - - return downloadByStrategy(payload: payload, useFallback: true); - } static Future> getDownloadProgress() async { final result = await _channel.invokeMethod('getDownloadProgress'); @@ -849,78 +719,7 @@ class PlatformBridge { return list.map((e) => e as Map).toList(); } - @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') - static Future> downloadWithExtensions({ - required String isrc, - required String spotifyId, - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'LOSSLESS', - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int trackNumber = 1, - int discNumber = 1, - int totalTracks = 1, - String? releaseDate, - String? itemId, - int durationMs = 0, - String? source, - String? genre, - String? label, - String? copyright, - String? tidalId, - String? qobuzId, - String? deezerId, - String lyricsMode = 'embed', - String? preferredService, - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - final payload = DownloadRequestPayload( - isrc: isrc, - service: preferredService ?? '', - spotifyId: spotifyId, - trackName: trackName, - artistName: artistName, - albumName: albumName, - albumArtist: albumArtist ?? artistName, - coverUrl: coverUrl ?? '', - outputDir: outputDir, - filenameFormat: filenameFormat, - quality: quality, - embedLyrics: embedLyrics, - embedMaxQualityCover: embedMaxQualityCover, - trackNumber: trackNumber, - discNumber: discNumber, - totalTracks: totalTracks, - releaseDate: releaseDate ?? '', - itemId: itemId ?? '', - durationMs: durationMs, - source: source ?? '', - genre: genre ?? '', - label: label ?? '', - copyright: copyright ?? '', - tidalId: tidalId ?? '', - qobuzId: qobuzId ?? '', - deezerId: deezerId ?? '', - lyricsMode: lyricsMode, - storageMode: storageMode, - safTreeUri: safTreeUri, - safRelativeDir: safRelativeDir, - safFileName: safFileName, - safOutputExt: safOutputExt, - ); - return downloadByStrategy(payload: payload, useExtensions: true); - } static Future cleanupExtensions() async { _log.d('cleanupExtensions'); @@ -1332,72 +1131,4 @@ class PlatformBridge { // ==================== YOUTUBE / COBALT ==================== - /// Download a track from YouTube using the Cobalt API. - /// YouTube is a lossy-only provider (Opus 256kbps or MP3 320kbps). - /// It does NOT participate in the lossless fallback chain. - @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') - static Future> downloadFromYouTube({ - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'opus_256', - int trackNumber = 1, - int discNumber = 1, - String? releaseDate, - String? itemId, - int durationMs = 0, - String? isrc, - String? spotifyId, - String? deezerId, - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int totalTracks = 1, - String? genre, - String? label, - String? copyright, - String lyricsMode = 'embed', - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - final payload = DownloadRequestPayload( - isrc: isrc ?? '', - service: 'youtube', - spotifyId: spotifyId ?? '', - trackName: trackName, - artistName: artistName, - albumName: albumName, - albumArtist: albumArtist ?? artistName, - coverUrl: coverUrl ?? '', - outputDir: outputDir, - filenameFormat: filenameFormat, - quality: quality, - embedLyrics: embedLyrics, - embedMaxQualityCover: embedMaxQualityCover, - trackNumber: trackNumber, - discNumber: discNumber, - totalTracks: totalTracks, - releaseDate: releaseDate ?? '', - itemId: itemId ?? '', - durationMs: durationMs, - deezerId: deezerId ?? '', - genre: genre ?? '', - label: label ?? '', - copyright: copyright ?? '', - lyricsMode: lyricsMode, - storageMode: storageMode, - safTreeUri: safTreeUri, - safRelativeDir: safRelativeDir, - safFileName: safFileName, - safOutputExt: safOutputExt, - ); - - return downloadByStrategy(payload: payload); - } } From f2941939b74b68536fb018e11ddf116f345c8a6a Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 10 Feb 2026 12:30:38 +0700 Subject: [PATCH 010/145] refactor: preserve extension ID case in DownloadByStrategy, only lowercase built-in providers --- go_backend/exports.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/go_backend/exports.go b/go_backend/exports.go index b6b7a630..1dacbac2 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -412,15 +412,21 @@ func DownloadByStrategy(requestJSON string) (string, error) { return errorResponse("Invalid request: " + err.Error()) } - service := strings.TrimSpace(strings.ToLower(req.Service)) - req.Service = service - normalizedBytes, err := json.Marshal(req) + serviceRaw := strings.TrimSpace(req.Service) + serviceNormalized := strings.ToLower(serviceRaw) + + normalizedReq := req + if serviceNormalized == "youtube" || isBuiltInProvider(serviceNormalized) { + normalizedReq.Service = serviceNormalized + } + + normalizedBytes, err := json.Marshal(normalizedReq) if err != nil { return errorResponse("Invalid request: " + err.Error()) } normalizedJSON := string(normalizedBytes) - if service == "youtube" { + if serviceNormalized == "youtube" { return DownloadFromYouTube(normalizedJSON) } From 3554a7b5b95fa1d96c77a1f06a17acc09b84028d Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 10 Feb 2026 20:46:45 +0700 Subject: [PATCH 011/145] chore: remove Buy Me a Coffee references (account suspended) --- .github/FUNDING.yml | 1 - README.md | 2 +- lib/constants/app_info.dart | 1 - lib/l10n/app_localizations.dart | 12 ----- lib/l10n/app_localizations_de.dart | 7 --- lib/l10n/app_localizations_en.dart | 6 --- lib/l10n/app_localizations_es.dart | 12 ----- lib/l10n/app_localizations_fr.dart | 6 --- lib/l10n/app_localizations_hi.dart | 6 --- lib/l10n/app_localizations_id.dart | 6 --- lib/l10n/app_localizations_ja.dart | 6 --- lib/l10n/app_localizations_ko.dart | 6 --- lib/l10n/app_localizations_nl.dart | 6 --- lib/l10n/app_localizations_pt.dart | 12 ----- lib/l10n/app_localizations_ru.dart | 6 --- lib/l10n/app_localizations_tr.dart | 6 --- lib/l10n/app_localizations_zh.dart | 18 ------- lib/l10n/arb/app_de.arb | 8 --- lib/l10n/arb/app_en.arb | 4 -- lib/l10n/arb/app_es.arb | 8 --- lib/l10n/arb/app_es_ES.arb | 8 --- lib/l10n/arb/app_fr.arb | 8 --- lib/l10n/arb/app_hi.arb | 8 --- lib/l10n/arb/app_id.arb | 10 +--- lib/l10n/arb/app_ja.arb | 8 --- lib/l10n/arb/app_ko.arb | 8 --- lib/l10n/arb/app_nl.arb | 8 --- lib/l10n/arb/app_pt.arb | 8 --- lib/l10n/arb/app_pt_PT.arb | 8 --- lib/l10n/arb/app_ru.arb | 8 --- lib/l10n/arb/app_tr.arb | 8 --- lib/l10n/arb/app_zh.arb | 8 --- lib/l10n/arb/app_zh_CN.arb | 8 --- lib/l10n/arb/app_zh_TW.arb | 8 --- lib/screens/settings/donate_page.dart | 15 ------ lib/widgets/donate_icons.dart | 77 --------------------------- 36 files changed, 2 insertions(+), 343 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c056ec6a..df9839e1 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,3 @@ github: zarzet ko_fi: zarzet -buy_me_a_coffee: zarzet diff --git a/README.md b/README.md index fbf8eb7d..5b7b9da7 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ A: Some countries have restricted access to certain streaming service APIs. If d ### Want to support SpotiFLAC-Mobile? -_If this software is useful and brings you value, consider supporting the project by buying me a coffee. Your support helps keep development going._ +_If this software is useful and brings you value, consider supporting the project. Your support helps keep development going._ [![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/zarzet) diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 75306c04..4ebab8ef 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -17,6 +17,5 @@ class AppInfo { static const String originalGithubUrl = 'https://github.com/afkarxyz/SpotiFLAC'; static const String kofiUrl = 'https://ko-fi.com/zarzet'; - static const String bmacUrl = 'https://buymeacoffee.com/zarzet'; static const String githubSponsorsUrl = 'https://github.com/sponsors/zarzet/'; } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d7713832..d7fd0338 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -928,18 +928,6 @@ abstract class AppLocalizations { /// **'Support'** String get aboutSupport; - /// Donation link - /// - /// In en, this message translates to: - /// **'Buy me a coffee'** - String get aboutBuyMeCoffee; - - /// Subtitle for donation - /// - /// In en, this message translates to: - /// **'Support development on Ko-fi'** - String get aboutBuyMeCoffeeSubtitle; - /// Section for app info /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e11c2238..052b9474 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -469,13 +469,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Spendiere mir einen Kaffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => - 'Unterstütze die Entwicklung auf Ko-fi'; - @override String get aboutApp => 'App'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 784777f1..0bcd87bf 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -457,12 +457,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Buy me a coffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; - @override String get aboutApp => 'App'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index d1eda587..574edcf6 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -457,12 +457,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Buy me a coffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; - @override String get aboutApp => 'App'; @@ -3338,12 +3332,6 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get aboutSupport => 'Soporte'; - @override - String get aboutBuyMeCoffee => 'Invítame a un café'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Apoyar el desarrollo en Ko-fi'; - @override String get aboutApp => 'Aplicación'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 600c9468..61057994 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -457,12 +457,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Buy me a coffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; - @override String get aboutApp => 'App'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 88daceea..a10d26cb 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -457,12 +457,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Buy me a coffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; - @override String get aboutApp => 'App'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 532d79e1..0a011ec9 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -462,12 +462,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get aboutSupport => 'Dukungan'; - @override - String get aboutBuyMeCoffee => 'Belikan saya kopi'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Dukung pengembangan di Ko-fi'; - @override String get aboutApp => 'Aplikasi'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 41f762b1..973227d6 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -453,12 +453,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get aboutSupport => 'サポート'; - @override - String get aboutBuyMeCoffee => 'コーヒーを買ってください'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Ko-fi で開発をサポートします'; - @override String get aboutApp => 'アプリ'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index f71686e0..dceb933f 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -457,12 +457,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Buy me a coffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; - @override String get aboutApp => 'App'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 453c445f..5a4eca35 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -457,12 +457,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Buy me a coffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; - @override String get aboutApp => 'App'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 722e3afe..4e8748a5 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -457,12 +457,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Buy me a coffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; - @override String get aboutApp => 'App'; @@ -3337,12 +3331,6 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get aboutSupport => 'Apoiar'; - @override - String get aboutBuyMeCoffee => 'Compre-me um café'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Apoie o desenvolvimento na Ko-fi'; - @override String get aboutApp => 'Aplicativo'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 331eaea1..9ffb5d94 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -470,12 +470,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get aboutSupport => 'Поддержка'; - @override - String get aboutBuyMeCoffee => 'Купить мне кофе'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Поддержать разработку на Ko-fi'; - @override String get aboutApp => 'Приложение'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index dd794add..7a6e5c3a 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -464,12 +464,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get aboutSupport => 'Destek'; - @override - String get aboutBuyMeCoffee => 'Bana bir kahve ısmarla'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Ko-fi üzerinden uygulamayı destekle'; - @override String get aboutApp => 'Uygulama'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b3478881..2e87063e 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -457,12 +457,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Buy me a coffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; - @override String get aboutApp => 'App'; @@ -3348,12 +3342,6 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Buy me a coffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; - @override String get aboutApp => 'App'; @@ -5494,12 +5482,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get aboutSupport => 'Support'; - @override - String get aboutBuyMeCoffee => 'Buy me a coffee'; - - @override - String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; - @override String get aboutApp => 'App'; diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index db6a5afd..435821e3 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -576,14 +576,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Spendiere mir einen Kaffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Unterstütze die Entwicklung auf Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "App", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6b7d627f..ca845fcf 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -326,10 +326,6 @@ "@aboutSocial": {"description": "Section for social links"}, "aboutSupport": "Support", "@aboutSupport": {"description": "Section for support/donation links"}, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": {"description": "Donation link"}, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": {"description": "Subtitle for donation"}, "aboutApp": "App", "@aboutApp": {"description": "Section for app info"}, "aboutVersion": "Version", diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 20c73801..f499cb14 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -548,14 +548,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "App", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_es_ES.arb b/lib/l10n/arb/app_es_ES.arb index f9e67d0b..8983f074 100644 --- a/lib/l10n/arb/app_es_ES.arb +++ b/lib/l10n/arb/app_es_ES.arb @@ -548,14 +548,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Invítame a un café", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Apoyar el desarrollo en Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "Aplicación", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index deb1c103..160ad1ad 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -576,14 +576,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "App", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_hi.arb b/lib/l10n/arb/app_hi.arb index 582a3ec7..b2959a61 100644 --- a/lib/l10n/arb/app_hi.arb +++ b/lib/l10n/arb/app_hi.arb @@ -576,14 +576,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "App", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 648e367f..2c6e8a2b 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -587,15 +587,7 @@ "aboutSupport": "Dukungan", "@aboutSupport": { "description": "Section for support/donation links" - }, - "aboutBuyMeCoffee": "Belikan saya kopi", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Dukung pengembangan di Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, + }, "aboutApp": "Aplikasi", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_ja.arb b/lib/l10n/arb/app_ja.arb index 55fb50c8..aa003de4 100644 --- a/lib/l10n/arb/app_ja.arb +++ b/lib/l10n/arb/app_ja.arb @@ -576,14 +576,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "コーヒーを買ってください", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Ko-fi で開発をサポートします", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "アプリ", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_ko.arb b/lib/l10n/arb/app_ko.arb index e475a220..6dcecff2 100644 --- a/lib/l10n/arb/app_ko.arb +++ b/lib/l10n/arb/app_ko.arb @@ -576,14 +576,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "App", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index 3db0d42a..384b764b 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -576,14 +576,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "App", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index 3c7ff088..654f6c08 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -548,14 +548,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "App", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_pt_PT.arb b/lib/l10n/arb/app_pt_PT.arb index ac8659e6..61589aa2 100644 --- a/lib/l10n/arb/app_pt_PT.arb +++ b/lib/l10n/arb/app_pt_PT.arb @@ -548,14 +548,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Compre-me um café", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Apoie o desenvolvimento na Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "Aplicativo", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_ru.arb b/lib/l10n/arb/app_ru.arb index 4d4fa62d..6869b741 100644 --- a/lib/l10n/arb/app_ru.arb +++ b/lib/l10n/arb/app_ru.arb @@ -576,14 +576,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Купить мне кофе", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Поддержать разработку на Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "Приложение", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_tr.arb b/lib/l10n/arb/app_tr.arb index 18c04bfc..a72454fa 100644 --- a/lib/l10n/arb/app_tr.arb +++ b/lib/l10n/arb/app_tr.arb @@ -576,14 +576,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Bana bir kahve ısmarla", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Ko-fi üzerinden uygulamayı destekle", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "Uygulama", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index e267e87e..aad9e509 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -548,14 +548,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "App", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_zh_CN.arb b/lib/l10n/arb/app_zh_CN.arb index 1823d0ea..d2f0a53b 100644 --- a/lib/l10n/arb/app_zh_CN.arb +++ b/lib/l10n/arb/app_zh_CN.arb @@ -576,14 +576,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "App", "@aboutApp": { "description": "Section for app info" diff --git a/lib/l10n/arb/app_zh_TW.arb b/lib/l10n/arb/app_zh_TW.arb index 3bc88cd5..4e98f0c0 100644 --- a/lib/l10n/arb/app_zh_TW.arb +++ b/lib/l10n/arb/app_zh_TW.arb @@ -576,14 +576,6 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, "aboutApp": "App", "@aboutApp": { "description": "Section for app info" diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index 6a145d52..9608bab9 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -255,21 +255,6 @@ class _DonateLinksCard extends StatelessWidget { endIndent: 16, color: colorScheme.outlineVariant.withValues(alpha: 0.3), ), - _DonateCardItem( - title: 'Buy Me a Coffee', - subtitle: 'buymeacoffee.com/zarzet', - customIcon: const BmacIcon(size: 22, color: Colors.black87), - color: const Color(0xFFFFDD00), - url: AppInfo.bmacUrl, - colorScheme: colorScheme, - ), - Divider( - height: 1, - thickness: 1, - indent: 74, - endIndent: 16, - color: colorScheme.outlineVariant.withValues(alpha: 0.3), - ), _DonateCardItem( title: 'GitHub Sponsors', subtitle: 'github.com/sponsors/zarzet', diff --git a/lib/widgets/donate_icons.dart b/lib/widgets/donate_icons.dart index 80edfe7d..16e83ef8 100644 --- a/lib/widgets/donate_icons.dart +++ b/lib/widgets/donate_icons.dart @@ -84,83 +84,6 @@ class _KofiPainter extends CustomPainter { bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } -class BmacIcon extends StatelessWidget { - final double size; - final Color color; - - const BmacIcon({super.key, this.size = 22, this.color = Colors.black87}); - - @override - Widget build(BuildContext context) { - return CustomPaint( - size: Size(size, size), - painter: _BmacPainter(color), - ); - } -} - -class _BmacPainter extends CustomPainter { - final Color color; - _BmacPainter(this.color); - - @override - void paint(Canvas canvas, Size size) { - final s = size.width; - final paint = Paint() - ..color = color - ..style = PaintingStyle.fill; - - // Cup body (slightly tapered) - final cup = Path() - ..moveTo(s * 0.15, s * 0.35) - ..lineTo(s * 0.20, s * 0.82) - ..quadraticBezierTo(s * 0.20, s * 0.90, s * 0.28, s * 0.90) - ..lineTo(s * 0.56, s * 0.90) - ..quadraticBezierTo(s * 0.64, s * 0.90, s * 0.64, s * 0.82) - ..lineTo(s * 0.69, s * 0.35) - ..close(); - canvas.drawPath(cup, paint); - - // Cup rim - final rim = RRect.fromRectAndRadius( - Rect.fromLTWH(s * 0.10, s * 0.30, s * 0.64, s * 0.10), - Radius.circular(s * 0.05), - ); - canvas.drawRRect(rim, paint); - - // Handle - final handlePaint = Paint() - ..color = color - ..style = PaintingStyle.stroke - ..strokeWidth = s * 0.07 - ..strokeCap = StrokeCap.round; - - final handle = Path() - ..moveTo(s * 0.69, s * 0.42) - ..quadraticBezierTo(s * 0.90, s * 0.42, s * 0.90, s * 0.56) - ..quadraticBezierTo(s * 0.90, s * 0.70, s * 0.69, s * 0.70); - canvas.drawPath(handle, handlePaint); - - // Steam - final steamPaint = Paint() - ..color = color.withValues(alpha: 0.5) - ..style = PaintingStyle.stroke - ..strokeWidth = s * 0.04 - ..strokeCap = StrokeCap.round; - - for (var i = 0; i < 3; i++) { - final sx = s * (0.26 + i * 0.14); - final steam = Path() - ..moveTo(sx, s * 0.26) - ..quadraticBezierTo(sx + s * 0.03, s * 0.18, sx, s * 0.10); - canvas.drawPath(steam, steamPaint); - } - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} - class GitHubIcon extends StatelessWidget { final double size; final Color color; From 42f15018aea3f95caa871b061f04df124863f542 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 10 Feb 2026 23:35:41 +0700 Subject: [PATCH 012/145] v3.6.5: audio format conversion, PC v7.0.8 backend merge, Amazon re-enabled --- CHANGELOG.md | 38 +- go_backend/amazon.go | 312 +++- go_backend/amazon_asin_test.go | 46 + go_backend/audio_metadata.go | 115 ++ go_backend/exports.go | 161 +- go_backend/extension_providers.go | 56 +- go_backend/extension_timeout.go | 2 + go_backend/metadata.go | 84 +- go_backend/qobuz.go | 4 +- go_backend/spotfetch_api.go | 80 + lib/constants/app_info.dart | 4 +- lib/l10n/app_localizations.dart | 64 + lib/l10n/app_localizations_de.dart | 38 + lib/l10n/app_localizations_en.dart | 38 + lib/l10n/app_localizations_es.dart | 38 + lib/l10n/app_localizations_fr.dart | 38 + lib/l10n/app_localizations_hi.dart | 38 + lib/l10n/app_localizations_id.dart | 38 + lib/l10n/app_localizations_ja.dart | 38 + lib/l10n/app_localizations_ko.dart | 38 + lib/l10n/app_localizations_nl.dart | 38 + lib/l10n/app_localizations_pt.dart | 38 + lib/l10n/app_localizations_ru.dart | 38 + lib/l10n/app_localizations_tr.dart | 38 + lib/l10n/app_localizations_zh.dart | 38 + lib/l10n/arb/app_en.arb | 35 +- lib/l10n/arb/app_id.arb | 37 +- lib/providers/download_queue_provider.dart | 160 +- lib/screens/queue_tab.dart | 37 +- lib/screens/settings/donate_page.dart | 1 + .../settings/download_settings_page.dart | 12 +- lib/screens/track_metadata_screen.dart | 1504 +++++++++++++---- lib/services/ffmpeg_service.dart | 240 ++- lib/services/history_database.dart | 177 +- lib/widgets/download_service_picker.dart | 14 +- pubspec.yaml | 2 +- 36 files changed, 3130 insertions(+), 549 deletions(-) create mode 100644 go_backend/amazon_asin_test.go create mode 100644 go_backend/spotfetch_api.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c1ec95..314a9930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,30 +1,56 @@ # Changelog -## [3.6.1] - 2026-02-10 +## [3.6.5] - 2026-02-10 + +### Highlights + +- **Audio Format Conversion**: Convert between FLAC, MP3, and Opus directly from Track Metadata screen with full metadata and cover art preservation +- **PC v7.0.8 Backend Merge**: Adopts several Go backend improvements from SpotiFLAC PC v7.0.8 including Amazon encrypted stream support, SpotFetch metadata fallback, and Qobuz API update +- **Amazon Music Re-enabled**: Amazon provider back in service with new API ### Added - "Use Primary Artist Only" setting: strips featured artists from folder names (e.g. "Justin Bieber, Quavo" becomes "Justin Bieber") for cleaner folder organization - Supports separators: `, ` `;` `&` `feat.` `ft.` `featuring` `with` `x` - Available in Settings > Download > below "Use Album Artist for folders" +- Audio format conversion from Track Metadata screen + - Convert between FLAC, MP3, and Opus formats (any direction) + - Selectable bitrate: 128k, 192k, 256k, 320k + - Full metadata and cover art preservation during conversion + - Confirmation dialog before converting (original file deleted after) + - SAF storage support: copies to temp, converts, writes back via SAF + - Download history automatically updated with new file path - Unified download request contract (`DownloadRequestPayload`) for all providers/flows - Includes full superset fields: lyrics mode, genre/label/copyright, provider IDs, SAF params, cover/quality settings - Added strategy flags in payload: `use_extensions`, `use_fallback` - New Go unified router entrypoint: `DownloadByStrategy(requestJSON)` - Routing priority: YouTube service -> extension fallback -> built-in fallback -> direct service - New Android method channel handler: `"downloadByStrategy"` -> `Gobackend.downloadByStrategy(...)` +- SpotFetch metadata fallback integration for Spotify-blocked regions + - New backend client for `spotify.afkarxyz.fun/api` + - Automatic fallback in Spotify metadata fetch path when primary source fails +- Lyrics extraction now supports MP3 (ID3v2) and Opus/OGG (Vorbis comments) in addition to FLAC + - Includes heuristic detection of lyrics stored in Comment fields ### Changed +- Merged several Go backend improvements from SpotiFLAC PC v7.0.8: Amazon new API with encrypted stream/decryption support, SpotFetch metadata fallback for Spotify-blocked regions, multi-format lyrics extraction (MP3/Opus/OGG), Qobuz Jumo API update. - Download queue execution now builds one payload and uses a single bridge entrypoint (`PlatformBridge.downloadByStrategy`) instead of branching into multiple bridge methods - Dart `downloadByStrategy` now sends a single request to Go (`downloadByStrategy` channel); routing concern is centralized in Go backend - Legacy Dart bridge methods (`downloadTrack`, `downloadWithFallback`, `downloadWithExtensions`, `downloadFromYouTube`) are now thin wrappers and marked `@Deprecated` +- Qobuz downloader updated to latest Jumo API contract (`/get` endpoint, required headers) +- Amazon download flow now returns `decryption_key` from Go and performs decryption in Flutter (local file + SAF paths) +- Amazon now uses the new `amazon.afkarxyz.fun` API flow (ASIN-based track endpoint + legacy fallback) with encrypted stream support +- Amazon ASIN extraction rewritten with robust URL/query-param parsing and regex fallback +- Amazon provider re-enabled in download service picker and download settings (alongside Tidal, Qobuz, and YouTube picker flow) ### Fixed - Fixed lyrics mode "External .LRC" still embedding lyrics into metadata - `lyrics_mode` was not being sent to Go backend for single-service downloads and YouTube provider, causing Go to default to "embed" - Fixed `flutter_local_notifications` v20 breaking changes - migrated all `initialize()`, `show()`, and `cancel()` calls from positional parameters to named parameters - Fixed SAF duplicate folder bug: concurrent batch downloads creating empty folders with `(1)`, `(2)`, `(3)` suffixes - added synchronized lock to `ensureDocumentDir` in Kotlin with duplicate detection and cleanup +- Track Metadata lyrics section now hides "Embed Lyrics" when lyrics are already embedded in file, preventing redundant embed attempts +- Fixed lyrics embed path to support FLAC/MP3/Opus consistently (including SAF files) without forcing unsupported parser paths - Inconsistent parameter parity across download paths - `downloadWithExtensions` now carries `copyright` - YouTube path now carries `embed_max_quality_cover` and metadata parity fields @@ -37,10 +63,12 @@ - Centralized request serialization in `PlatformBridge` via shared invoke helper and unified payload model - Go strategy router normalizes incoming service casing before dispatch -- Verified integration after AAR refresh with: - - `flutter analyze` - - `go test -v ./...` - - Android Kotlin compile check (`:app:compileDebugKotlin`) +- Extension runtime: `customSearch` now passes query/options via VM globals instead of string interpolation, preventing parser edge cases on certain devices +- Extension runtime: JS panic handler now logs full stack trace for easier debugging + +### Removed + +- Buy Me a Coffee references removed from donate page, FUNDING.yml, README, and all localization files (account suspended) --- diff --git a/go_backend/amazon.go b/go_backend/amazon.go index 33bd6203..75cdac82 100644 --- a/go_backend/amazon.go +++ b/go_backend/amazon.go @@ -31,6 +31,8 @@ type AmazonDownloader struct { var ( globalAmazonDownloader *AmazonDownloader amazonDownloaderOnce sync.Once + amazonASINRegex = regexp.MustCompile(`(?i)^B[0-9A-Z]{9}$`) + amazonASINFindRegex = regexp.MustCompile(`(?i)B[0-9A-Z]{9}`) ) // AfkarXYZResponse is the response from AfkarXYZ API @@ -43,6 +45,12 @@ type AfkarXYZResponse struct { } `json:"data"` } +// AmazonStreamResponse is the new response format from amazon.afkarxyz.fun/api/track/{asin} +type AmazonStreamResponse struct { + StreamURL string `json:"streamUrl"` + DecryptionKey string `json:"decryptionKey"` +} + func NewAmazonDownloader() *AmazonDownloader { amazonDownloaderOnce.Do(func() { globalAmazonDownloader = &AmazonDownloader{ @@ -52,10 +60,9 @@ func NewAmazonDownloader() *AmazonDownloader { return globalAmazonDownloader } -// fetchAmazonURLWithRetry fetches from AfkarXYZ API with retry logic for mobile networks -func (a *AmazonDownloader) fetchAmazonURLWithRetry(amazonURL string) (string, string, error) { - apiURL := "https://amazon.afkarxyz.fun/convert?url=" + url.QueryEscape(amazonURL) - +// fetchAmazonURLWithRetry fetches from AfkarXYZ API with retry logic for mobile networks. +// Returns downloadURL, suggested fileName, optional decryptionKey. +func (a *AmazonDownloader) fetchAmazonURLWithRetry(amazonURL string) (string, string, string, error) { var lastErr error for attempt := 0; attempt <= amazonMaxRetries; attempt++ { if attempt > 0 { @@ -64,66 +71,184 @@ func (a *AmazonDownloader) fetchAmazonURLWithRetry(amazonURL string) (string, st time.Sleep(delay) } - downloadURL, fileName, err := a.doAfkarXYZRequest(apiURL) + downloadURL, fileName, decryptionKey, err := a.doAfkarXYZRequest(amazonURL) if err == nil { - return downloadURL, fileName, nil + return downloadURL, fileName, decryptionKey, nil } lastErr = err - errStr := err.Error() + errStr := strings.ToLower(err.Error()) // Check if error is retryable isRetryable := strings.Contains(errStr, "timeout") || strings.Contains(errStr, "connection reset") || strings.Contains(errStr, "connection refused") || - strings.Contains(errStr, "EOF") || + strings.Contains(errStr, "eof") || strings.Contains(errStr, "status 5") || - strings.Contains(errStr, "status 429") + strings.Contains(errStr, "status 429") || + strings.Contains(errStr, "http 429") if !isRetryable { - return "", "", err + return "", "", "", err } GoLog("[Amazon] Attempt %d failed (retryable): %v\n", attempt+1, err) } - return "", "", fmt.Errorf("all %d attempts failed: %w", amazonMaxRetries+1, lastErr) + return "", "", "", fmt.Errorf("all %d attempts failed: %w", amazonMaxRetries+1, lastErr) } -// doAfkarXYZRequest performs a single request to AfkarXYZ API -func (a *AmazonDownloader) doAfkarXYZRequest(apiURL string) (string, string, error) { +func normalizeAmazonASIN(candidate string) string { + trimmed := strings.TrimSpace(candidate) + if trimmed == "" { + return "" + } + + if decoded, err := url.QueryUnescape(trimmed); err == nil { + trimmed = decoded + } + + trimmed = strings.ToUpper(trimmed) + if idx := strings.IndexAny(trimmed, "?#&/"); idx >= 0 { + trimmed = trimmed[:idx] + } + + if amazonASINRegex.MatchString(trimmed) { + return trimmed + } + + return "" +} + +func extractAmazonASIN(amazonURL string) string { + raw := strings.TrimSpace(amazonURL) + if raw == "" { + return "" + } + + parsed, err := url.Parse(raw) + if err == nil { + query := parsed.Query() + + // Prefer track-level ASIN when URL also contains albumAsin. + for _, key := range []string{"trackAsin", "trackasin", "trackASIN", "asin", "ASIN", "i"} { + if asin := normalizeAmazonASIN(query.Get(key)); asin != "" { + return asin + } + } + + path := strings.Trim(parsed.Path, "/") + if path != "" { + segments := strings.Split(path, "/") + + for i := 0; i < len(segments)-1; i++ { + segment := strings.ToLower(strings.TrimSpace(segments[i])) + if segment == "track" || segment == "tracks" { + if asin := normalizeAmazonASIN(segments[i+1]); asin != "" { + return asin + } + } + } + + if asin := normalizeAmazonASIN(segments[len(segments)-1]); asin != "" { + return asin + } + } + } + + match := amazonASINFindRegex.FindString(strings.ToUpper(raw)) + return normalizeAmazonASIN(match) +} + +// doAfkarXYZRequest performs a single request to Amazon API. +// It tries new endpoint first, then falls back to legacy /convert endpoint. +func (a *AmazonDownloader) doAfkarXYZRequest(amazonURL string) (string, string, string, error) { + asin := extractAmazonASIN(amazonURL) + if asin != "" { + GoLog("[Amazon] Using ASIN: %s\n", asin) + downloadURL, fileName, decryptKey, err := a.doAfkarXYZRequestNew(asin) + if err == nil { + return downloadURL, fileName, decryptKey, nil + } + GoLog("[Amazon] New API failed for ASIN %s, trying legacy endpoint: %v\n", asin, err) + } + return a.doAfkarXYZRequestLegacy(amazonURL) +} + +func (a *AmazonDownloader) doAfkarXYZRequestNew(asin string) (string, string, string, error) { ctx, cancel := context.WithTimeout(context.Background(), amazonAPITimeoutMobile) defer cancel() + apiURL := fmt.Sprintf("https://amazon.afkarxyz.fun/api/track/%s", asin) req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { - return "", "", fmt.Errorf("failed to create request: %w", err) + return "", "", "", fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36") + + resp, err := a.client.Do(req) + if err != nil { + return "", "", "", fmt.Errorf("failed to call Amazon API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return "", "", "", fmt.Errorf("Amazon API returned status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", "", "", fmt.Errorf("failed to read response: %w", err) + } + + var apiResp AmazonStreamResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + return "", "", "", fmt.Errorf("failed to decode response: %w", err) + } + + if strings.TrimSpace(apiResp.StreamURL) == "" { + return "", "", "", fmt.Errorf("Amazon API returned empty stream URL") + } + + fileName := asin + ".m4a" + return apiResp.StreamURL, fileName, strings.TrimSpace(apiResp.DecryptionKey), nil +} + +func (a *AmazonDownloader) doAfkarXYZRequestLegacy(amazonURL string) (string, string, string, error) { + ctx, cancel := context.WithTimeout(context.Background(), amazonAPITimeoutMobile) + defer cancel() + + apiURL := "https://amazon.afkarxyz.fun/convert?url=" + url.QueryEscape(amazonURL) + req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) + if err != nil { + return "", "", "", fmt.Errorf("failed to create legacy request: %w", err) } req.Header.Set("User-Agent", getRandomUserAgent()) resp, err := a.client.Do(req) if err != nil { - return "", "", fmt.Errorf("failed to call AfkarXYZ API: %w", err) + return "", "", "", fmt.Errorf("failed to call legacy AfkarXYZ API: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { - return "", "", fmt.Errorf("AfkarXYZ API returned status %d", resp.StatusCode) + return "", "", "", fmt.Errorf("legacy AfkarXYZ API returned status %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { - return "", "", fmt.Errorf("failed to read response: %w", err) + return "", "", "", fmt.Errorf("failed to read legacy response: %w", err) } var apiResp AfkarXYZResponse if err := json.Unmarshal(body, &apiResp); err != nil { - return "", "", fmt.Errorf("failed to decode response: %w", err) + return "", "", "", fmt.Errorf("failed to decode legacy response: %w", err) } - if !apiResp.Success || apiResp.Data.DirectLink == "" { - return "", "", fmt.Errorf("AfkarXYZ API failed or no download link found") + if !apiResp.Success || strings.TrimSpace(apiResp.Data.DirectLink) == "" { + return "", "", "", fmt.Errorf("legacy AfkarXYZ API failed or no download link found") } fileName := apiResp.Data.FileName @@ -134,19 +259,22 @@ func (a *AmazonDownloader) doAfkarXYZRequest(apiURL string) (string, string, err reg := regexp.MustCompile(`[<>:"/\\|?*]`) fileName = reg.ReplaceAllString(fileName, "") - return apiResp.Data.DirectLink, fileName, nil + return apiResp.Data.DirectLink, fileName, "", nil } -func (a *AmazonDownloader) downloadFromAfkarXYZ(amazonURL string) (string, string, error) { +func (a *AmazonDownloader) downloadFromAfkarXYZ(amazonURL string) (string, string, string, error) { GoLog("[Amazon] Fetching from AfkarXYZ API...\n") - downloadURL, fileName, err := a.fetchAmazonURLWithRetry(amazonURL) + downloadURL, fileName, decryptionKey, err := a.fetchAmazonURLWithRetry(amazonURL) if err != nil { - return "", "", err + return "", "", "", err } + if decryptionKey != "" { + GoLog("[Amazon] AfkarXYZ returned encrypted stream (decryption key available)\n") + } GoLog("[Amazon] AfkarXYZ returned: %s\n", fileName) - return downloadURL, fileName, nil + return downloadURL, fileName, decryptionKey, nil } func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath string, outputFD int, itemID string) error { @@ -233,17 +361,18 @@ func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath string, outputFD // AmazonDownloadResult contains download result with quality info type AmazonDownloadResult struct { - FilePath string - BitDepth int - SampleRate int - Title string - Artist string - Album string - ReleaseDate string - TrackNumber int - DiscNumber int - ISRC string - LyricsLRC string + FilePath string + BitDepth int + SampleRate int + Title string + Artist string + Album string + ReleaseDate string + TrackNumber int + DiscNumber int + ISRC string + LyricsLRC string + DecryptionKey string } func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { @@ -299,7 +428,7 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { } // Download using AfkarXYZ API - downloadURL, _, err := downloader.downloadFromAfkarXYZ(amazonURL) + downloadURL, afkarFileName, decryptionKey, err := downloader.downloadFromAfkarXYZ(amazonURL) if err != nil { return AmazonDownloadResult{}, fmt.Errorf("failed to get download URL from AfkarXYZ: %w", err) } @@ -321,7 +450,11 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { outputPath = fmt.Sprintf("/proc/self/fd/%d", req.OutputFD) } } else { - filename = sanitizeFilename(filename) + ".flac" + outputExt := strings.ToLower(filepath.Ext(afkarFileName)) + if outputExt == "" { + outputExt = ".flac" + } + filename = sanitizeFilename(filename) + outputExt outputPath = filepath.Join(req.OutputDir, filename) if fileInfo, statErr := os.Stat(outputPath); statErr == nil && fileInfo.Size() > 0 { return AmazonDownloadResult{FilePath: "EXISTS:" + outputPath}, nil @@ -352,6 +485,12 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { return AmazonDownloadResult{}, fmt.Errorf("download failed: %w", err) } + actualOutputPath := outputPath + needsDecryption := strings.TrimSpace(decryptionKey) != "" + if needsDecryption { + GoLog("[Amazon] Download requires decryption; deferring decrypt to Flutter FFmpeg path\n") + } + // Wait for parallel operations to complete <-parallelDone @@ -360,7 +499,6 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { SetItemFinalizing(req.ItemID) } - existingMeta, metaErr := ReadMetadata(outputPath) actualTrackNum := req.TrackNumber actualDiscNum := req.DiscNumber actualDate := req.ReleaseDate @@ -368,25 +506,28 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { actualTitle := req.TrackName actualArtist := req.ArtistName - if metaErr == nil && existingMeta != nil { - if existingMeta.TrackNumber > 0 && (req.TrackNumber == 0 || req.TrackNumber == 1) { - actualTrackNum = existingMeta.TrackNumber - GoLog("[Amazon] Using track number from file: %d (request had: %d)\n", actualTrackNum, req.TrackNumber) + if !needsDecryption { + existingMeta, metaErr := ReadMetadata(actualOutputPath) + if metaErr == nil && existingMeta != nil { + if existingMeta.TrackNumber > 0 && (req.TrackNumber == 0 || req.TrackNumber == 1) { + actualTrackNum = existingMeta.TrackNumber + GoLog("[Amazon] Using track number from file: %d (request had: %d)\n", actualTrackNum, req.TrackNumber) + } + if existingMeta.DiscNumber > 0 && (req.DiscNumber == 0 || req.DiscNumber == 1) { + actualDiscNum = existingMeta.DiscNumber + GoLog("[Amazon] Using disc number from file: %d (request had: %d)\n", actualDiscNum, req.DiscNumber) + } + if existingMeta.Date != "" && req.ReleaseDate == "" { + actualDate = existingMeta.Date + GoLog("[Amazon] Using release date from file: %s\n", actualDate) + } + if existingMeta.Album != "" && req.AlbumName == "" { + actualAlbum = existingMeta.Album + GoLog("[Amazon] Using album from file: %s\n", actualAlbum) + } + GoLog("[Amazon] Existing metadata - Title: %s, Artist: %s, Album: %s, Date: %s\n", + existingMeta.Title, existingMeta.Artist, existingMeta.Album, existingMeta.Date) } - if existingMeta.DiscNumber > 0 && (req.DiscNumber == 0 || req.DiscNumber == 1) { - actualDiscNum = existingMeta.DiscNumber - GoLog("[Amazon] Using disc number from file: %d (request had: %d)\n", actualDiscNum, req.DiscNumber) - } - if existingMeta.Date != "" && req.ReleaseDate == "" { - actualDate = existingMeta.Date - GoLog("[Amazon] Using release date from file: %s\n", actualDate) - } - if existingMeta.Album != "" && req.AlbumName == "" { - actualAlbum = existingMeta.Album - GoLog("[Amazon] Using album from file: %s\n", actualAlbum) - } - GoLog("[Amazon] Existing metadata - Title: %s, Artist: %s, Album: %s, Date: %s\n", - existingMeta.Title, existingMeta.Artist, existingMeta.Album, existingMeta.Date) } metadata := Metadata{ @@ -409,7 +550,7 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { coverData = parallelResult.CoverData GoLog("[Amazon] Using parallel-fetched cover (%d bytes)\n", len(coverData)) } else { - existingCover, coverErr := ExtractCoverArt(outputPath) + existingCover, coverErr := ExtractCoverArt(actualOutputPath) if coverErr == nil && len(existingCover) > 0 { coverData = existingCover GoLog("[Amazon] Using existing cover from Amazon file (%d bytes)\n", len(coverData)) @@ -418,11 +559,16 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { } } - if isSafOutput { + if isSafOutput || needsDecryption { GoLog("[Amazon] SAF output detected - skipping in-backend metadata/lyrics embedding (handled in Flutter)\n") } else { - if err := EmbedMetadataWithCoverData(outputPath, metadata, coverData); err != nil { - GoLog("[Amazon] Warning: failed to embed metadata: %v\n", err) + isFlacOutput := strings.HasSuffix(strings.ToLower(actualOutputPath), ".flac") + if isFlacOutput { + if err := EmbedMetadataWithCoverData(actualOutputPath, metadata, coverData); err != nil { + GoLog("[Amazon] Warning: failed to embed metadata: %v\n", err) + } + } else { + GoLog("[Amazon] Non-FLAC output detected (%s), skipping native metadata embedding\n", filepath.Ext(actualOutputPath)) } if req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { @@ -433,20 +579,22 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { if lyricsMode == "external" || lyricsMode == "both" { GoLog("[Amazon] Saving external LRC file...\n") - if lrcPath, lrcErr := SaveLRCFile(outputPath, parallelResult.LyricsLRC); lrcErr != nil { + if lrcPath, lrcErr := SaveLRCFile(actualOutputPath, parallelResult.LyricsLRC); lrcErr != nil { GoLog("[Amazon] Warning: failed to save LRC file: %v\n", lrcErr) } else { GoLog("[Amazon] LRC file saved: %s\n", lrcPath) } } - if lyricsMode == "embed" || lyricsMode == "both" { + if (lyricsMode == "embed" || lyricsMode == "both") && isFlacOutput { GoLog("[Amazon] Embedding parallel-fetched lyrics (%d lines)...\n", len(parallelResult.LyricsData.Lines)) - if embedErr := EmbedLyrics(outputPath, parallelResult.LyricsLRC); embedErr != nil { + if embedErr := EmbedLyrics(actualOutputPath, parallelResult.LyricsLRC); embedErr != nil { GoLog("[Amazon] Warning: failed to embed lyrics: %v\n", embedErr) } else { GoLog("[Amazon] Lyrics embedded successfully\n") } + } else if (lyricsMode == "embed" || lyricsMode == "both") && !isFlacOutput { + GoLog("[Amazon] Skipping embedded lyrics for non-FLAC output\n") } } else if req.EmbedLyrics { GoLog("[Amazon] No lyrics available from parallel fetch\n") @@ -456,17 +604,17 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { GoLog("[Amazon] Downloaded successfully from Amazon Music\n") quality := AudioQuality{} - if isSafOutput { + if isSafOutput || needsDecryption { GoLog("[Amazon] SAF output detected - skipping post-write file inspection in backend\n") } else { - quality, err = GetAudioQuality(outputPath) + quality, err = GetAudioQuality(actualOutputPath) if err != nil { GoLog("[Amazon] Warning: couldn't read quality from file: %v\n", err) } else { GoLog("[Amazon] Actual quality: %d-bit/%dHz\n", quality.BitDepth, quality.SampleRate) } - finalMeta, metaReadErr := ReadMetadata(outputPath) + finalMeta, metaReadErr := ReadMetadata(actualOutputPath) if metaReadErr == nil && finalMeta != nil { GoLog("[Amazon] Final metadata from file - Track: %d, Disc: %d, Date: %s\n", finalMeta.TrackNumber, finalMeta.DiscNumber, finalMeta.Date) @@ -478,9 +626,10 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { } } - // Add to ISRC index for fast duplicate checking - if !isSafOutput { - AddToISRCIndex(req.OutputDir, req.ISRC, outputPath) + // Add to ISRC index for fast duplicate checking. + // When decryption is pending in Flutter, postpone indexing until final file is settled. + if !isSafOutput && !needsDecryption { + AddToISRCIndex(req.OutputDir, req.ISRC, actualOutputPath) } bitDepth := 0 @@ -496,16 +645,17 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { } return AmazonDownloadResult{ - FilePath: outputPath, - BitDepth: bitDepth, - SampleRate: sampleRate, - Title: req.TrackName, - Artist: req.ArtistName, - Album: req.AlbumName, - ReleaseDate: req.ReleaseDate, - TrackNumber: actualTrackNum, - DiscNumber: actualDiscNum, - ISRC: req.ISRC, - LyricsLRC: lyricsLRC, + FilePath: outputPath, + BitDepth: bitDepth, + SampleRate: sampleRate, + Title: req.TrackName, + Artist: req.ArtistName, + Album: req.AlbumName, + ReleaseDate: req.ReleaseDate, + TrackNumber: actualTrackNum, + DiscNumber: actualDiscNum, + ISRC: req.ISRC, + LyricsLRC: lyricsLRC, + DecryptionKey: decryptionKey, }, nil } diff --git a/go_backend/amazon_asin_test.go b/go_backend/amazon_asin_test.go new file mode 100644 index 00000000..705e9c01 --- /dev/null +++ b/go_backend/amazon_asin_test.go @@ -0,0 +1,46 @@ +package gobackend + +import "testing" + +func TestExtractAmazonASIN(t *testing.T) { + tests := []struct { + name string + url string + want string + }{ + { + name: "prefers trackAsin over albumAsin", + url: "https://music.amazon.com/albums/B0ALBUM123?trackAsin=B0TRACK456&musicTerritory=US", + want: "B0TRACK456", + }, + { + name: "extract from tracks path", + url: "https://music.amazon.com/tracks/B0CYQHGWZJ?musicTerritory=US", + want: "B0CYQHGWZJ", + }, + { + name: "extract from plain query asin", + url: "https://example.com/?asin=B0CYQHGWZJ", + want: "B0CYQHGWZJ", + }, + { + name: "fallback regex", + url: "https://example.com/path/B0CYQHGWZJ", + want: "B0CYQHGWZJ", + }, + { + name: "invalid url", + url: "https://music.amazon.com/tracks/not-valid", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractAmazonASIN(tt.url) + if got != tt.want { + t.Fatalf("extractAmazonASIN() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/go_backend/audio_metadata.go b/go_backend/audio_metadata.go index 73279971..a681c88a 100644 --- a/go_backend/audio_metadata.go +++ b/go_backend/audio_metadata.go @@ -23,6 +23,7 @@ type AudioMetadata struct { TrackNumber int DiscNumber int ISRC string + Lyrics string Label string Copyright string Composer string @@ -181,6 +182,15 @@ func parseID3v22Frames(data []byte, metadata *AudioMetadata, tagUnsync bool) { metadata.Label = value case "TCR": metadata.Copyright = value + case "ULT": + if v := extractLyricsFrame(frameData); v != "" && metadata.Lyrics == "" { + metadata.Lyrics = v + } + case "TXX": + desc, userValue := extractUserTextFrame(frameData) + if isLyricsDescription(desc) && userValue != "" && metadata.Lyrics == "" { + metadata.Lyrics = userValue + } } pos += 6 + frameSize @@ -297,6 +307,15 @@ func parseID3v23Frames(data []byte, metadata *AudioMetadata, version byte, tagUn if v := extractCommentFrame(frameData); v != "" { metadata.Comment = v } + case "USLT": + if v := extractLyricsFrame(frameData); v != "" && metadata.Lyrics == "" { + metadata.Lyrics = v + } + case "TXXX": + desc, userValue := extractUserTextFrame(frameData) + if isLyricsDescription(desc) && userValue != "" && metadata.Lyrics == "" { + metadata.Lyrics = userValue + } } pos += 10 + frameSize @@ -399,6 +418,98 @@ func extractCommentFrame(data []byte) string { return extractTextFrame(framed) } +// extractLyricsFrame parses ID3 unsynchronized lyrics frames (USLT/ULT). +// Format: encoding(1) + language(3) + description(null-terminated) + lyrics text. +func extractLyricsFrame(data []byte) string { + if len(data) < 5 { + return "" + } + + encoding := data[0] + rest := data[4:] // skip 3-byte language code + + var text []byte + switch encoding { + case 1, 2: // UTF-16 variants use double-null terminator + for i := 0; i+1 < len(rest); i += 2 { + if rest[i] == 0 && rest[i+1] == 0 { + text = rest[i+2:] + break + } + } + default: // ISO-8859-1 or UTF-8 + idx := bytes.IndexByte(rest, 0) + if idx >= 0 && idx+1 < len(rest) { + text = rest[idx+1:] + } else { + text = rest + } + } + + if len(text) == 0 { + return "" + } + + framed := make([]byte, 1+len(text)) + framed[0] = encoding + copy(framed[1:], text) + return extractTextFrame(framed) +} + +// extractUserTextFrame parses ID3 TXXX/TXX user text frame: +// encoding(1) + description + separator + value. +func extractUserTextFrame(data []byte) (string, string) { + if len(data) < 2 { + return "", "" + } + + encoding := data[0] + payload := data[1:] + + var descRaw, valueRaw []byte + switch encoding { + case 1, 2: // UTF-16 variants + for i := 0; i+1 < len(payload); i += 2 { + if payload[i] == 0 && payload[i+1] == 0 { + descRaw = payload[:i] + valueRaw = payload[i+2:] + break + } + } + default: // ISO-8859-1 or UTF-8 + idx := bytes.IndexByte(payload, 0) + if idx >= 0 { + descRaw = payload[:idx] + if idx+1 <= len(payload) { + valueRaw = payload[idx+1:] + } + } + } + + if len(valueRaw) == 0 { + return "", "" + } + + descFramed := make([]byte, 1+len(descRaw)) + descFramed[0] = encoding + copy(descFramed[1:], descRaw) + + valueFramed := make([]byte, 1+len(valueRaw)) + valueFramed[0] = encoding + copy(valueFramed[1:], valueRaw) + + return strings.TrimSpace(extractTextFrame(descFramed)), strings.TrimSpace(extractTextFrame(valueFramed)) +} + +func isLyricsDescription(description string) bool { + switch strings.ToLower(strings.TrimSpace(description)) { + case "lyrics", "lyric", "unsyncedlyrics", "unsynced lyrics", "lrc": + return true + default: + return false + } +} + func decodeUTF16(data []byte) string { if len(data) < 2 { return "" @@ -843,6 +954,10 @@ func parseVorbisComments(data []byte, metadata *AudioMetadata) { metadata.Composer = value case "COMMENT", "DESCRIPTION": metadata.Comment = value + case "LYRICS", "UNSYNCEDLYRICS": + if metadata.Lyrics == "" { + metadata.Lyrics = value + } case "ORGANIZATION", "LABEL", "PUBLISHER": metadata.Label = value case "COPYRIGHT": diff --git a/go_backend/exports.go b/go_backend/exports.go index 1dacbac2..b09ce74c 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -47,10 +47,30 @@ func GetSpotifyMetadata(spotifyURL string) (string, error) { client, err := NewSpotifyMetadataClient() if err != nil { + if shouldTrySpotFetchFallback(err) { + data, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL) + if apiErr == nil { + jsonBytes, marshalErr := json.Marshal(data) + if marshalErr != nil { + return "", marshalErr + } + return string(jsonBytes), nil + } + } return "", err } data, err := client.GetFilteredData(ctx, spotifyURL, false, 0) if err != nil { + if shouldTrySpotFetchFallback(err) { + fallbackData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL) + if apiErr == nil { + jsonBytes, marshalErr := json.Marshal(fallbackData) + if marshalErr != nil { + return "", marshalErr + } + return string(jsonBytes), nil + } + } return "", err } @@ -178,20 +198,22 @@ type DownloadResponse struct { Copyright string `json:"copyright,omitempty"` SkipMetadataEnrichment bool `json:"skip_metadata_enrichment,omitempty"` LyricsLRC string `json:"lyrics_lrc,omitempty"` + DecryptionKey string `json:"decryption_key,omitempty"` } type DownloadResult struct { - FilePath string - BitDepth int - SampleRate int - Title string - Artist string - Album string - ReleaseDate string - TrackNumber int - DiscNumber int - ISRC string - LyricsLRC string + FilePath string + BitDepth int + SampleRate int + Title string + Artist string + Album string + ReleaseDate string + TrackNumber int + DiscNumber int + ISRC string + LyricsLRC string + DecryptionKey string } func buildDownloadSuccessResponse( @@ -258,6 +280,7 @@ func buildDownloadSuccessResponse( Label: req.Label, Copyright: req.Copyright, LyricsLRC: result.LyricsLRC, + DecryptionKey: result.DecryptionKey, } } @@ -323,17 +346,18 @@ func DownloadTrack(requestJSON string) (string, error) { amazonResult, amazonErr := downloadFromAmazon(req) if amazonErr == nil { result = DownloadResult{ - FilePath: amazonResult.FilePath, - BitDepth: amazonResult.BitDepth, - SampleRate: amazonResult.SampleRate, - Title: amazonResult.Title, - Artist: amazonResult.Artist, - Album: amazonResult.Album, - ReleaseDate: amazonResult.ReleaseDate, - TrackNumber: amazonResult.TrackNumber, - DiscNumber: amazonResult.DiscNumber, - ISRC: amazonResult.ISRC, - LyricsLRC: amazonResult.LyricsLRC, + FilePath: amazonResult.FilePath, + BitDepth: amazonResult.BitDepth, + SampleRate: amazonResult.SampleRate, + Title: amazonResult.Title, + Artist: amazonResult.Artist, + Album: amazonResult.Album, + ReleaseDate: amazonResult.ReleaseDate, + TrackNumber: amazonResult.TrackNumber, + DiscNumber: amazonResult.DiscNumber, + ISRC: amazonResult.ISRC, + LyricsLRC: amazonResult.LyricsLRC, + DecryptionKey: amazonResult.DecryptionKey, } } err = amazonErr @@ -534,17 +558,18 @@ func DownloadWithFallback(requestJSON string) (string, error) { amazonResult, amazonErr := downloadFromAmazon(req) if amazonErr == nil { result = DownloadResult{ - FilePath: amazonResult.FilePath, - BitDepth: amazonResult.BitDepth, - SampleRate: amazonResult.SampleRate, - Title: amazonResult.Title, - Artist: amazonResult.Artist, - Album: amazonResult.Album, - ReleaseDate: amazonResult.ReleaseDate, - TrackNumber: amazonResult.TrackNumber, - DiscNumber: amazonResult.DiscNumber, - ISRC: amazonResult.ISRC, - LyricsLRC: amazonResult.LyricsLRC, + FilePath: amazonResult.FilePath, + BitDepth: amazonResult.BitDepth, + SampleRate: amazonResult.SampleRate, + Title: amazonResult.Title, + Artist: amazonResult.Artist, + Album: amazonResult.Album, + ReleaseDate: amazonResult.ReleaseDate, + TrackNumber: amazonResult.TrackNumber, + DiscNumber: amazonResult.DiscNumber, + ISRC: amazonResult.ISRC, + LyricsLRC: amazonResult.LyricsLRC, + DecryptionKey: amazonResult.DecryptionKey, } } else if !errors.Is(amazonErr, ErrDownloadCancelled) { GoLog("[DownloadWithFallback] Amazon error: %v\n", amazonErr) @@ -699,6 +724,7 @@ func ReadFileMetadata(filePath string) (string, error) { result["track_number"] = meta.TrackNumber result["disc_number"] = meta.DiscNumber result["isrc"] = meta.ISRC + result["lyrics"] = meta.Lyrics result["genre"] = meta.Genre result["composer"] = meta.Composer result["comment"] = meta.Comment @@ -723,6 +749,7 @@ func ReadFileMetadata(filePath string) (string, error) { result["track_number"] = meta.TrackNumber result["disc_number"] = meta.DiscNumber result["isrc"] = meta.ISRC + result["lyrics"] = meta.Lyrics result["genre"] = meta.Genre result["composer"] = meta.Composer result["comment"] = meta.Comment @@ -1178,9 +1205,12 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + var spotifyErr error + client, err := NewSpotifyMetadataClient() if err != nil { LogWarn("Spotify", "Credentials not configured, falling back to Deezer") + spotifyErr = err } else { data, err := client.GetFilteredData(ctx, spotifyURL, false, 0) if err == nil { @@ -1191,28 +1221,81 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { return string(jsonBytes), nil } - errStr := strings.ToLower(err.Error()) - if !strings.Contains(errStr, "429") && !strings.Contains(errStr, "rate") && !strings.Contains(errStr, "limit") { + spotifyErr = err + if !shouldTrySpotFetchFallback(err) { return "", err } } + spotFetchData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL) + if apiErr == nil { + GoLog("[Fallback] Spotify metadata fetched via SpotFetch API\n") + jsonBytes, err := json.Marshal(spotFetchData) + if err != nil { + return "", err + } + return string(jsonBytes), nil + } + GoLog("[Fallback] SpotFetch API fallback failed: %v\n", apiErr) + parsed, parseErr := parseSpotifyURI(spotifyURL) if parseErr != nil { - return "", fmt.Errorf("spotify rate limited and failed to parse URL: %w", parseErr) + if spotifyErr != nil { + return "", fmt.Errorf("spotify failed (%v), SpotFetch fallback failed (%v), and URL parsing failed: %w", spotifyErr, apiErr, parseErr) + } + return "", fmt.Errorf("SpotFetch fallback failed (%v) and URL parsing failed: %w", apiErr, parseErr) } - GoLog("[Fallback] Spotify rate limited for %s, trying Deezer...\n", parsed.Type) + GoLog("[Fallback] Trying Deezer conversion fallback for %s...\n", parsed.Type) if parsed.Type == "track" || parsed.Type == "album" { return ConvertSpotifyToDeezer(parsed.Type, parsed.ID) } if parsed.Type == "artist" { - return "", fmt.Errorf("spotify rate limited. Artist pages require Spotify API - please try again later") + if spotifyErr != nil { + return "", fmt.Errorf("spotify metadata unavailable (%v) and SpotFetch fallback failed (%v). Artist pages require Spotify/SpotFetch API", spotifyErr, apiErr) + } + return "", fmt.Errorf("SpotFetch fallback failed (%v). Artist pages require Spotify/SpotFetch API", apiErr) } - return "", fmt.Errorf("spotify rate limited. Playlists are user-specific and require Spotify API") + if spotifyErr != nil { + return "", fmt.Errorf("spotify metadata unavailable (%v), SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", spotifyErr, apiErr) + } + return "", fmt.Errorf("SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", apiErr) +} + +func shouldTrySpotFetchFallback(err error) bool { + if err == nil { + return false + } + if errors.Is(err, ErrNoSpotifyCredentials) { + return true + } + + errStr := strings.ToLower(err.Error()) + indicators := []string{ + "429", + "rate", + "limit", + "403", + "forbidden", + "401", + "unauthorized", + "timeout", + "connection", + "spotify error", + "access token", + "client token", + "eof", + } + + for _, indicator := range indicators { + if strings.Contains(errStr, indicator) { + return true + } + } + return false } func CheckAvailabilityFromDeezerID(deezerTrackID string) (string, error) { diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 93f3a825..1fa0e3f0 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -1082,16 +1082,18 @@ func tryBuiltInProvider(providerID string, req DownloadRequest) (*DownloadRespon amazonResult, amazonErr := downloadFromAmazon(req) if amazonErr == nil { result = DownloadResult{ - FilePath: amazonResult.FilePath, - BitDepth: amazonResult.BitDepth, - SampleRate: amazonResult.SampleRate, - Title: amazonResult.Title, - Artist: amazonResult.Artist, - Album: amazonResult.Album, - ReleaseDate: amazonResult.ReleaseDate, - TrackNumber: amazonResult.TrackNumber, - DiscNumber: amazonResult.DiscNumber, - ISRC: amazonResult.ISRC, + FilePath: amazonResult.FilePath, + BitDepth: amazonResult.BitDepth, + SampleRate: amazonResult.SampleRate, + Title: amazonResult.Title, + Artist: amazonResult.Artist, + Album: amazonResult.Album, + ReleaseDate: amazonResult.ReleaseDate, + TrackNumber: amazonResult.TrackNumber, + DiscNumber: amazonResult.DiscNumber, + ISRC: amazonResult.ISRC, + LyricsLRC: amazonResult.LyricsLRC, + DecryptionKey: amazonResult.DecryptionKey, } } err = amazonErr @@ -1119,6 +1121,8 @@ func tryBuiltInProvider(providerID string, req DownloadRequest) (*DownloadRespon Genre: req.Genre, Label: req.Label, Copyright: req.Copyright, + LyricsLRC: result.LyricsLRC, + DecryptionKey: result.DecryptionKey, }, nil } @@ -1164,16 +1168,30 @@ func (p *ExtensionProviderWrapper) CustomSearch(query string, options map[string p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() - optionsJSON, _ := json.Marshal(options) + if options == nil { + options = map[string]interface{}{} + } - script := fmt.Sprintf(` + // Avoid embedding user input directly into JS source. Some inputs can trigger + // parser/runtime edge cases on specific devices/Goja builds. + const queryVar = "__sf_custom_search_query" + const optionsVar = "__sf_custom_search_options" + global := p.vm.GlobalObject() + _ = global.Set(queryVar, query) + _ = global.Set(optionsVar, options) + defer func() { + global.Delete(queryVar) + global.Delete(optionsVar) + }() + + const script = ` (function() { if (typeof extension !== 'undefined' && typeof extension.customSearch === 'function') { - return extension.customSearch(%q, %s); + return extension.customSearch(__sf_custom_search_query, __sf_custom_search_options); } return null; })() - `, query, string(optionsJSON)) + ` result, err := RunWithTimeoutAndRecover(p.vm, script, DefaultJSTimeout) if err != nil { @@ -1358,12 +1376,12 @@ type PostProcessResult struct { } type PostProcessInput struct { - Path string `json:"path,omitempty"` - URI string `json:"uri,omitempty"` - Name string `json:"name,omitempty"` + Path string `json:"path,omitempty"` + URI string `json:"uri,omitempty"` + Name string `json:"name,omitempty"` MimeType string `json:"mime_type,omitempty"` - Size int64 `json:"size,omitempty"` - IsSAF bool `json:"is_saf,omitempty"` + Size int64 `json:"size,omitempty"` + IsSAF bool `json:"is_saf,omitempty"` } const PostProcessTimeout = 2 * time.Minute diff --git a/go_backend/extension_timeout.go b/go_backend/extension_timeout.go index 79fbdaa6..e9a5605f 100644 --- a/go_backend/extension_timeout.go +++ b/go_backend/extension_timeout.go @@ -4,6 +4,7 @@ package gobackend import ( "context" "fmt" + "runtime/debug" "sync" "time" @@ -49,6 +50,7 @@ func RunWithTimeout(vm *goja.Runtime, script string, timeout time.Duration) (goj IsTimeout: true, }} } else { + GoLog("[ExtensionRuntime] panic during JS execution: %v\n%s\n", r, string(debug.Stack())) resultCh <- result{nil, fmt.Errorf("panic during execution: %v", r)} } } diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 7f8c9723..370218e4 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -475,33 +475,91 @@ func EmbedGenreLabel(filePath string, genre, label string) error { } func ExtractLyrics(filePath string) (string, error) { + lower := strings.ToLower(filePath) + + if strings.HasSuffix(lower, ".flac") { + return extractLyricsFromFlac(filePath) + } + + if strings.HasSuffix(lower, ".mp3") { + meta, err := ReadID3Tags(filePath) + if err != nil || meta == nil { + return "", fmt.Errorf("no lyrics found in file") + } + if strings.TrimSpace(meta.Lyrics) != "" { + return meta.Lyrics, nil + } + if looksLikeEmbeddedLyrics(meta.Comment) { + return meta.Comment, nil + } + return "", fmt.Errorf("no lyrics found in file") + } + + if strings.HasSuffix(lower, ".opus") || strings.HasSuffix(lower, ".ogg") { + meta, err := ReadOggVorbisComments(filePath) + if err != nil || meta == nil { + return "", fmt.Errorf("no lyrics found in file") + } + if strings.TrimSpace(meta.Lyrics) != "" { + return meta.Lyrics, nil + } + if looksLikeEmbeddedLyrics(meta.Comment) { + return meta.Comment, nil + } + return "", fmt.Errorf("no lyrics found in file") + } + + return "", fmt.Errorf("unsupported file format for lyrics extraction") +} + +func extractLyricsFromFlac(filePath string) (string, error) { f, err := flac.ParseFile(filePath) if err != nil { return "", fmt.Errorf("failed to parse FLAC file: %w", err) } for _, meta := range f.Meta { - if meta.Type == flac.VorbisComment { - cmt, err := flacvorbis.ParseFromMetaDataBlock(*meta) - if err != nil { - continue - } + if meta.Type != flac.VorbisComment { + continue + } - lyrics, err := cmt.Get("LYRICS") - if err == nil && len(lyrics) > 0 && lyrics[0] != "" { - return lyrics[0], nil - } + cmt, err := flacvorbis.ParseFromMetaDataBlock(*meta) + if err != nil { + continue + } - lyrics, err = cmt.Get("UNSYNCEDLYRICS") - if err == nil && len(lyrics) > 0 && lyrics[0] != "" { - return lyrics[0], nil - } + lyrics, err := cmt.Get("LYRICS") + if err == nil && len(lyrics) > 0 && strings.TrimSpace(lyrics[0]) != "" { + return lyrics[0], nil + } + + lyrics, err = cmt.Get("UNSYNCEDLYRICS") + if err == nil && len(lyrics) > 0 && strings.TrimSpace(lyrics[0]) != "" { + return lyrics[0], nil } } return "", fmt.Errorf("no lyrics found in file") } +func looksLikeEmbeddedLyrics(value string) bool { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return false + } + + lower := strings.ToLower(trimmed) + if strings.Contains(lower, "[ar:") || strings.Contains(lower, "[ti:") { + return true + } + + if strings.Contains(trimmed, "\n") && strings.Contains(trimmed, "[") && strings.Contains(trimmed, "]") { + return true + } + + return false +} + type AudioQuality struct { BitDepth int `json:"bit_depth"` SampleRate int `json:"sample_rate"` diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index a74a1ea1..5336394c 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -419,7 +419,7 @@ func extractQobuzDownloadURLFromBody(body []byte) (string, error) { func (q *QobuzDownloader) downloadFromJumo(trackID int64, quality string) (string, error) { formatID := mapJumoQuality(quality) region := "US" - jumoURL := fmt.Sprintf("https://jumo-dl.pages.dev/file?track_id=%d&format_id=%d®ion=%s", trackID, formatID, region) + jumoURL := fmt.Sprintf("https://jumo-dl.pages.dev/get?track_id=%d&format_id=%d®ion=%s", trackID, formatID, region) GoLog("[Qobuz] Trying Jumo API fallback...\n") @@ -428,6 +428,8 @@ func (q *QobuzDownloader) downloadFromJumo(trackID int64, quality string) (strin if err != nil { return "", err } + req.Header.Set("User-Agent", getRandomUserAgent()) + req.Header.Set("Referer", "https://jumo-dl.pages.dev/") resp, err := client.Do(req) if err != nil { diff --git a/go_backend/spotfetch_api.go b/go_backend/spotfetch_api.go new file mode 100644 index 00000000..d6bfab19 --- /dev/null +++ b/go_backend/spotfetch_api.go @@ -0,0 +1,80 @@ +package gobackend + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +const DefaultSpotFetchAPIBaseURL = "https://spotify.afkarxyz.fun/api" + +// GetSpotifyDataWithAPI fetches Spotify metadata through SpotFetch-compatible API. +// This is used as a fallback when direct Spotify API access is blocked/limited. +func GetSpotifyDataWithAPI(ctx context.Context, spotifyURL, apiBaseURL string) (interface{}, error) { + parsed, err := parseSpotifyURI(spotifyURL) + if err != nil { + return nil, fmt.Errorf("invalid Spotify URL: %w", err) + } + + base := strings.TrimSpace(apiBaseURL) + if base == "" { + base = DefaultSpotFetchAPIBaseURL + } + + endpoint := fmt.Sprintf("%s/%s/%s", strings.TrimSuffix(base, "/"), parsed.Type, parsed.ID) + req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) + if err != nil { + return nil, fmt.Errorf("failed to create SpotFetch API request: %w", err) + } + req.Header.Set("User-Agent", getRandomUserAgent()) + req.Header.Set("Accept", "application/json") + + client := NewHTTPClientWithTimeout(30 * time.Second) + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("SpotFetch API request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("SpotFetch API error: HTTP %d", resp.StatusCode) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read SpotFetch API response: %w", err) + } + + switch parsed.Type { + case "track": + var trackResp TrackResponse + if err := json.Unmarshal(bodyBytes, &trackResp); err != nil { + return nil, fmt.Errorf("failed to decode track response: %w", err) + } + return trackResp, nil + case "album": + var albumResp AlbumResponsePayload + if err := json.Unmarshal(bodyBytes, &albumResp); err != nil { + return nil, fmt.Errorf("failed to decode album response: %w", err) + } + return &albumResp, nil + case "playlist": + var playlistResp PlaylistResponsePayload + if err := json.Unmarshal(bodyBytes, &playlistResp); err != nil { + return nil, fmt.Errorf("failed to decode playlist response: %w", err) + } + return playlistResp, nil + case "artist": + var artistResp ArtistResponsePayload + if err := json.Unmarshal(bodyBytes, &artistResp); err != nil { + return nil, fmt.Errorf("failed to decode artist response: %w", err) + } + return &artistResp, nil + default: + return nil, fmt.Errorf("unsupported Spotify type: %s", parsed.Type) + } +} diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 4ebab8ef..4fa3ab82 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -1,8 +1,8 @@ /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.6.1'; - static const String buildNumber = '78'; + static const String version = '3.6.5'; + static const String buildNumber = '79'; static const String fullVersion = '$version+$buildNumber'; diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d7fd0338..f05056d5 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5139,6 +5139,70 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Failed: {error}'** String trackSaveFailed(String error); + + /// Menu item - convert audio format + /// + /// In en, this message translates to: + /// **'Convert Format'** + String get trackConvertFormat; + + /// Subtitle for convert format menu item + /// + /// In en, this message translates to: + /// **'Convert to MP3 or Opus'** + String get trackConvertFormatSubtitle; + + /// Title of convert bottom sheet + /// + /// In en, this message translates to: + /// **'Convert Audio'** + String get trackConvertTitle; + + /// Label for format selection + /// + /// In en, this message translates to: + /// **'Target Format'** + String get trackConvertTargetFormat; + + /// Label for bitrate selection + /// + /// In en, this message translates to: + /// **'Bitrate'** + String get trackConvertBitrate; + + /// Confirmation dialog title + /// + /// In en, this message translates to: + /// **'Confirm Conversion'** + String get trackConvertConfirmTitle; + + /// Confirmation dialog message + /// + /// In en, this message translates to: + /// **'Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.'** + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ); + + /// Snackbar while converting + /// + /// In en, this message translates to: + /// **'Converting audio...'** + String get trackConvertConverting; + + /// Snackbar after successful conversion + /// + /// In en, this message translates to: + /// **'Converted to {format} successfully'** + String trackConvertSuccess(String format); + + /// Snackbar when conversion fails + /// + /// In en, this message translates to: + /// **'Conversion failed'** + String get trackConvertFailed; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 052b9474..61c28595 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2908,4 +2908,42 @@ class AppLocalizationsDe extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0bcd87bf..bafc4f3f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2894,4 +2894,42 @@ class AppLocalizationsEn extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 574edcf6..e2c130b7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2894,6 +2894,44 @@ class AppLocalizationsEs extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } /// The translations for Spanish Castilian, as used in Spain (`es_ES`). diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 61057994..6c658887 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2894,4 +2894,42 @@ class AppLocalizationsFr extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index a10d26cb..6a3ce249 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2894,4 +2894,42 @@ class AppLocalizationsHi extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 0a011ec9..52cf8925 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2913,4 +2913,42 @@ class AppLocalizationsId extends AppLocalizations { String trackSaveFailed(String error) { return 'Gagal: $error'; } + + @override + String get trackConvertFormat => 'Konversi Format'; + + @override + String get trackConvertFormatSubtitle => 'Konversi ke MP3 atau Opus'; + + @override + String get trackConvertTitle => 'Konversi Audio'; + + @override + String get trackConvertTargetFormat => 'Format Tujuan'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Konfirmasi Konversi'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Konversi dari $sourceFormat ke $targetFormat pada $bitrate?\n\nFile asli akan dihapus setelah konversi.'; + } + + @override + String get trackConvertConverting => 'Mengkonversi audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Berhasil dikonversi ke $format'; + } + + @override + String get trackConvertFailed => 'Konversi gagal'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 973227d6..3e9a1b3d 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2880,4 +2880,42 @@ class AppLocalizationsJa extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index dceb933f..a74e36d4 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2894,4 +2894,42 @@ class AppLocalizationsKo extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 5a4eca35..52c05aa9 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2894,4 +2894,42 @@ class AppLocalizationsNl extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 4e8748a5..cea6c5fe 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2894,6 +2894,44 @@ class AppLocalizationsPt extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } /// The translations for Portuguese, as used in Portugal (`pt_PT`). diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 9ffb5d94..79438058 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2940,4 +2940,42 @@ class AppLocalizationsRu extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 7a6e5c3a..004e1119 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2909,4 +2909,42 @@ class AppLocalizationsTr extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 2e87063e..eb41c0d9 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2894,6 +2894,44 @@ class AppLocalizationsZh extends AppLocalizations { String trackSaveFailed(String error) { return 'Failed: $error'; } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index ca845fcf..03e26018 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2184,5 +2184,38 @@ "placeholders": { "error": {"type": "String"} } - } + }, + + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": {"description": "Menu item - convert audio format"}, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": {"description": "Subtitle for convert format menu item"}, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": {"description": "Title of convert bottom sheet"}, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": {"description": "Label for format selection"}, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": {"description": "Label for bitrate selection"}, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": {"description": "Confirmation dialog title"}, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": {"type": "String"}, + "targetFormat": {"type": "String"}, + "bitrate": {"type": "String"} + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": {"description": "Snackbar while converting"}, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": {"type": "String"} + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": {"description": "Snackbar when conversion fails"} } diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 2c6e8a2b..43b98f13 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -587,7 +587,7 @@ "aboutSupport": "Dukungan", "@aboutSupport": { "description": "Section for support/donation links" - }, + }, "aboutApp": "Aplikasi", "@aboutApp": { "description": "Section for app info" @@ -3206,5 +3206,38 @@ "placeholders": { "error": {"type": "String"} } - } + }, + + "trackConvertFormat": "Konversi Format", + "@trackConvertFormat": {"description": "Menu item - convert audio format"}, + "trackConvertFormatSubtitle": "Konversi ke MP3 atau Opus", + "@trackConvertFormatSubtitle": {"description": "Subtitle for convert format menu item"}, + "trackConvertTitle": "Konversi Audio", + "@trackConvertTitle": {"description": "Title of convert bottom sheet"}, + "trackConvertTargetFormat": "Format Tujuan", + "@trackConvertTargetFormat": {"description": "Label for format selection"}, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": {"description": "Label for bitrate selection"}, + "trackConvertConfirmTitle": "Konfirmasi Konversi", + "@trackConvertConfirmTitle": {"description": "Confirmation dialog title"}, + "trackConvertConfirmMessage": "Konversi dari {sourceFormat} ke {targetFormat} pada {bitrate}?\n\nFile asli akan dihapus setelah konversi.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": {"type": "String"}, + "targetFormat": {"type": "String"}, + "bitrate": {"type": "String"} + } + }, + "trackConvertConverting": "Mengkonversi audio...", + "@trackConvertConverting": {"description": "Snackbar while converting"}, + "trackConvertSuccess": "Berhasil dikonversi ke {format}", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": {"type": "String"} + } + }, + "trackConvertFailed": "Konversi gagal", + "@trackConvertFailed": {"description": "Snackbar when conversion fails"} } diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index b65f77ac..5d5de696 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1237,6 +1237,11 @@ class DownloadQueueNotifier extends Notifier { } return '.opus'; } + // Amazon stream is delivered as MP4/M4A container (may contain FLAC audio), + // so SAF should keep .m4a before decrypt/convert pipeline. + if (service.toLowerCase() == 'amazon') { + return '.m4a'; + } if (service.toLowerCase() == 'tidal' && quality == 'HIGH') { return '.m4a'; } @@ -2897,6 +2902,123 @@ class DownloadQueueNotifier extends Notifier { final actualService = ((result['service'] as String?)?.toLowerCase()) ?? item.service.toLowerCase(); + final decryptionKey = + (result['decryption_key'] as String?)?.trim() ?? ''; + + if (!wasExisting && + decryptionKey.isNotEmpty && + filePath != null && + actualService == 'amazon') { + _log.i( + 'Amazon encrypted stream detected, decrypting via FFmpeg...', + ); + updateItemStatus( + item.id, + DownloadStatus.downloading, + progress: 0.9, + ); + + if (effectiveSafMode && isContentUri(filePath)) { + final currentFilePath = filePath; + final tempPath = await _copySafToTemp(currentFilePath); + if (tempPath == null) { + _log.e('Failed to copy encrypted SAF file to temp for decrypt'); + updateItemStatus( + item.id, + DownloadStatus.failed, + error: 'Failed to access encrypted SAF file', + errorType: DownloadErrorType.unknown, + ); + return; + } + + String? decryptedTempPath; + try { + decryptedTempPath = await FFmpegService.decryptAudioFile( + inputPath: tempPath, + decryptionKey: decryptionKey, + deleteOriginal: false, + ); + if (decryptedTempPath == null) { + _log.e('FFmpeg decrypt failed for SAF file'); + updateItemStatus( + item.id, + DownloadStatus.failed, + error: 'Failed to decrypt Amazon stream', + errorType: DownloadErrorType.unknown, + ); + return; + } + + final dotIndex = decryptedTempPath.lastIndexOf('.'); + final decryptedExt = dotIndex >= 0 + ? decryptedTempPath.substring(dotIndex).toLowerCase() + : '.flac'; + final allowedExt = {'.flac', '.m4a', '.mp3', '.opus'}; + final finalExt = allowedExt.contains(decryptedExt) + ? decryptedExt + : '.flac'; + + final newFileName = '${safBaseName ?? 'track'}$finalExt'; + final newUri = await _writeTempToSaf( + treeUri: settings.downloadTreeUri, + relativeDir: effectiveOutputDir, + fileName: newFileName, + mimeType: _mimeTypeForExt(finalExt), + srcPath: decryptedTempPath, + ); + + if (newUri == null) { + _log.e('Failed to write decrypted Amazon stream back to SAF'); + updateItemStatus( + item.id, + DownloadStatus.failed, + error: 'Failed to write decrypted file to storage', + errorType: DownloadErrorType.unknown, + ); + return; + } + + if (newUri != currentFilePath) { + await _deleteSafFile(currentFilePath); + } + filePath = newUri; + finalSafFileName = newFileName; + _log.i('Amazon SAF decryption completed'); + } finally { + try { + await File(tempPath).delete(); + } catch (_) {} + if (decryptedTempPath != null && decryptedTempPath != tempPath) { + try { + await File(decryptedTempPath).delete(); + } catch (_) {} + } + } + } else { + final decryptedPath = await FFmpegService.decryptAudioFile( + inputPath: filePath, + decryptionKey: decryptionKey, + deleteOriginal: true, + ); + if (decryptedPath == null) { + _log.e('FFmpeg decrypt failed for local file'); + updateItemStatus( + item.id, + DownloadStatus.failed, + error: 'Failed to decrypt Amazon stream', + errorType: DownloadErrorType.unknown, + ); + try { + await deleteFile(filePath); + } catch (_) {} + return; + } + filePath = decryptedPath; + _log.i('Amazon local decryption completed'); + } + } + final isContentUriPath = filePath != null && isContentUri(filePath); final mimeType = isContentUriPath ? await _getSafMimeType(filePath) @@ -3323,7 +3445,43 @@ class DownloadQueueNotifier extends Notifier { await File(tempPath).delete(); } catch (_) {} } - } + } + } else if (!isContentUriPath && + !effectiveSafMode && + isFlacFile && + !wasExisting && + actualService == 'amazon' && + decryptionKey.isNotEmpty) { + _log.d( + 'Local FLAC after Amazon decrypt detected, embedding metadata and cover...', + ); + try { + updateItemStatus( + item.id, + DownloadStatus.downloading, + progress: 0.99, + ); + + final finalTrack = _buildTrackForMetadataEmbedding( + trackToDownload, + result, + normalizedAlbumArtist, + ); + final backendGenre = result['genre'] as String?; + final backendLabel = result['label'] as String?; + final backendCopyright = result['copyright'] as String?; + + await _embedMetadataAndCover( + filePath, + finalTrack, + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + ); + _log.d('Local FLAC metadata embedding completed'); + } catch (e) { + _log.w('Local FLAC metadata embedding failed: $e'); + } } // YouTube downloads: embed metadata to raw Opus/MP3 files from Cobalt diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 0c0265b3..ad967413 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -646,13 +646,26 @@ class _QueueTabState extends ConsumerState { } String _getQualityBadgeText(String quality) { - if (quality.contains('bit')) { + final q = quality.trim().toLowerCase(); + if (q.contains('bit')) { return quality.split('/').first; } - final bitrateMatch = RegExp(r'(\d+)kbps').firstMatch(quality); - if (bitrateMatch != null) { - return '${bitrateMatch.group(1)}k'; + + // Supports "MP3 320k", "Opus 256kbps", etc. + final bitrateTextMatch = RegExp( + r'(\d+)\s*k(?:bps)?', + caseSensitive: false, + ).firstMatch(quality); + if (bitrateTextMatch != null) { + return '${bitrateTextMatch.group(1)}k'; } + + // Supports legacy quality IDs like "opus_256" / "mp3_320". + final bitrateIdMatch = RegExp(r'_(\d+)$').firstMatch(q); + if (bitrateIdMatch != null) { + return '${bitrateIdMatch.group(1)}k'; + } + return quality.split(' ').first; } @@ -1647,7 +1660,9 @@ class _QueueTabState extends ConsumerState { ), // Search bar - always at top - if (allHistoryItems.isNotEmpty || hasQueueItems || localLibraryItems.isNotEmpty) + if (allHistoryItems.isNotEmpty || + hasQueueItems || + localLibraryItems.isNotEmpty) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), @@ -2946,13 +2961,13 @@ class _QueueTabState extends ConsumerState { // show bytes downloaded instead of percentage item.progress > 0 ? (item.speedMBps > 0 - ? '${(item.progress * 100).toStringAsFixed(0)}% • ${item.speedMBps.toStringAsFixed(1)} MB/s' - : '${(item.progress * 100).toStringAsFixed(0)}%') + ? '${(item.progress * 100).toStringAsFixed(0)}% • ${item.speedMBps.toStringAsFixed(1)} MB/s' + : '${(item.progress * 100).toStringAsFixed(0)}%') : (item.bytesReceived > 0 - ? '${(item.bytesReceived / (1024 * 1024)).toStringAsFixed(1)} MB • ${item.speedMBps.toStringAsFixed(1)} MB/s' - : (item.speedMBps > 0 - ? 'Downloading • ${item.speedMBps.toStringAsFixed(1)} MB/s' - : 'Starting...')), + ? '${(item.bytesReceived / (1024 * 1024)).toStringAsFixed(1)} MB • ${item.speedMBps.toStringAsFixed(1)} MB/s' + : (item.speedMBps > 0 + ? 'Downloading • ${item.speedMBps.toStringAsFixed(1)} MB/s' + : 'Starting...')), style: Theme.of(context).textTheme.labelSmall ?.copyWith( color: colorScheme.primary, diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index 9608bab9..1ec3e784 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -202,6 +202,7 @@ class _RecentDonorsCard extends StatelessWidget { const SizedBox(height: 16), _DonorTile(name: 'J', colorScheme: colorScheme), _DonorTile(name: 'Julian', colorScheme: colorScheme), + _DonorTile(name: 'matt_3050', colorScheme: colorScheme), _DonorTile(name: 'Daniel', colorScheme: colorScheme), _DonorTile( name: '283Fabio', diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 0638e847..9f0456d0 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -22,7 +22,7 @@ class DownloadSettingsPage extends ConsumerStatefulWidget { } class _DownloadSettingsPageState extends ConsumerState { - static const _builtInServices = ['tidal', 'qobuz']; + static const _builtInServices = ['tidal', 'qobuz', 'amazon']; int _androidSdkVersion = 0; bool _hasAllFilesAccess = false; @@ -248,7 +248,7 @@ class _DownloadSettingsPageState extends ConsumerState { const SizedBox(width: 8), Expanded( child: Text( - 'Select Tidal or Qobuz above to configure quality', + 'Select Tidal, Qobuz, or Amazon above to configure quality', style: Theme.of(context).textTheme.bodySmall ?.copyWith( color: colorScheme.onSurfaceVariant, @@ -1366,6 +1366,7 @@ class _ServiceSelector extends ConsumerWidget { final isExtensionService = ![ 'tidal', 'qobuz', + 'amazon', ].contains(currentService); final isCurrentExtensionEnabled = isExtensionService ? extensionProviders.any((e) => e.id == currentService) @@ -1392,6 +1393,13 @@ class _ServiceSelector extends ConsumerWidget { isSelected: effectiveService == 'qobuz', onTap: () => onChanged('qobuz'), ), + const SizedBox(width: 8), + _ServiceChip( + icon: Icons.shopping_bag_outlined, + label: 'Amazon', + isSelected: effectiveService == 'amazon', + onTap: () => onChanged('amazon'), + ), ], ), if (extensionProviders.isNotEmpty) ...[ diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 8a695600..857f4508 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -5,43 +5,53 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:spotiflac_android/services/cover_cache_manager.dart'; +import 'package:spotiflac_android/services/history_database.dart'; import 'package:spotiflac_android/services/library_database.dart'; import 'package:spotiflac_android/utils/file_access.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:share_plus/share_plus.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/services/ffmpeg_service.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/utils/logger.dart'; + +final _log = AppLogger('TrackMetadata'); class TrackMetadataScreen extends ConsumerStatefulWidget { final DownloadHistoryItem? item; final LocalLibraryItem? localItem; const TrackMetadataScreen({super.key, this.item, this.localItem}) - : assert(item != null || localItem != null, 'Either item or localItem must be provided'); + : assert( + item != null || localItem != null, + 'Either item or localItem must be provided', + ); @override - ConsumerState createState() => _TrackMetadataScreenState(); + ConsumerState createState() => + _TrackMetadataScreenState(); } class _TrackMetadataScreenState extends ConsumerState { bool _fileExists = false; int? _fileSize; - String? _lyrics; // Cleaned lyrics for display (no timestamps) - String? _rawLyrics; // Raw LRC with timestamps for embedding + String? _lyrics; // Cleaned lyrics for display (no timestamps) + String? _rawLyrics; // Raw LRC with timestamps for embedding bool _lyricsLoading = false; String? _lyricsError; bool _showTitleInAppBar = false; - bool _lyricsEmbedded = false; // Track if lyrics are embedded in file - bool _isEmbedding = false; // Track embed operation in progress - bool _isInstrumental = false; // Track if detected as instrumental + bool _lyricsEmbedded = false; // Track if lyrics are embedded in file + bool _isEmbedding = false; // Track embed operation in progress + bool _isInstrumental = false; // Track if detected as instrumental + bool _isConverting = false; // Track convert operation in progress Map? _editedMetadata; // Overrides after metadata edit final ScrollController _scrollController = ScrollController(); - static final RegExp _lrcTimestampPattern = - RegExp(r'^\[\d{2}:\d{2}\.\d{2,3}\]'); - static final RegExp _lrcMetadataPattern = - RegExp(r'^\[[a-zA-Z]+:.*\]$'); + static final RegExp _lrcTimestampPattern = RegExp( + r'^\[\d{2}:\d{2}\.\d{2,3}\]', + ); + static final RegExp _lrcMetadataPattern = RegExp(r'^\[[a-zA-Z]+:.*\]$'); static const List _months = [ 'Jan', 'Feb', @@ -117,49 +127,87 @@ class _TrackMetadataScreenState extends ConsumerState { bool get _isLocalItem => widget.localItem != null; DownloadHistoryItem? get _downloadItem => widget.item; LocalLibraryItem? get _localLibraryItem => widget.localItem; - - String get _itemId => _isLocalItem ? _localLibraryItem!.id : _downloadItem!.id; - String get trackName => _editedMetadata?['title']?.toString() ?? (_isLocalItem ? _localLibraryItem!.trackName : _downloadItem!.trackName); - String get artistName => _editedMetadata?['artist']?.toString() ?? (_isLocalItem ? _localLibraryItem!.artistName : _downloadItem!.artistName); - String get albumName => _editedMetadata?['album']?.toString() ?? (_isLocalItem ? _localLibraryItem!.albumName : _downloadItem!.albumName); + + String get _itemId => + _isLocalItem ? _localLibraryItem!.id : _downloadItem!.id; + String get trackName => + _editedMetadata?['title']?.toString() ?? + (_isLocalItem ? _localLibraryItem!.trackName : _downloadItem!.trackName); + String get artistName => + _editedMetadata?['artist']?.toString() ?? + (_isLocalItem + ? _localLibraryItem!.artistName + : _downloadItem!.artistName); + String get albumName => + _editedMetadata?['album']?.toString() ?? + (_isLocalItem ? _localLibraryItem!.albumName : _downloadItem!.albumName); String? get albumArtist { final edited = _editedMetadata?['album_artist']?.toString(); if (edited != null && edited.isNotEmpty) return edited; - return _normalizeOptionalString(_isLocalItem ? _localLibraryItem!.albumArtist : _downloadItem!.albumArtist); + return _normalizeOptionalString( + _isLocalItem + ? _localLibraryItem!.albumArtist + : _downloadItem!.albumArtist, + ); } + int? get trackNumber { final edited = _editedMetadata?['track_number']; if (edited != null) { final v = int.tryParse(edited.toString()); if (v != null && v > 0) return v; } - return _isLocalItem ? _localLibraryItem!.trackNumber : _downloadItem!.trackNumber; + return _isLocalItem + ? _localLibraryItem!.trackNumber + : _downloadItem!.trackNumber; } + int? get discNumber { final edited = _editedMetadata?['disc_number']; if (edited != null) { final v = int.tryParse(edited.toString()); if (v != null && v > 0) return v; } - return _isLocalItem ? _localLibraryItem!.discNumber : _downloadItem!.discNumber; + return _isLocalItem + ? _localLibraryItem!.discNumber + : _downloadItem!.discNumber; } - String? get releaseDate => _editedMetadata?['date']?.toString() ?? (_isLocalItem ? _localLibraryItem!.releaseDate : _downloadItem!.releaseDate); - String? get isrc => _editedMetadata?['isrc']?.toString() ?? (_isLocalItem ? _localLibraryItem!.isrc : _downloadItem!.isrc); - String? get genre => _editedMetadata?['genre']?.toString() ?? (_isLocalItem ? _localLibraryItem!.genre : _downloadItem!.genre); - String? get label => _editedMetadata?['label']?.toString() ?? (_isLocalItem ? null : _downloadItem!.label); - String? get copyright => _editedMetadata?['copyright']?.toString() ?? (_isLocalItem ? null : _downloadItem!.copyright); - int? get duration => _isLocalItem ? _localLibraryItem!.duration : _downloadItem!.duration; - int? get bitDepth => _isLocalItem ? _localLibraryItem!.bitDepth : _downloadItem!.bitDepth; - int? get sampleRate => _isLocalItem ? _localLibraryItem!.sampleRate : _downloadItem!.sampleRate; - - String get _filePath => _isLocalItem ? _localLibraryItem!.filePath : _downloadItem!.filePath; + + String? get releaseDate => + _editedMetadata?['date']?.toString() ?? + (_isLocalItem + ? _localLibraryItem!.releaseDate + : _downloadItem!.releaseDate); + String? get isrc => + _editedMetadata?['isrc']?.toString() ?? + (_isLocalItem ? _localLibraryItem!.isrc : _downloadItem!.isrc); + String? get genre => + _editedMetadata?['genre']?.toString() ?? + (_isLocalItem ? _localLibraryItem!.genre : _downloadItem!.genre); + String? get label => + _editedMetadata?['label']?.toString() ?? + (_isLocalItem ? null : _downloadItem!.label); + String? get copyright => + _editedMetadata?['copyright']?.toString() ?? + (_isLocalItem ? null : _downloadItem!.copyright); + int? get duration => + _isLocalItem ? _localLibraryItem!.duration : _downloadItem!.duration; + int? get bitDepth => + _isLocalItem ? _localLibraryItem!.bitDepth : _downloadItem!.bitDepth; + int? get sampleRate => + _isLocalItem ? _localLibraryItem!.sampleRate : _downloadItem!.sampleRate; + + String get _filePath => + _isLocalItem ? _localLibraryItem!.filePath : _downloadItem!.filePath; String? get _coverUrl => _isLocalItem ? null : _downloadItem!.coverUrl; - String? get _localCoverPath => _isLocalItem ? _localLibraryItem!.coverPath : null; + String? get _localCoverPath => + _isLocalItem ? _localLibraryItem!.coverPath : null; String? get _spotifyId => _isLocalItem ? null : _downloadItem!.spotifyId; String get _service => _isLocalItem ? 'local' : _downloadItem!.service; - DateTime get _addedAt => _isLocalItem ? _localLibraryItem!.scannedAt : _downloadItem!.downloadedAt; + DateTime get _addedAt => + _isLocalItem ? _localLibraryItem!.scannedAt : _downloadItem!.downloadedAt; String? get _quality => _isLocalItem ? null : _downloadItem!.quality; - + String get cleanFilePath { final path = _filePath; return path.startsWith('EXISTS:') ? path.substring(7) : path; @@ -179,7 +227,8 @@ class _TrackMetadataScreenState extends ConsumerState { expandedHeight: 320, pinned: true, stretch: true, - backgroundColor: colorScheme.surface, // Use theme color for collapsed state + backgroundColor: + colorScheme.surface, // Use theme color for collapsed state surfaceTintColor: Colors.transparent, title: AnimatedOpacity( duration: const Duration(milliseconds: 200), @@ -197,12 +246,19 @@ class _TrackMetadataScreenState extends ConsumerState { ), flexibleSpace: LayoutBuilder( builder: (context, constraints) { - final collapseRatio = (constraints.maxHeight - kToolbarHeight) / (320 - kToolbarHeight); + final collapseRatio = + (constraints.maxHeight - kToolbarHeight) / + (320 - kToolbarHeight); final showContent = collapseRatio > 0.3; - + return FlexibleSpaceBar( collapseMode: CollapseMode.none, - background: _buildHeaderBackground(context, colorScheme, coverSize, showContent), + background: _buildHeaderBackground( + context, + colorScheme, + coverSize, + showContent, + ), stretchModes: const [ StretchMode.zoomBackground, StretchMode.blurBackground, @@ -243,23 +299,28 @@ class _TrackMetadataScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTrackInfoCard(context, colorScheme, _fileExists), - + const SizedBox(height: 16), - + _buildMetadataCard(context, colorScheme, _fileSize), - + const SizedBox(height: 16), - - _buildFileInfoCard(context, colorScheme, _fileExists, _fileSize), - + + _buildFileInfoCard( + context, + colorScheme, + _fileExists, + _fileSize, + ), + const SizedBox(height: 16), - + _buildLyricsCard(context, colorScheme), - + const SizedBox(height: 24), - + _buildActionButtons(context, ref, colorScheme, _fileExists), - + const SizedBox(height: 32), ], ), @@ -270,7 +331,12 @@ class _TrackMetadataScreenState extends ConsumerState { ); } - Widget _buildHeaderBackground(BuildContext context, ColorScheme colorScheme, double coverSize, bool showContent) { + Widget _buildHeaderBackground( + BuildContext context, + ColorScheme colorScheme, + double coverSize, + bool showContent, + ) { return Stack( fit: StackFit.expand, children: [ @@ -291,17 +357,15 @@ class _TrackMetadataScreenState extends ConsumerState { ) else Container(color: colorScheme.surface), - + // Blur filter ClipRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( - color: colorScheme.surface.withValues(alpha: 0.4), - ), + child: Container(color: colorScheme.surface.withValues(alpha: 0.4)), ), ), - + // Bottom fade to surface Positioned( left: 0, @@ -321,7 +385,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), ), - + // Cover art AnimatedOpacity( duration: const Duration(milliseconds: 150), @@ -362,18 +426,15 @@ class _TrackMetadataScreenState extends ConsumerState { ), ) : _localCoverPath != null && _localCoverPath!.isNotEmpty - ? Image.file( - File(_localCoverPath!), - fit: BoxFit.cover, - ) - : Container( - color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.music_note, - size: 64, - color: colorScheme.onSurfaceVariant, - ), - ), + ? Image.file(File(_localCoverPath!), fit: BoxFit.cover) + : Container( + color: colorScheme.surfaceContainerHighest, + child: Icon( + Icons.music_note, + size: 64, + color: colorScheme.onSurfaceVariant, + ), + ), ), ), ), @@ -384,7 +445,11 @@ class _TrackMetadataScreenState extends ConsumerState { ); } - Widget _buildTrackInfoCard(BuildContext context, ColorScheme colorScheme, bool fileExists) { + Widget _buildTrackInfoCard( + BuildContext context, + ColorScheme colorScheme, + bool fileExists, + ) { return Card( elevation: 0, color: colorScheme.surfaceContainerLow, @@ -402,15 +467,15 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), const SizedBox(height: 4), - + Text( artistName, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: colorScheme.primary, - ), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(color: colorScheme.primary), ), const SizedBox(height: 8), - + Row( children: [ Icon( @@ -429,11 +494,14 @@ class _TrackMetadataScreenState extends ConsumerState { ), ], ), - + if (!fileExists) ...[ const SizedBox(height: 12), Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: colorScheme.errorContainer, borderRadius: BorderRadius.circular(20), @@ -465,7 +533,11 @@ class _TrackMetadataScreenState extends ConsumerState { ); } - Widget _buildMetadataCard(BuildContext context, ColorScheme colorScheme, int? fileSize) { + Widget _buildMetadataCard( + BuildContext context, + ColorScheme colorScheme, + int? fileSize, + ) { return Card( elevation: 0, color: colorScheme.surfaceContainerLow, @@ -477,11 +549,7 @@ class _TrackMetadataScreenState extends ConsumerState { children: [ Row( children: [ - Icon( - Icons.info_outline, - size: 20, - color: colorScheme.primary, - ), + Icon(Icons.info_outline, size: 20, color: colorScheme.primary), const SizedBox(width: 8), Text( context.l10n.trackMetadata, @@ -493,9 +561,9 @@ class _TrackMetadataScreenState extends ConsumerState { ], ), const SizedBox(height: 16), - + _buildMetadataGrid(context, colorScheme), - + if (_spotifyId != null && _spotifyId!.isNotEmpty) ...[ const SizedBox(height: 8), Builder( @@ -504,15 +572,22 @@ class _TrackMetadataScreenState extends ConsumerState { return OutlinedButton.icon( onPressed: () => _openServiceUrl(context), icon: const Icon(Icons.open_in_new, size: 18), - label: Text(isDeezer ? context.l10n.trackOpenInDeezer : context.l10n.trackOpenInSpotify), + label: Text( + isDeezer + ? context.l10n.trackOpenInDeezer + : context.l10n.trackOpenInSpotify, + ), style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ); - } + }, ), ], ], @@ -523,24 +598,24 @@ class _TrackMetadataScreenState extends ConsumerState { Future _openServiceUrl(BuildContext context) async { if (_spotifyId == null) return; - + final isDeezer = _spotifyId!.contains('deezer'); final rawId = _spotifyId!.replaceAll('deezer:', ''); - - final webUrl = isDeezer + + final webUrl = isDeezer ? 'https://www.deezer.com/track/$rawId' : 'https://open.spotify.com/track/$rawId'; - + final appUri = isDeezer ? Uri.parse('deezer://www.deezer.com/track/$rawId') : Uri.parse('spotify:track:$rawId'); - + try { final launched = await launchUrl( appUri, mode: LaunchMode.externalApplication, ); - + if (!launched) { await launchUrl( Uri.parse(webUrl), @@ -557,7 +632,11 @@ class _TrackMetadataScreenState extends ConsumerState { if (context.mounted) { _copyToClipboard(context, webUrl); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.snackbarUrlCopied(isDeezer ? 'Deezer' : 'Spotify'))), + SnackBar( + content: Text( + context.l10n.snackbarUrlCopied(isDeezer ? 'Deezer' : 'Spotify'), + ), + ), ); } } @@ -568,8 +647,10 @@ class _TrackMetadataScreenState extends ConsumerState { // Determine audio quality string - prefer stored quality from download String? audioQualityStr; final fileName = _filePath.split('/').last; - final fileExt = fileName.contains('.') ? fileName.split('.').last.toUpperCase() : ''; - + final fileExt = fileName.contains('.') + ? fileName.split('.').last.toUpperCase() + : ''; + // Use stored quality from download history if available if (_quality != null && _quality!.isNotEmpty) { audioQualityStr = _quality; @@ -587,7 +668,7 @@ class _TrackMetadataScreenState extends ConsumerState { audioQualityStr = 'AAC'; } } - + final items = <_MetadataItem>[ _MetadataItem(context.l10n.trackTrackName, trackName), _MetadataItem(context.l10n.trackArtist, artistName), @@ -610,28 +691,30 @@ class _TrackMetadataScreenState extends ConsumerState { _MetadataItem(context.l10n.trackLabel, label!), if (copyright != null && copyright!.isNotEmpty) _MetadataItem(context.l10n.trackCopyright, copyright!), - if (isrc != null && isrc!.isNotEmpty) - _MetadataItem('ISRC', isrc!), + if (isrc != null && isrc!.isNotEmpty) _MetadataItem('ISRC', isrc!), ]; - + if (!_isLocalItem && _spotifyId != null && _spotifyId!.isNotEmpty) { final isDeezer = _spotifyId!.contains('deezer'); final cleanId = _spotifyId!.replaceAll('deezer:', ''); items.add(_MetadataItem(isDeezer ? 'Deezer ID' : 'Spotify ID', cleanId)); } - - items.add(_MetadataItem(context.l10n.trackMetadataService, _service.toUpperCase())); - items.add(_MetadataItem( - context.l10n.trackDownloaded, - _formatFullDate(_addedAt), - )); + + items.add( + _MetadataItem(context.l10n.trackMetadataService, _service.toUpperCase()), + ); + items.add( + _MetadataItem(context.l10n.trackDownloaded, _formatFullDate(_addedAt)), + ); return Column( children: items.map((metadata) { - final isCopyable = metadata.label == 'ISRC' || - metadata.label == 'Spotify ID'; + final isCopyable = + metadata.label == 'ISRC' || metadata.label == 'Spotify ID'; return InkWell( - onTap: isCopyable ? () => _copyToClipboard(context, metadata.value) : null, + onTap: isCopyable + ? () => _copyToClipboard(context, metadata.value) + : null, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4), @@ -675,10 +758,18 @@ class _TrackMetadataScreenState extends ConsumerState { return '$minutes:${secs.toString().padLeft(2, '0')}'; } - Widget _buildFileInfoCard(BuildContext context, ColorScheme colorScheme, bool fileExists, int? fileSize) { + Widget _buildFileInfoCard( + BuildContext context, + ColorScheme colorScheme, + bool fileExists, + int? fileSize, + ) { final fileName = cleanFilePath.split(Platform.pathSeparator).last; - final fileExtension = fileName.contains('.') ? fileName.split('.').last.toUpperCase() : 'Unknown'; - + final fileExtension = fileName.contains('.') + ? fileName.split('.').last.toUpperCase() + : 'Unknown'; + final lossyBitrateLabel = _extractLossyBitrateLabel(_quality); + return Card( elevation: 0, color: colorScheme.surfaceContainerLow, @@ -706,13 +797,16 @@ class _TrackMetadataScreenState extends ConsumerState { ], ), const SizedBox(height: 16), - + Wrap( spacing: 8, runSpacing: 8, children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(20), @@ -728,7 +822,10 @@ class _TrackMetadataScreenState extends ConsumerState { ), if (fileSize != null) Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(20), @@ -742,15 +839,21 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), ), - if (fileExtension == 'MP3') + if ((fileExtension == 'MP3' || + fileExtension == 'OPUS' || + fileExtension == 'OGG') && + lossyBitrateLabel != null) Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(20), ), child: Text( - '320kbps', + lossyBitrateLabel, style: TextStyle( color: colorScheme.onTertiaryContainer, fontWeight: FontWeight.w600, @@ -760,7 +863,10 @@ class _TrackMetadataScreenState extends ConsumerState { ) else if (bitDepth != null && sampleRate != null) Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(20), @@ -775,7 +881,10 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: _getServiceColor(_service, colorScheme), borderRadius: BorderRadius.circular(20), @@ -802,9 +911,9 @@ class _TrackMetadataScreenState extends ConsumerState { ), ], ), - + const SizedBox(height: 16), - + InkWell( onTap: () => _copyToClipboard(context, cleanFilePath), borderRadius: BorderRadius.circular(12), @@ -878,7 +987,7 @@ class _TrackMetadataScreenState extends ConsumerState { ], ), const SizedBox(height: 12), - + if (_lyricsLoading) const Center( child: Padding( @@ -895,7 +1004,11 @@ class _TrackMetadataScreenState extends ConsumerState { ), child: Row( children: [ - Icon(Icons.error_outline, color: colorScheme.error, size: 20), + Icon( + Icons.error_outline, + color: colorScheme.error, + size: 20, + ), const SizedBox(width: 12), Expanded( child: Text( @@ -920,7 +1033,11 @@ class _TrackMetadataScreenState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.music_note, color: colorScheme.tertiary, size: 20), + Icon( + Icons.music_note, + color: colorScheme.tertiary, + size: 20, + ), const SizedBox(width: 12), Text( context.l10n.trackInstrumental, @@ -958,7 +1075,9 @@ class _TrackMetadataScreenState extends ConsumerState { ? const SizedBox( width: 18, height: 18, - child: CircularProgressIndicator(strokeWidth: 2), + child: CircularProgressIndicator( + strokeWidth: 2, + ), ) : const Icon(Icons.save_alt), label: Text(context.l10n.trackEmbedLyrics), @@ -983,7 +1102,7 @@ class _TrackMetadataScreenState extends ConsumerState { Future _fetchLyrics() async { if (_lyricsLoading) return; - + setState(() { _lyricsLoading = true; _lyricsError = null; @@ -993,7 +1112,7 @@ class _TrackMetadataScreenState extends ConsumerState { try { // Convert duration from seconds to milliseconds final durationMs = (duration ?? 0) * 1000; - + // First, check if lyrics are embedded in the file if (_fileExists) { final embeddedResult = await PlatformBridge.getLyricsLRC( @@ -1003,7 +1122,7 @@ class _TrackMetadataScreenState extends ConsumerState { filePath: cleanFilePath, durationMs: 0, ).timeout(const Duration(seconds: 5), onTimeout: () => ''); - + if (embeddedResult.isNotEmpty) { // Lyrics found in file if (mounted) { @@ -1017,7 +1136,7 @@ class _TrackMetadataScreenState extends ConsumerState { return; } } - + // No embedded lyrics, fetch from online final result = await PlatformBridge.getLyricsLRC( _spotifyId ?? '', @@ -1025,11 +1144,8 @@ class _TrackMetadataScreenState extends ConsumerState { artistName, filePath: null, // Don't check file again durationMs: durationMs, - ).timeout( - const Duration(seconds: 20), - onTimeout: () => '', - ); - + ).timeout(const Duration(seconds: 20), onTimeout: () => ''); + if (mounted) { // Check for instrumental marker if (result == '[instrumental:true]') { @@ -1054,7 +1170,7 @@ class _TrackMetadataScreenState extends ConsumerState { } } catch (e) { if (mounted) { - final errorMsg = e.toString().contains('TimeoutException') + final errorMsg = e.toString().contains('TimeoutException') ? context.l10n.trackLyricsTimeout : context.l10n.trackLyricsLoadFailed; setState(() { @@ -1064,21 +1180,119 @@ class _TrackMetadataScreenState extends ConsumerState { } } } - + Future _embedLyrics() async { if (_isEmbedding || _rawLyrics == null || !_fileExists) return; - + setState(() => _isEmbedding = true); - + + String? safTempPath; + String? coverPath; + try { - // Use raw LRC content directly - it already has timestamps and metadata - final result = await PlatformBridge.embedLyricsToFile( - cleanFilePath, - _rawLyrics!, - ); - - if (mounted) { + final rawLyrics = _rawLyrics!; + var workingPath = cleanFilePath; + + if (_isSafFile) { + safTempPath = await PlatformBridge.copyContentUriToTemp(cleanFilePath); + if (safTempPath == null || safTempPath.isEmpty) { + throw Exception('Failed to access SAF file'); + } + workingPath = safTempPath; + } + + final lower = workingPath.toLowerCase(); + final isFlac = lower.endsWith('.flac'); + final isMp3 = lower.endsWith('.mp3'); + final isOpus = lower.endsWith('.opus') || lower.endsWith('.ogg'); + + bool success = false; + String? error; + + if (isFlac) { + final result = await PlatformBridge.embedLyricsToFile( + workingPath, + rawLyrics, + ); if (result['success'] == true) { + if (_isSafFile) { + final ok = await PlatformBridge.writeTempToSaf( + workingPath, + cleanFilePath, + ); + success = ok; + if (!ok) { + error = 'Failed to write back to storage'; + } + } else { + success = true; + } + } else { + error = result['error']?.toString() ?? 'Failed to embed lyrics'; + } + } else if (isMp3 || isOpus) { + final metadata = _buildFallbackMetadata(); + try { + final result = await PlatformBridge.readFileMetadata(workingPath); + if (result['error'] == null) { + final mapped = _mapMetadataForTagEmbed(result); + metadata.addAll(mapped); + } + } catch (e) { + _log.w('Failed reading file metadata before lyrics embed: $e'); + } + + metadata['LYRICS'] = rawLyrics; + metadata['UNSYNCEDLYRICS'] = rawLyrics; + + try { + final tempDir = await getTemporaryDirectory(); + final coverOutput = + '${tempDir.path}${Platform.pathSeparator}lyrics_cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final coverResult = await PlatformBridge.extractCoverToFile( + workingPath, + coverOutput, + ); + if (coverResult['error'] == null) { + coverPath = coverOutput; + } + } catch (_) {} + + String? ffmpegResult; + if (isMp3) { + ffmpegResult = await FFmpegService.embedMetadataToMp3( + mp3Path: workingPath, + coverPath: coverPath, + metadata: metadata, + ); + } else { + ffmpegResult = await FFmpegService.embedMetadataToOpus( + opusPath: workingPath, + coverPath: coverPath, + metadata: metadata, + ); + } + + if (ffmpegResult == null) { + error = 'Failed to embed lyrics'; + } else if (_isSafFile) { + final ok = await PlatformBridge.writeTempToSaf( + ffmpegResult, + cleanFilePath, + ); + success = ok; + if (!ok) { + error = 'Failed to write back to storage'; + } + } else { + success = true; + } + } else { + error = 'Unsupported audio format'; + } + + if (mounted) { + if (success) { setState(() { _lyricsEmbedded = true; _isEmbedding = false; @@ -1089,16 +1303,27 @@ class _TrackMetadataScreenState extends ConsumerState { } else { setState(() => _isEmbedding = false); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(result['error'] ?? 'Failed to embed lyrics')), + SnackBar(content: Text(error ?? 'Failed to embed lyrics')), ); } } } catch (e) { if (mounted) { setState(() => _isEmbedding = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $e')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error: $e'))); + } + } finally { + if (coverPath != null) { + try { + await File(coverPath).delete(); + } catch (_) {} + } + if (safTempPath != null) { + try { + await File(safTempPath).delete(); + } catch (_) {} } } } @@ -1127,7 +1352,8 @@ class _TrackMetadataScreenState extends ConsumerState { if (_isSafFile) { // SAF file: save to temp, then copy to SAF tree final tempDir = await Directory.systemTemp.createTemp('cover_'); - final tempOutput = '${tempDir.path}${Platform.pathSeparator}$baseName.jpg'; + final tempOutput = + '${tempDir.path}${Platform.pathSeparator}$baseName.jpg'; Map result; if (_coverUrl != null && _coverUrl!.isNotEmpty) { @@ -1153,10 +1379,16 @@ class _TrackMetadataScreenState extends ConsumerState { if (result['error'] != null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackSaveFailed(result['error'].toString()))), + SnackBar( + content: Text( + context.l10n.trackSaveFailed(result['error'].toString()), + ), + ), ); } - try { await Directory(tempDir.path).delete(recursive: true); } catch (_) {} + try { + await Directory(tempDir.path).delete(recursive: true); + } catch (_) {} return; } @@ -1171,7 +1403,9 @@ class _TrackMetadataScreenState extends ConsumerState { mimeType: 'image/jpeg', srcPath: tempOutput, ); - try { await Directory(tempDir.path).delete(recursive: true); } catch (_) {} + try { + await Directory(tempDir.path).delete(recursive: true); + } catch (_) {} if (mounted) { if (safUri != null) { ScaffoldMessenger.of(context).showSnackBar( @@ -1179,16 +1413,26 @@ class _TrackMetadataScreenState extends ConsumerState { ); } else { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackSaveFailed('Failed to write to storage'))), + SnackBar( + content: Text( + context.l10n.trackSaveFailed('Failed to write to storage'), + ), + ), ); } } } else { // No SAF tree info, keep in temp - try { await Directory(tempDir.path).delete(recursive: true); } catch (_) {} + try { + await Directory(tempDir.path).delete(recursive: true); + } catch (_) {} if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackSaveFailed('No storage access'))), + SnackBar( + content: Text( + context.l10n.trackSaveFailed('No storage access'), + ), + ), ); } } @@ -1223,7 +1467,11 @@ class _TrackMetadataScreenState extends ConsumerState { if (mounted) { if (result['error'] != null) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackSaveFailed(result['error'].toString()))), + SnackBar( + content: Text( + context.l10n.trackSaveFailed(result['error'].toString()), + ), + ), ); } else { ScaffoldMessenger.of(context).showSnackBar( @@ -1248,7 +1496,8 @@ class _TrackMetadataScreenState extends ConsumerState { if (_isSafFile) { // SAF file: save to temp, then copy to SAF tree final tempDir = await Directory.systemTemp.createTemp('lyrics_'); - final tempOutput = '${tempDir.path}${Platform.pathSeparator}$baseName.lrc'; + final tempOutput = + '${tempDir.path}${Platform.pathSeparator}$baseName.lrc'; final result = await PlatformBridge.fetchAndSaveLyrics( trackName: trackName, @@ -1261,10 +1510,16 @@ class _TrackMetadataScreenState extends ConsumerState { if (result['error'] != null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackSaveFailed(result['error'].toString()))), + SnackBar( + content: Text( + context.l10n.trackSaveFailed(result['error'].toString()), + ), + ), ); } - try { await Directory(tempDir.path).delete(recursive: true); } catch (_) {} + try { + await Directory(tempDir.path).delete(recursive: true); + } catch (_) {} return; } @@ -1279,23 +1534,37 @@ class _TrackMetadataScreenState extends ConsumerState { mimeType: 'text/plain', srcPath: tempOutput, ); - try { await Directory(tempDir.path).delete(recursive: true); } catch (_) {} + try { + await Directory(tempDir.path).delete(recursive: true); + } catch (_) {} if (mounted) { if (safUri != null) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackLyricsSaved(baseName))), + SnackBar( + content: Text(context.l10n.trackLyricsSaved(baseName)), + ), ); } else { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackSaveFailed('Failed to write to storage'))), + SnackBar( + content: Text( + context.l10n.trackSaveFailed('Failed to write to storage'), + ), + ), ); } } } else { - try { await Directory(tempDir.path).delete(recursive: true); } catch (_) {} + try { + await Directory(tempDir.path).delete(recursive: true); + } catch (_) {} if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackSaveFailed('No storage access'))), + SnackBar( + content: Text( + context.l10n.trackSaveFailed('No storage access'), + ), + ), ); } } @@ -1317,7 +1586,11 @@ class _TrackMetadataScreenState extends ConsumerState { if (mounted) { if (result['error'] != null) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackSaveFailed(result['error'].toString()))), + SnackBar( + content: Text( + context.l10n.trackSaveFailed(result['error'].toString()), + ), + ), ); } else { ScaffoldMessenger.of(context).showSnackBar( @@ -1402,8 +1675,9 @@ class _TrackMetadataScreenState extends ConsumerState { final ffmpegTarget = tempPath ?? cleanFilePath; final coverPath = result['cover_path'] as String?; - final metadata = (result['metadata'] as Map?) - ?.map((k, v) => MapEntry(k, v.toString())); + final metadata = (result['metadata'] as Map?)?.map( + (k, v) => MapEntry(k, v.toString()), + ); final lower = cleanFilePath.toLowerCase(); String? ffmpegResult; @@ -1426,14 +1700,24 @@ class _TrackMetadataScreenState extends ConsumerState { final ok = await PlatformBridge.writeTempToSaf(ffmpegResult, safUri); if (!ok && mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackSaveFailed('Failed to write back to storage'))), + SnackBar( + content: Text( + context.l10n.trackSaveFailed( + 'Failed to write back to storage', + ), + ), + ), ); // Cleanup temp files if (coverPath != null && coverPath.isNotEmpty) { - try { await File(coverPath).delete(); } catch (_) {} + try { + await File(coverPath).delete(); + } catch (_) {} } if (tempPath.isNotEmpty) { - try { await File(tempPath).delete(); } catch (_) {} + try { + await File(tempPath).delete(); + } catch (_) {} } return; } @@ -1441,7 +1725,9 @@ class _TrackMetadataScreenState extends ConsumerState { // Cleanup temp files if (tempPath != null && tempPath.isNotEmpty) { - try { await File(tempPath).delete(); } catch (_) {} + try { + await File(tempPath).delete(); + } catch (_) {} } if (mounted) { @@ -1458,7 +1744,9 @@ class _TrackMetadataScreenState extends ConsumerState { // Cleanup temp cover from Go backend if (coverPath != null && coverPath.isNotEmpty) { - try { await File(coverPath).delete(); } catch (_) {} + try { + await File(coverPath).delete(); + } catch (_) {} } } else { if (mounted) { @@ -1480,32 +1768,39 @@ class _TrackMetadataScreenState extends ConsumerState { String _cleanLrcForDisplay(String lrc) { final lines = lrc.split('\n'); final cleanLines = []; - + for (final line in lines) { final trimmedLine = line.trim(); - + // Skip metadata tags if (_lrcMetadataPattern.hasMatch(trimmedLine)) { continue; } - + // Remove timestamp and clean up final cleanLine = trimmedLine.replaceAll(_lrcTimestampPattern, '').trim(); if (cleanLine.isNotEmpty) { cleanLines.add(cleanLine); } } - + return cleanLines.join('\n'); } - Widget _buildActionButtons(BuildContext context, WidgetRef ref, ColorScheme colorScheme, bool fileExists) { + Widget _buildActionButtons( + BuildContext context, + WidgetRef ref, + ColorScheme colorScheme, + bool fileExists, + ) { return Row( children: [ Expanded( flex: 2, child: FilledButton.icon( - onPressed: fileExists ? () => _openFile(context, cleanFilePath) : null, + onPressed: fileExists + ? () => _openFile(context, cleanFilePath) + : null, icon: const Icon(Icons.play_arrow), label: Text(context.l10n.trackMetadataPlay), style: FilledButton.styleFrom( @@ -1517,12 +1812,15 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), const SizedBox(width: 12), - + Expanded( child: OutlinedButton.icon( onPressed: () => _confirmDelete(context, ref, colorScheme), icon: Icon(Icons.delete_outline, color: colorScheme.error), - label: Text(context.l10n.trackMetadataDelete, style: TextStyle(color: colorScheme.error)), + label: Text( + context.l10n.trackMetadataDelete, + style: TextStyle(color: colorScheme.error), + ), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( @@ -1536,7 +1834,11 @@ class _TrackMetadataScreenState extends ConsumerState { ); } - void _showOptionsMenu(BuildContext context, WidgetRef ref, ColorScheme colorScheme) { + void _showOptionsMenu( + BuildContext context, + WidgetRef ref, + ColorScheme colorScheme, + ) { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( @@ -1549,91 +1851,624 @@ class _TrackMetadataScreenState extends ConsumerState { builder: (context) => SafeArea( child: SingleChildScrollView( child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4), - borderRadius: BorderRadius.circular(2), + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(2), + ), ), - ), - const SizedBox(height: 16), - ListTile( - leading: const Icon(Icons.copy), - title: Text(context.l10n.trackCopyFilePath), - onTap: () { - Navigator.pop(context); - _copyToClipboard(context, cleanFilePath); - }, - ), - if (_fileExists) + const SizedBox(height: 16), ListTile( - leading: const Icon(Icons.edit_outlined), - title: Text(context.l10n.trackEditMetadata), + leading: const Icon(Icons.copy), + title: Text(context.l10n.trackCopyFilePath), onTap: () { Navigator.pop(context); - _showEditMetadataSheet(context, ref, colorScheme); + _copyToClipboard(context, cleanFilePath); }, ), - if (!_isLocalItem && (_coverUrl != null || _fileExists)) + if (_fileExists) + ListTile( + leading: const Icon(Icons.edit_outlined), + title: Text(context.l10n.trackEditMetadata), + onTap: () { + Navigator.pop(context); + _showEditMetadataSheet(context, ref, colorScheme); + }, + ), + if (!_isLocalItem && (_coverUrl != null || _fileExists)) + ListTile( + leading: const Icon(Icons.image_outlined), + title: Text(context.l10n.trackSaveCoverArt), + subtitle: Text(context.l10n.trackSaveCoverArtSubtitle), + onTap: () { + Navigator.pop(context); + _saveCoverArt(); + }, + ), + if (!_isLocalItem) + ListTile( + leading: const Icon(Icons.lyrics_outlined), + title: Text(context.l10n.trackSaveLyrics), + subtitle: Text(context.l10n.trackSaveLyricsSubtitle), + onTap: () { + Navigator.pop(context); + _saveLyrics(); + }, + ), + if (_fileExists) + ListTile( + leading: const Icon(Icons.travel_explore), + title: Text(context.l10n.trackReEnrich), + subtitle: Text(context.l10n.trackReEnrichOnlineSubtitle), + onTap: () { + Navigator.pop(context); + _reEnrichMetadata(); + }, + ), + if (_fileExists && _isConvertibleFormat) + ListTile( + leading: const Icon(Icons.swap_horiz), + title: Text(context.l10n.trackConvertFormat), + subtitle: Text(context.l10n.trackConvertFormatSubtitle), + onTap: () { + Navigator.pop(context); + _showConvertSheet(context); + }, + ), + const Divider(height: 1), ListTile( - leading: const Icon(Icons.image_outlined), - title: Text(context.l10n.trackSaveCoverArt), - subtitle: Text(context.l10n.trackSaveCoverArtSubtitle), + leading: const Icon(Icons.share), + title: Text(context.l10n.trackMetadataShare), onTap: () { Navigator.pop(context); - _saveCoverArt(); + _shareFile(context); }, ), - if (!_isLocalItem) ListTile( - leading: const Icon(Icons.lyrics_outlined), - title: Text(context.l10n.trackSaveLyrics), - subtitle: Text(context.l10n.trackSaveLyricsSubtitle), + leading: Icon(Icons.delete, color: colorScheme.error), + title: Text( + context.l10n.trackRemoveFromDevice, + style: TextStyle(color: colorScheme.error), + ), onTap: () { Navigator.pop(context); - _saveLyrics(); + _confirmDelete(context, ref, colorScheme); }, ), - if (_fileExists) - ListTile( - leading: const Icon(Icons.travel_explore), - title: Text(context.l10n.trackReEnrich), - subtitle: Text(context.l10n.trackReEnrichOnlineSubtitle), - onTap: () { - Navigator.pop(context); - _reEnrichMetadata(); - }, - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.share), - title: Text(context.l10n.trackMetadataShare), - onTap: () { - Navigator.pop(context); - _shareFile(context); - }, - ), - ListTile( - leading: Icon(Icons.delete, color: colorScheme.error), - title: Text(context.l10n.trackRemoveFromDevice, style: TextStyle(color: colorScheme.error)), - onTap: () { - Navigator.pop(context); - _confirmDelete(context, ref, colorScheme); - }, - ), - const SizedBox(height: 16), - ], - ), + const SizedBox(height: 16), + ], + ), ), ), ); } - void _showEditMetadataSheet(BuildContext context, WidgetRef ref, ColorScheme colorScheme) async { + /// Whether the current file format supports conversion + bool get _isConvertibleFormat { + final lower = cleanFilePath.toLowerCase(); + return lower.endsWith('.flac') || + lower.endsWith('.mp3') || + lower.endsWith('.opus') || + lower.endsWith('.ogg'); + } + + String get _currentFileFormat { + final lower = cleanFilePath.toLowerCase(); + if (lower.endsWith('.flac')) return 'FLAC'; + if (lower.endsWith('.mp3')) return 'MP3'; + if (lower.endsWith('.opus') || lower.endsWith('.ogg')) return 'Opus'; + return 'Unknown'; + } + + Map _buildFallbackMetadata() { + return { + 'TITLE': trackName, + 'ARTIST': artistName, + 'ALBUM': albumName, + if (albumArtist != null && albumArtist!.isNotEmpty) + 'ALBUMARTIST': albumArtist!, + if (trackNumber != null) 'TRACKNUMBER': trackNumber.toString(), + if (discNumber != null) 'DISCNUMBER': discNumber.toString(), + if (releaseDate != null && releaseDate!.isNotEmpty) 'DATE': releaseDate!, + if (isrc != null && isrc!.isNotEmpty) 'ISRC': isrc!, + if (genre != null && genre!.isNotEmpty) 'GENRE': genre!, + if (label != null && label!.isNotEmpty) 'LABEL': label!, + if (copyright != null && copyright!.isNotEmpty) 'COPYRIGHT': copyright!, + }; + } + + Map _mapMetadataForTagEmbed(Map source) { + final mapped = {}; + + void put(String key, dynamic value) { + final normalized = value?.toString().trim(); + if (normalized == null || normalized.isEmpty) return; + mapped[key] = normalized; + } + + put('TITLE', source['title']); + put('ARTIST', source['artist']); + put('ALBUM', source['album']); + put('ALBUMARTIST', source['album_artist']); + put('DATE', source['date']); + put('ISRC', source['isrc']); + put('GENRE', source['genre']); + put('ORGANIZATION', source['label']); + put('COPYRIGHT', source['copyright']); + put('COMPOSER', source['composer']); + put('COMMENT', source['comment']); + + final trackNumber = source['track_number']; + if (trackNumber != null && trackNumber.toString() != '0') { + put('TRACKNUMBER', trackNumber); + } + final discNumber = source['disc_number']; + if (discNumber != null && discNumber.toString() != '0') { + put('DISCNUMBER', discNumber); + } + + return mapped; + } + + String _buildConvertedQualityLabel(String targetFormat, String bitrate) { + final normalizedBitrate = bitrate.trim().toLowerCase(); + return '${targetFormat.toUpperCase()} $normalizedBitrate'; + } + + String? _extractLossyBitrateLabel(String? quality) { + if (quality == null || quality.isEmpty) return null; + final match = RegExp( + r'(\d+)\s*k(?:bps)?', + caseSensitive: false, + ).firstMatch(quality); + if (match == null) return null; + return '${match.group(1)}kbps'; + } + + String _extractFileNameFromPathOrUri(String pathOrUri) { + if (pathOrUri.isEmpty) return ''; + try { + if (pathOrUri.startsWith('content://')) { + final uri = Uri.parse(pathOrUri); + if (uri.pathSegments.isNotEmpty) { + var last = Uri.decodeComponent(uri.pathSegments.last); + if (last.contains('/')) { + last = last.split('/').last; + } + if (last.contains(':')) { + last = last.split(':').last; + } + if (last.isNotEmpty) return last; + } + } + } catch (_) {} + + final normalized = pathOrUri.replaceAll('\\', '/'); + if (normalized.contains('/')) { + return normalized.split('/').last; + } + return normalized; + } + + void _showConvertSheet(BuildContext context) { + final currentFormat = _currentFileFormat; + // Available target formats (exclude current) + final formats = [ + 'MP3', + 'Opus', + ].where((f) => f != currentFormat).toList(); + if (currentFormat == 'FLAC') { + // FLAC can convert to both + } + + String selectedFormat = formats.first; + String selectedBitrate = selectedFormat == 'Opus' ? '128k' : '320k'; + + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (sheetContext) { + return StatefulBuilder( + builder: (context, setSheetState) { + final colorScheme = Theme.of(context).colorScheme; + final bitrates = ['128k', '192k', '256k', '320k']; + + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: colorScheme.onSurfaceVariant.withValues( + alpha: 0.4, + ), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const SizedBox(height: 16), + Text( + context.l10n.trackConvertTitle, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + + // Target format + Text( + context.l10n.trackConvertTargetFormat, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Row( + children: formats.map((format) { + final isSelected = format == selectedFormat; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: ChoiceChip( + label: Text(format), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setSheetState(() { + selectedFormat = format; + // Reset bitrate to default for format + selectedBitrate = format == 'Opus' + ? '128k' + : '320k'; + }); + } + }, + ), + ); + }).toList(), + ), + const SizedBox(height: 16), + + // Bitrate + Text( + context.l10n.trackConvertBitrate, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: bitrates.map((br) { + final isSelected = br == selectedBitrate; + return ChoiceChip( + label: Text(br), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setSheetState(() => selectedBitrate = br); + } + }, + ); + }).toList(), + ), + const SizedBox(height: 24), + + // Convert button + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () { + Navigator.pop(context); + _confirmAndConvert( + context: this.context, + sourceFormat: currentFormat, + targetFormat: selectedFormat, + bitrate: selectedBitrate, + ); + }, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + '$currentFormat -> $selectedFormat @ $selectedBitrate', + ), + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ); + }, + ); + }, + ); + } + + void _confirmAndConvert({ + required BuildContext context, + required String sourceFormat, + required String targetFormat, + required String bitrate, + }) { + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Text(dialogContext.l10n.trackConvertConfirmTitle), + content: Text( + dialogContext.l10n.trackConvertConfirmMessage( + sourceFormat, + targetFormat, + bitrate, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(dialogContext.l10n.dialogCancel), + ), + FilledButton( + onPressed: () { + Navigator.pop(dialogContext); + _performConversion( + targetFormat: targetFormat, + bitrate: bitrate, + ); + }, + child: Text(dialogContext.l10n.trackConvertFormat), + ), + ], + ); + }, + ); + } + + Future _performConversion({ + required String targetFormat, + required String bitrate, + }) async { + if (_isConverting) return; + setState(() => _isConverting = true); + + try { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.trackConvertConverting)), + ); + + // Step 1: Read metadata from file (fallback to known item metadata). + final metadata = _buildFallbackMetadata(); + try { + final result = await PlatformBridge.readFileMetadata(cleanFilePath); + if (result['error'] == null) { + result.forEach((key, value) { + if (key == 'error' || value == null) return; + final normalizedValue = value.toString().trim(); + if (normalizedValue.isEmpty) return; + metadata[key.toUpperCase()] = normalizedValue; + }); + } else { + _log.w('readFileMetadata returned error, using fallback metadata'); + } + } catch (e) { + _log.w('readFileMetadata threw, using fallback metadata: $e'); + } + + // Step 2: Extract cover art to temp file + String? coverPath; + try { + final tempDir = await getTemporaryDirectory(); + final coverOutput = + '${tempDir.path}${Platform.pathSeparator}convert_cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final coverResult = await PlatformBridge.extractCoverToFile( + cleanFilePath, + coverOutput, + ); + if (coverResult['error'] == null) { + coverPath = coverOutput; + } + } catch (_) {} + + // Step 3: Handle SAF vs regular file + String workingPath = cleanFilePath; + final isSaf = _isSafFile; + String? safTempPath; + + if (isSaf) { + // Copy SAF file to temp for processing + safTempPath = await PlatformBridge.copyContentUriToTemp(cleanFilePath); + if (safTempPath == null) { + if (mounted) { + setState(() => _isConverting = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.trackConvertFailed)), + ); + } + return; + } + workingPath = safTempPath; + } + + // Step 4: Convert + final newPath = await FFmpegService.convertAudioFormat( + inputPath: workingPath, + targetFormat: targetFormat.toLowerCase(), + bitrate: bitrate, + metadata: metadata, + coverPath: coverPath, + deleteOriginal: !isSaf, // Don't delete temp copy for SAF, we handle it + ); + + // Cleanup cover temp + if (coverPath != null) { + try { + await File(coverPath).delete(); + } catch (_) {} + } + + if (newPath == null) { + // Cleanup SAF temp if needed + if (safTempPath != null) { + try { + await File(safTempPath).delete(); + } catch (_) {} + } + if (mounted) { + setState(() => _isConverting = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.trackConvertFailed)), + ); + } + return; + } + + final newQuality = _buildConvertedQualityLabel(targetFormat, bitrate); + + // Step 5: Handle SAF write-back + if (isSaf) { + final treeUri = _downloadItem?.downloadTreeUri; + final relativeDir = _downloadItem?.safRelativeDir ?? ''; + if (treeUri == null || treeUri.isEmpty) { + try { + await File(newPath).delete(); + } catch (_) {} + if (safTempPath != null) { + try { + await File(safTempPath).delete(); + } catch (_) {} + } + if (mounted) { + setState(() => _isConverting = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.trackConvertFailed)), + ); + } + return; + } + + final oldFileName = + (_downloadItem?.safFileName != null && + _downloadItem!.safFileName!.isNotEmpty) + ? _downloadItem!.safFileName! + : _extractFileNameFromPathOrUri(cleanFilePath); + final dotIdx = oldFileName.lastIndexOf('.'); + final baseName = dotIdx > 0 + ? oldFileName.substring(0, dotIdx) + : oldFileName; + final newExt = targetFormat.toLowerCase() == 'opus' ? '.opus' : '.mp3'; + final newFileName = '$baseName$newExt'; + final mimeType = targetFormat.toLowerCase() == 'opus' + ? 'audio/opus' + : 'audio/mpeg'; + + final safUri = await PlatformBridge.createSafFileFromPath( + treeUri: treeUri, + relativeDir: relativeDir, + fileName: newFileName, + mimeType: mimeType, + srcPath: newPath, + ); + + if (safUri == null || safUri.isEmpty) { + try { + await File(newPath).delete(); + } catch (_) {} + if (safTempPath != null) { + try { + await File(safTempPath).delete(); + } catch (_) {} + } + if (mounted) { + setState(() => _isConverting = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.trackConvertFailed)), + ); + } + return; + } + + final deletedOriginal = await PlatformBridge.safDelete( + cleanFilePath, + ).catchError((_) => false); + if (deletedOriginal != true) { + _log.w('Converted SAF file created but failed deleting original URI'); + } + + // Update history with new SAF info + if (!_isLocalItem) { + await HistoryDatabase.instance.updateFilePath( + _downloadItem!.id, + safUri, + newSafFileName: newFileName, + newQuality: newQuality, + clearAudioSpecs: true, + ); + await ref.read(downloadHistoryProvider.notifier).reloadFromStorage(); + } + + // Cleanup temp files + try { + await File(newPath).delete(); + } catch (_) {} + if (safTempPath != null) { + try { + await File(safTempPath).delete(); + } catch (_) {} + } + } else { + // Regular file: update history with new path + if (!_isLocalItem) { + await HistoryDatabase.instance.updateFilePath( + _downloadItem!.id, + newPath, + newQuality: newQuality, + clearAudioSpecs: true, + ); + await ref.read(downloadHistoryProvider.notifier).reloadFromStorage(); + } + } + + if (mounted) { + setState(() => _isConverting = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.trackConvertSuccess(targetFormat)), + ), + ); + // Pop and let the caller refresh + Navigator.pop(context, true); + } + } catch (e) { + if (mounted) { + setState(() => _isConverting = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.trackSaveFailed(e.toString()))), + ); + } + } + } + + void _showEditMetadataSheet( + BuildContext context, + WidgetRef ref, + ColorScheme colorScheme, + ) async { // Read current metadata from file, fall back to item data on failure Map? fileMetadata; try { @@ -1657,8 +2492,10 @@ class _TrackMetadataScreenState extends ConsumerState { 'album': val('album', albumName), 'album_artist': val('album_artist', albumArtist), 'date': val('date', releaseDate), - 'track_number': (fileMetadata?['track_number'] ?? trackNumber ?? '').toString(), - 'disc_number': (fileMetadata?['disc_number'] ?? discNumber ?? '').toString(), + 'track_number': (fileMetadata?['track_number'] ?? trackNumber ?? '') + .toString(), + 'disc_number': (fileMetadata?['disc_number'] ?? discNumber ?? '') + .toString(), 'genre': val('genre', genre), 'isrc': val('isrc', isrc), 'label': val('label', label), @@ -1697,7 +2534,11 @@ class _TrackMetadataScreenState extends ConsumerState { } } - void _confirmDelete(BuildContext context, WidgetRef ref, ColorScheme colorScheme) { + void _confirmDelete( + BuildContext context, + WidgetRef ref, + ColorScheme colorScheme, + ) { showDialog( context: context, builder: (context) => AlertDialog( @@ -1726,16 +2567,21 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (e) { debugPrint('Failed to delete file: $e'); } - - ref.read(downloadHistoryProvider.notifier).removeFromHistory(_downloadItem!.id); + + ref + .read(downloadHistoryProvider.notifier) + .removeFromHistory(_downloadItem!.id); } - + if (context.mounted) { Navigator.pop(context); Navigator.pop(context); } }, - child: Text(context.l10n.dialogDelete, style: TextStyle(color: colorScheme.error)), + child: Text( + context.l10n.dialogDelete, + style: TextStyle(color: colorScheme.error), + ), ), ], ), @@ -1748,7 +2594,9 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString()))), + SnackBar( + content: Text(context.l10n.snackbarCannotOpenFile(e.toString())), + ), ); } } @@ -1784,31 +2632,34 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (_) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.snackbarCannotOpenFile('Failed to share file'))), + SnackBar( + content: Text( + context.l10n.snackbarCannotOpenFile('Failed to share file'), + ), + ), ); } } return; } - + await SharePlus.instance.share( - ShareParams( - files: [XFile(sharePath)], - text: shareTitle, - ), + ShareParams(files: [XFile(sharePath)], text: shareTitle), ); } String _formatFullDate(DateTime date) { return '${date.day} ${_months[date.month - 1]} ${date.year}, ' - '${date.hour.toString().padLeft(2, '0')}:' - '${date.minute.toString().padLeft(2, '0')}'; + '${date.hour.toString().padLeft(2, '0')}:' + '${date.minute.toString().padLeft(2, '0')}'; } String _formatFileSize(int bytes) { if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; - if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + if (bytes < 1024 * 1024 * 1024) { + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + } return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB'; } @@ -1936,9 +2787,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { if (result['error'] != null) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${result['error']}')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('${result['error']}'))); } setState(() => _saving = false); return; @@ -1958,30 +2809,58 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { final isOpus = lower.endsWith('.opus') || lower.endsWith('.ogg'); final vorbisMap = {}; - if (metadata['title']?.isNotEmpty == true) vorbisMap['TITLE'] = metadata['title']!; - if (metadata['artist']?.isNotEmpty == true) vorbisMap['ARTIST'] = metadata['artist']!; - if (metadata['album']?.isNotEmpty == true) vorbisMap['ALBUM'] = metadata['album']!; - if (metadata['album_artist']?.isNotEmpty == true) vorbisMap['ALBUMARTIST'] = metadata['album_artist']!; - if (metadata['date']?.isNotEmpty == true) vorbisMap['DATE'] = metadata['date']!; - if (metadata['track_number']?.isNotEmpty == true && metadata['track_number'] != '0') { + if (metadata['title']?.isNotEmpty == true) { + vorbisMap['TITLE'] = metadata['title']!; + } + if (metadata['artist']?.isNotEmpty == true) { + vorbisMap['ARTIST'] = metadata['artist']!; + } + if (metadata['album']?.isNotEmpty == true) { + vorbisMap['ALBUM'] = metadata['album']!; + } + if (metadata['album_artist']?.isNotEmpty == true) { + vorbisMap['ALBUMARTIST'] = metadata['album_artist']!; + } + if (metadata['date']?.isNotEmpty == true) { + vorbisMap['DATE'] = metadata['date']!; + } + if (metadata['track_number']?.isNotEmpty == true && + metadata['track_number'] != '0') { vorbisMap['TRACKNUMBER'] = metadata['track_number']!; } - if (metadata['disc_number']?.isNotEmpty == true && metadata['disc_number'] != '0') { + if (metadata['disc_number']?.isNotEmpty == true && + metadata['disc_number'] != '0') { vorbisMap['DISCNUMBER'] = metadata['disc_number']!; } - if (metadata['genre']?.isNotEmpty == true) vorbisMap['GENRE'] = metadata['genre']!; - if (metadata['isrc']?.isNotEmpty == true) vorbisMap['ISRC'] = metadata['isrc']!; - if (metadata['label']?.isNotEmpty == true) vorbisMap['ORGANIZATION'] = metadata['label']!; - if (metadata['copyright']?.isNotEmpty == true) vorbisMap['COPYRIGHT'] = metadata['copyright']!; - if (metadata['composer']?.isNotEmpty == true) vorbisMap['COMPOSER'] = metadata['composer']!; - if (metadata['comment']?.isNotEmpty == true) vorbisMap['COMMENT'] = metadata['comment']!; + if (metadata['genre']?.isNotEmpty == true) { + vorbisMap['GENRE'] = metadata['genre']!; + } + if (metadata['isrc']?.isNotEmpty == true) { + vorbisMap['ISRC'] = metadata['isrc']!; + } + if (metadata['label']?.isNotEmpty == true) { + vorbisMap['ORGANIZATION'] = metadata['label']!; + } + if (metadata['copyright']?.isNotEmpty == true) { + vorbisMap['COPYRIGHT'] = metadata['copyright']!; + } + if (metadata['composer']?.isNotEmpty == true) { + vorbisMap['COMPOSER'] = metadata['composer']!; + } + if (metadata['comment']?.isNotEmpty == true) { + vorbisMap['COMMENT'] = metadata['comment']!; + } // Extract existing cover art before re-embedding metadata String? existingCoverPath; try { final tempDir = await Directory.systemTemp.createTemp('cover_'); - final coverOutput = '${tempDir.path}${Platform.pathSeparator}cover.jpg'; - final coverResult = await PlatformBridge.extractCoverToFile(ffmpegTarget, coverOutput); + final coverOutput = + '${tempDir.path}${Platform.pathSeparator}cover.jpg'; + final coverResult = await PlatformBridge.extractCoverToFile( + ffmpegTarget, + coverOutput, + ); if (coverResult['error'] == null) { existingCoverPath = coverOutput; } @@ -2006,13 +2885,17 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { // Cleanup temp cover if (existingCoverPath != null) { - try { await File(existingCoverPath).delete(); } catch (_) {} + try { + await File(existingCoverPath).delete(); + } catch (_) {} } if (ffmpegResult == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Failed to save metadata via FFmpeg')), + const SnackBar( + content: Text('Failed to save metadata via FFmpeg'), + ), ); } setState(() => _saving = false); @@ -2024,7 +2907,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { final ok = await PlatformBridge.writeTempToSaf(ffmpegResult, safUri); if (!ok && mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Failed to write metadata back to storage')), + const SnackBar( + content: Text('Failed to write metadata back to storage'), + ), ); setState(() => _saving = false); return; @@ -2037,9 +2922,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to save metadata: $e')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to save metadata: $e'))); } } finally { if (mounted) setState(() => _saving = false); @@ -2093,10 +2978,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { child: CircularProgressIndicator(strokeWidth: 2), ) else - FilledButton( - onPressed: _save, - child: const Text('Save'), - ), + FilledButton(onPressed: _save, child: const Text('Save')), ], ), ), @@ -2107,6 +2989,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { controller: scrollController, padding: const EdgeInsets.symmetric(horizontal: 24), children: [ + const SizedBox(height: 6), _field('Title', _titleCtrl), _field('Artist', _artistCtrl), _field('Album', _albumCtrl), @@ -2114,9 +2997,21 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { _field('Date', _dateCtrl, hint: 'YYYY-MM-DD or YYYY'), Row( children: [ - Expanded(child: _field('Track #', _trackNumCtrl, keyboard: TextInputType.number)), + Expanded( + child: _field( + 'Track #', + _trackNumCtrl, + keyboard: TextInputType.number, + ), + ), const SizedBox(width: 12), - Expanded(child: _field('Disc #', _discNumCtrl, keyboard: TextInputType.number)), + Expanded( + child: _field( + 'Disc #', + _discNumCtrl, + keyboard: TextInputType.number, + ), + ), ], ), _field('Genre', _genreCtrl), @@ -2125,23 +3020,25 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { Padding( padding: const EdgeInsets.only(top: 8, bottom: 4), child: InkWell( - onTap: () => setState(() => _showAdvanced = !_showAdvanced), + onTap: () => + setState(() => _showAdvanced = !_showAdvanced), borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Icon( - _showAdvanced ? Icons.expand_less : Icons.expand_more, + _showAdvanced + ? Icons.expand_less + : Icons.expand_more, size: 20, color: cs.onSurfaceVariant, ), const SizedBox(width: 8), Text( 'Advanced', - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: cs.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.labelLarge + ?.copyWith(color: cs.onSurfaceVariant), ), ], ), @@ -2185,17 +3082,24 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { fillColor: cs.surfaceContainerHighest.withValues(alpha: 0.5), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: cs.outlineVariant.withValues(alpha: 0.5)), + borderSide: BorderSide( + color: cs.outlineVariant.withValues(alpha: 0.5), + ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: cs.outlineVariant.withValues(alpha: 0.5)), + borderSide: BorderSide( + color: cs.outlineVariant.withValues(alpha: 0.5), + ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: cs.primary, width: 2), ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), ), ), ); @@ -2205,6 +3109,6 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { class _MetadataItem { final String label; final String value; - + _MetadataItem(this.label, this.value); } diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index b7194bb1..81b511d2 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -56,6 +56,48 @@ class FFmpegService { return '$tempDirPath${Platform.pathSeparator}temp_embed_${timestamp}_${processId}_$_tempEmbedCounter$normalizedExt'; } + static List _buildDecryptionKeyCandidates(String rawKey) { + final candidates = []; + + void addCandidate(String key) { + final normalized = key.trim(); + if (normalized.isEmpty) return; + if (!candidates.contains(normalized)) { + candidates.add(normalized); + } + } + + final trimmed = rawKey.trim(); + if (trimmed.isEmpty) return candidates; + + addCandidate(trimmed); + + final noPrefix = trimmed.startsWith(RegExp(r'0x', caseSensitive: false)) + ? trimmed.substring(2) + : trimmed; + addCandidate(noPrefix); + + final compactHex = noPrefix.replaceAll(RegExp(r'[^0-9a-fA-F]'), ''); + if (compactHex.isNotEmpty && compactHex.length.isEven) { + addCandidate(compactHex); + } + + try { + final b64 = noPrefix.replaceAll(RegExp(r'\s+'), ''); + final decoded = base64Decode(b64); + if (decoded.isNotEmpty) { + final hex = decoded + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); + if (hex.isNotEmpty) { + addCandidate(hex); + } + } + } catch (_) {} + + return candidates; + } + static Future _execute(String command) async { try { final session = await FFmpegKit.execute(command); @@ -77,7 +119,7 @@ class FFmpegService { final outputPath = _buildOutputPath(inputPath, '.flac'); final command = - '-i "$inputPath" -c:a flac -compression_level 8 "$outputPath" -y'; + '-v error -xerror -i "$inputPath" -c:a flac -compression_level 8 "$outputPath" -y'; final result = await _execute(command); @@ -133,6 +175,111 @@ class FFmpegService { return null; } + static Future decryptAudioFile({ + required String inputPath, + required String decryptionKey, + bool deleteOriginal = true, + }) async { + final trimmedKey = decryptionKey.trim(); + if (trimmedKey.isEmpty) return inputPath; + + // Amazon encrypted streams are commonly MP4 container with FLAC audio. + // Prefer FLAC output to avoid MP4 muxing errors during decrypt copy. + final preferredExt = inputPath.toLowerCase().endsWith('.m4a') + ? '.flac' + : inputPath.toLowerCase().endsWith('.flac') + ? '.flac' + : inputPath.toLowerCase().endsWith('.mp3') + ? '.mp3' + : inputPath.toLowerCase().endsWith('.opus') + ? '.opus' + : '.flac'; + var tempOutput = _buildOutputPath(inputPath, preferredExt); + + String buildDecryptCommand( + String outputPath, { + required bool mapAudioOnly, + required String key, + }) { + final audioMap = mapAudioOnly ? '-map 0:a ' : ''; + return '-v error -decryption_key "$key" -i "$inputPath" $audioMap-c copy "$outputPath" -y'; + } + + final keyCandidates = _buildDecryptionKeyCandidates(trimmedKey); + if (keyCandidates.isEmpty) { + _log.e('No usable decryption key candidates'); + return null; + } + + FFmpegResult? lastResult; + var decryptSucceeded = false; + + for (final keyCandidate in keyCandidates) { + _log.d( + 'Executing FFmpeg decrypt command (key length: ${keyCandidate.length})', + ); + var result = await _execute( + buildDecryptCommand( + tempOutput, + mapAudioOnly: preferredExt == '.flac', + key: keyCandidate, + ), + ); + + // Fallback for uncommon streams that cannot be remuxed into FLAC. + if (!result.success && preferredExt == '.flac') { + final fallbackOutput = _buildOutputPath(inputPath, '.m4a'); + final fallbackResult = await _execute( + buildDecryptCommand( + fallbackOutput, + mapAudioOnly: false, + key: keyCandidate, + ), + ); + if (fallbackResult.success) { + tempOutput = fallbackOutput; + result = fallbackResult; + } + } + + if (result.success) { + decryptSucceeded = true; + lastResult = result; + break; + } + + try { + final tempFile = File(tempOutput); + if (await tempFile.exists()) { + await tempFile.delete(); + } + } catch (_) {} + lastResult = result; + } + + if (!decryptSucceeded) { + _log.e('FFmpeg decrypt failed: ${lastResult?.output ?? 'unknown error'}'); + return null; + } + + try { + final tempFile = File(tempOutput); + final inputFile = File(inputPath); + if (!await tempFile.exists()) { + _log.e('Decrypted output file not found: $tempOutput'); + return null; + } + + if (deleteOriginal && await inputFile.exists()) { + await inputFile.delete(); + } + return tempOutput; + } catch (e) { + _log.e('Failed to finalize decrypted file: $e'); + return null; + } + } + static Future convertFlacToMp3( String inputPath, { String bitrate = '320k', @@ -616,6 +763,97 @@ class FFmpegService { } } + /// Unified audio format conversion with full metadata + cover preservation. + /// Supports: FLAC/MP3/Opus -> MP3/Opus (any direction except same format). + /// Returns the new file path on success, null on failure. + static Future convertAudioFormat({ + required String inputPath, + required String targetFormat, + required String bitrate, + required Map metadata, + String? coverPath, + bool deleteOriginal = true, + }) async { + final format = targetFormat.toLowerCase(); + if (format != 'mp3' && format != 'opus') { + _log.e('Unsupported target format: $targetFormat'); + return null; + } + + final extension = format == 'opus' ? '.opus' : '.mp3'; + final outputPath = _buildOutputPath(inputPath, extension); + + // Step 1: Convert audio + String command; + if (format == 'opus') { + command = + '-i "$inputPath" -codec:a libopus -b:a $bitrate -vbr on -compression_level 10 -map 0:a "$outputPath" -y'; + } else { + command = + '-i "$inputPath" -codec:a libmp3lame -b:a $bitrate -map 0:a -id3v2_version 3 "$outputPath" -y'; + } + + _log.i( + 'Converting ${inputPath.split(Platform.pathSeparator).last} to $format @ $bitrate', + ); + final result = await _execute(command); + + if (!result.success) { + _log.e('Audio conversion failed: ${result.output}'); + return null; + } + + // Step 2: Embed metadata + cover into the converted file. + // Treat embed failure as conversion failure when metadata/cover was requested. + final hasMetadata = metadata.values.any((v) => v.trim().isNotEmpty); + final hasCover = coverPath != null && coverPath.trim().isNotEmpty; + if (hasMetadata || hasCover) { + String? embedResult; + if (format == 'mp3') { + embedResult = await embedMetadataToMp3( + mp3Path: outputPath, + coverPath: coverPath, + metadata: metadata, + ); + } else { + embedResult = await embedMetadataToOpus( + opusPath: outputPath, + coverPath: coverPath, + metadata: metadata, + ); + } + + if (embedResult == null) { + _log.e( + 'Metadata/Cover preservation failed, rolling back converted file', + ); + try { + final out = File(outputPath); + if (await out.exists()) { + await out.delete(); + } + } catch (e) { + _log.w('Failed to cleanup failed converted file: $e'); + } + return null; + } + } + + // Step 3: Delete original if requested + if (deleteOriginal) { + try { + await File(inputPath).delete(); + _log.i( + 'Deleted original: ${inputPath.split(Platform.pathSeparator).last}', + ); + } catch (e) { + _log.w('Failed to delete original: $e'); + } + } + + return outputPath; + } + static Map _convertToId3Tags( Map vorbisMetadata, ) { diff --git a/lib/services/history_database.dart b/lib/services/history_database.dart index 7cb314d4..1e9f91d7 100644 --- a/lib/services/history_database.dart +++ b/lib/services/history_database.dart @@ -17,21 +17,21 @@ String? _currentContainerPath; class HistoryDatabase { static final HistoryDatabase instance = HistoryDatabase._init(); static Database? _database; - + HistoryDatabase._init(); - + Future get database async { if (_database != null) return _database!; _database = await _initDB('history.db'); return _database!; } - + Future _initDB(String fileName) async { final dbPath = await getApplicationDocumentsDirectory(); final path = join(dbPath.path, fileName); - + _log.i('Initializing database at: $path'); - + return await openDatabase( path, version: 3, @@ -39,10 +39,10 @@ class HistoryDatabase { onUpgrade: _upgradeDB, ); } - + Future _createDB(Database db, int version) async { _log.i('Creating database schema v$version'); - + await db.execute(''' CREATE TABLE history ( id TEXT PRIMARY KEY, @@ -73,16 +73,20 @@ class HistoryDatabase { copyright TEXT ) '''); - + // Indexes for fast lookups await db.execute('CREATE INDEX idx_spotify_id ON history(spotify_id)'); await db.execute('CREATE INDEX idx_isrc ON history(isrc)'); - await db.execute('CREATE INDEX idx_downloaded_at ON history(downloaded_at DESC)'); - await db.execute('CREATE INDEX idx_album ON history(album_name, album_artist)'); - + await db.execute( + 'CREATE INDEX idx_downloaded_at ON history(downloaded_at DESC)', + ); + await db.execute( + 'CREATE INDEX idx_album ON history(album_name, album_artist)', + ); + _log.i('Database schema created with indexes'); } - + Future _upgradeDB(Database db, int oldVersion, int newVersion) async { _log.i('Upgrading database from v$oldVersion to v$newVersion'); if (oldVersion < 2) { @@ -95,20 +99,20 @@ class HistoryDatabase { await db.execute('ALTER TABLE history ADD COLUMN saf_repaired INTEGER'); } } - + // ==================== iOS Path Normalization ==================== - + /// Pattern to match iOS container paths /// Example: /var/mobile/Containers/Data/Application/UUID-HERE/Documents/... static final _iosContainerPattern = RegExp( r'/var/mobile/Containers/Data/Application/[A-F0-9\-]+/', caseSensitive: false, ); - + /// Initialize and cache the current iOS container path Future _initContainerPath() async { if (!Platform.isIOS || _currentContainerPath != null) return; - + try { final docDir = await getApplicationDocumentsDirectory(); // Extract container path up to and including the UUID folder @@ -122,55 +126,58 @@ class HistoryDatabase { _log.w('Failed to get iOS container path: $e'); } } - + /// Normalize iOS file path by replacing old container UUID with current one /// This fixes the issue where iOS changes container UUID after app updates String _normalizeIosPath(String? filePath) { if (filePath == null || filePath.isEmpty) return filePath ?? ''; if (!Platform.isIOS || _currentContainerPath == null) return filePath; - + // Check if path contains an iOS container path if (_iosContainerPattern.hasMatch(filePath)) { - final normalized = filePath.replaceFirst(_iosContainerPattern, _currentContainerPath!); + final normalized = filePath.replaceFirst( + _iosContainerPattern, + _currentContainerPath!, + ); if (normalized != filePath) { _log.d('Normalized iOS path: $filePath -> $normalized'); } return normalized; } - + return filePath; } - + /// Migrate iOS paths in database to use current container UUID /// This is called once after app update if container changed Future migrateIosContainerPaths() async { if (!Platform.isIOS) return false; - + await _initContainerPath(); if (_currentContainerPath == null) return false; - + final prefs = await _prefs; final lastContainer = prefs.getString('ios_last_container_path'); - + if (lastContainer == _currentContainerPath) { _log.d('iOS container path unchanged, skipping migration'); return false; } - + _log.i('iOS container changed: $lastContainer -> $_currentContainerPath'); - + try { final db = await database; - + // Get all items with iOS paths final rows = await db.query('history', columns: ['id', 'file_path']); int updatedCount = 0; final batch = db.batch(); - + for (final row in rows) { final id = row['id'] as String; final oldPath = row['file_path'] as String?; - + if (oldPath != null && _iosContainerPattern.hasMatch(oldPath)) { final newPath = _normalizeIosPath(oldPath); if (newPath != oldPath) { @@ -184,14 +191,14 @@ class HistoryDatabase { } } } - + if (updatedCount > 0) { await batch.commit(noResult: true); } - + // Save current container path await prefs.setString('ios_last_container_path', _currentContainerPath!); - + _log.i('iOS path migration complete: $updatedCount paths updated'); return updatedCount > 0; } catch (e, stack) { @@ -199,32 +206,34 @@ class HistoryDatabase { return false; } } - + /// Migrate data from SharedPreferences to SQLite /// Returns true if migration was performed, false if already migrated Future migrateFromSharedPreferences() async { final prefs = await _prefs; final migrationKey = 'history_migrated_to_sqlite'; - + if (prefs.getBool(migrationKey) == true) { _log.d('Already migrated to SQLite'); return false; } - + final jsonStr = prefs.getString('download_history'); if (jsonStr == null || jsonStr.isEmpty) { _log.d('No SharedPreferences history to migrate'); await prefs.setBool(migrationKey, true); return false; } - + try { final List jsonList = jsonDecode(jsonStr); - _log.i('Migrating ${jsonList.length} items from SharedPreferences to SQLite'); - + _log.i( + 'Migrating ${jsonList.length} items from SharedPreferences to SQLite', + ); + final db = await database; final batch = db.batch(); - + for (final json in jsonList) { final map = json as Map; batch.insert( @@ -233,20 +242,20 @@ class HistoryDatabase { conflictAlgorithm: ConflictAlgorithm.replace, ); } - + await batch.commit(noResult: true); - + // Mark as migrated but keep old data for safety await prefs.setBool(migrationKey, true); _log.i('Migration complete: ${jsonList.length} items'); - + return true; } catch (e, stack) { _log.e('Migration failed: $e', e, stack); return false; } } - + /// Convert JSON format (camelCase) to DB row (snake_case) Map _jsonToDbRow(Map json) { return { @@ -278,7 +287,7 @@ class HistoryDatabase { 'copyright': json['copyright'], }; } - + /// Convert DB row (snake_case) to JSON format (camelCase) /// Also normalizes iOS paths if container UUID changed Map _dbRowToJson(Map row) { @@ -311,9 +320,9 @@ class HistoryDatabase { 'copyright': row['copyright'], }; } - + // ==================== CRUD Operations ==================== - + /// Insert or update a history item Future upsert(Map json) async { final db = await database; @@ -323,7 +332,7 @@ class HistoryDatabase { conflictAlgorithm: ConflictAlgorithm.replace, ); } - + /// Get all history items ordered by download date (newest first) Future>> getAll({int? limit, int? offset}) async { final db = await database; @@ -335,7 +344,7 @@ class HistoryDatabase { ); return rows.map(_dbRowToJson).toList(); } - + /// Get item by ID Future?> getById(String id) async { final db = await database; @@ -348,7 +357,7 @@ class HistoryDatabase { if (rows.isEmpty) return null; return _dbRowToJson(rows.first); } - + /// Get item by Spotify ID - O(1) with index Future?> getBySpotifyId(String spotifyId) async { final db = await database; @@ -361,7 +370,7 @@ class HistoryDatabase { if (rows.isEmpty) return null; return _dbRowToJson(rows.first); } - + /// Get item by ISRC - O(1) with index Future?> getByIsrc(String isrc) async { final db = await database; @@ -374,7 +383,7 @@ class HistoryDatabase { if (rows.isEmpty) return null; return _dbRowToJson(rows.first); } - + /// Check if spotify_id exists - O(1) with index Future existsBySpotifyId(String spotifyId) async { final db = await database; @@ -384,42 +393,42 @@ class HistoryDatabase { ); return result.isNotEmpty; } - + /// Get all spotify_ids as Set for fast in-memory lookup Future> getAllSpotifyIds() async { final db = await database; final rows = await db.rawQuery( - 'SELECT spotify_id FROM history WHERE spotify_id IS NOT NULL AND spotify_id != ""' + 'SELECT spotify_id FROM history WHERE spotify_id IS NOT NULL AND spotify_id != ""', ); return rows.map((r) => r['spotify_id'] as String).toSet(); } - + /// Delete by ID Future deleteById(String id) async { final db = await database; await db.delete('history', where: 'id = ?', whereArgs: [id]); } - + /// Delete by Spotify ID Future deleteBySpotifyId(String spotifyId) async { final db = await database; await db.delete('history', where: 'spotify_id = ?', whereArgs: [spotifyId]); } - + /// Clear all history Future clearAll() async { final db = await database; await db.delete('history'); _log.i('Cleared all history'); } - + /// Get total count Future getCount() async { final db = await database; final result = await db.rawQuery('SELECT COUNT(*) as count FROM history'); return Sqflite.firstIntValue(result) ?? 0; } - + /// Find existing item by spotify_id or isrc (for deduplication) Future?> findExisting({ String? spotifyId, @@ -428,7 +437,7 @@ class HistoryDatabase { if (spotifyId != null && spotifyId.isNotEmpty) { final bySpotify = await getBySpotifyId(spotifyId); if (bySpotify != null) return bySpotify; - + // Check for deezer: prefix matching if (spotifyId.startsWith('deezer:')) { final deezerId = spotifyId.substring(7); @@ -442,31 +451,63 @@ class HistoryDatabase { if (rows.isNotEmpty) return _dbRowToJson(rows.first); } } - + if (isrc != null && isrc.isNotEmpty) { return await getByIsrc(isrc); } - + return null; } - -/// Close database + + /// Close database Future close() async { final db = await database; await db.close(); _database = null; } - + + /// Update file path for a history entry (e.g. after format conversion) + Future updateFilePath( + String id, + String newFilePath, { + String? newSafFileName, + String? newQuality, + int? newBitDepth, + int? newSampleRate, + bool clearAudioSpecs = false, + }) async { + final db = await database; + final values = {'file_path': newFilePath}; + if (newSafFileName != null) { + values['saf_file_name'] = newSafFileName; + } + if (newQuality != null) { + values['quality'] = newQuality; + } + if (clearAudioSpecs) { + values['bit_depth'] = null; + values['sample_rate'] = null; + } else { + if (newBitDepth != null) { + values['bit_depth'] = newBitDepth; + } + if (newSampleRate != null) { + values['sample_rate'] = newSampleRate; + } + } + await db.update('history', values, where: 'id = ?', whereArgs: [id]); + } + /// Get all file paths from download history /// Used to exclude downloaded files from local library scan Future> getAllFilePaths() async { final db = await database; final rows = await db.rawQuery( - 'SELECT file_path FROM history WHERE file_path IS NOT NULL AND file_path != ""' + 'SELECT file_path FROM history WHERE file_path IS NOT NULL AND file_path != ""', ); return rows.map((r) => r['file_path'] as String).toSet(); } - + /// Get all entries with file paths for orphan detection /// Returns list of (id, file_path, storage_mode, download_tree_uri, saf_relative_dir, saf_file_name) Future>> getAllEntriesWithPaths() async { @@ -478,11 +519,11 @@ class HistoryDatabase { '''); return rows.map((r) => Map.from(r)).toList(); } - + /// Delete multiple entries by IDs Future deleteByIds(List ids) async { if (ids.isEmpty) return 0; - + final db = await database; final placeholders = List.filled(ids.length, '?').join(','); final count = await db.rawDelete( diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index f87713ab..d87386b2 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -22,8 +22,7 @@ class BuiltInService { }); } -/// Default quality options for built-in services (Tidal, Qobuz, YouTube) -/// Note: Amazon is fallback-only and not shown in picker +/// Default quality options for built-in services (Tidal, Qobuz, Amazon, YouTube) /// Note: Tidal lossy (HIGH) removed - use YouTube for lossy downloads const _builtInServices = [ BuiltInService( @@ -44,6 +43,17 @@ const _builtInServices = [ QualityOption(id: 'HI_RES_LOSSLESS', label: 'Hi-Res FLAC Max', description: '24-bit / up to 192kHz'), ], ), + BuiltInService( + id: 'amazon', + label: 'Amazon', + qualityOptions: [ + QualityOption( + id: 'LOSSLESS', + label: 'FLAC Best Available', + description: 'Amazon API delivers the best available lossless quality', + ), + ], + ), BuiltInService( id: 'youtube', label: 'YouTube', diff --git a/pubspec.yaml b/pubspec.yaml index c7ce0a98..1af38e5f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music publish_to: "none" -version: 3.6.1+78 +version: 3.6.5+79 environment: sdk: ^3.10.0 From 915934e5dd6334b3ae766ac7dede44ae440b0a7c Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 00:22:48 +0700 Subject: [PATCH 013/145] fix: various improvements and fixes --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 2 +- go_backend/exports.go | 67 ++- go_backend/metadata.go | 111 +++- lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_de.dart | 3 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_es.dart | 3 + lib/l10n/app_localizations_fr.dart | 3 + lib/l10n/app_localizations_hi.dart | 3 + lib/l10n/app_localizations_id.dart | 3 + lib/l10n/app_localizations_ja.dart | 3 + lib/l10n/app_localizations_ko.dart | 3 + lib/l10n/app_localizations_nl.dart | 3 + lib/l10n/app_localizations_pt.dart | 3 + lib/l10n/app_localizations_ru.dart | 3 + lib/l10n/app_localizations_tr.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/arb/app_en.arb | 2 + lib/l10n/arb/app_id.arb | 12 +- lib/screens/track_metadata_screen.dart | 503 +++++++++++++++--- 20 files changed, 634 insertions(+), 108 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 365ad757..fc937882 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -1725,7 +1725,7 @@ class MainActivity: FlutterFragmentActivity() { val trackName = call.argument("track_name") ?: "" val artistName = call.argument("artist_name") ?: "" val spotifyId = call.argument("spotify_id") ?: "" - val durationMs = call.argument("duration_ms") ?: 0L + val durationMs = call.argument("duration_ms")?.toLong() ?: 0L val outputPath = call.argument("output_path") ?: "" val response = withContext(Dispatchers.IO) { try { diff --git a/go_backend/exports.go b/go_backend/exports.go index b09ce74c..0e2c1b5a 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "strings" "time" @@ -782,6 +783,7 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) { lower := strings.ToLower(filePath) isFlac := strings.HasSuffix(lower, ".flac") + coverPath := strings.TrimSpace(fields["cover_path"]) if isFlac { trackNum := 0 @@ -809,7 +811,7 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) { Comment: fields["comment"], } - if err := EmbedMetadata(filePath, meta, ""); err != nil { + if err := EmbedMetadata(filePath, meta, coverPath); err != nil { return "", fmt.Errorf("failed to write FLAC metadata: %w", err) } @@ -1692,19 +1694,47 @@ func ReEnrichFile(requestJSON string) (string, error) { GoLog("[ReEnrich] track=%d, disc=%d, date=%s, isrc=%s, genre=%s, label=%s\n", req.TrackNumber, req.DiscNumber, req.ReleaseDate, req.ISRC, req.Genre, req.Label) + lower := strings.ToLower(req.FilePath) + isFlac := strings.HasSuffix(lower, ".flac") + // Download cover art to temp file var coverTempPath string + var coverDataBytes []byte if req.CoverURL != "" { coverData, err := downloadCoverToMemory(req.CoverURL, req.MaxQuality) if err != nil { GoLog("[ReEnrich] Failed to download cover: %v\n", err) } else { - tmpFile, err := os.CreateTemp("", "reenrich_cover_*.jpg") - if err == nil { - coverTempPath = tmpFile.Name() - tmpFile.Write(coverData) - tmpFile.Close() - GoLog("[ReEnrich] Cover downloaded: %d KB\n", len(coverData)/1024) + coverDataBytes = coverData + GoLog("[ReEnrich] Cover downloaded: %d KB\n", len(coverData)/1024) + // MP3/Opus requires a real image file path for Dart FFmpeg. + // FLAC uses in-memory embed and does not require temp files. + if !isFlac { + tmpFile, err := os.CreateTemp("", "reenrich_cover_*.jpg") + if err != nil { + fallbackDir := filepath.Dir(req.FilePath) + if fallbackDir == "" || fallbackDir == "." { + GoLog("[ReEnrich] Failed to create cover temp file: %v\n", err) + } else { + tmpFile, err = os.CreateTemp(fallbackDir, "reenrich_cover_*.jpg") + if err != nil { + GoLog("[ReEnrich] Failed to create cover temp file (fallback dir %s): %v\n", fallbackDir, err) + } + } + } + if err == nil && tmpFile != nil { + coverTempPath = tmpFile.Name() + if _, writeErr := tmpFile.Write(coverData); writeErr != nil { + GoLog("[ReEnrich] Failed writing cover temp file: %v\n", writeErr) + tmpFile.Close() + os.Remove(coverTempPath) + coverTempPath = "" + } else if closeErr := tmpFile.Close(); closeErr != nil { + GoLog("[ReEnrich] Failed closing cover temp file: %v\n", closeErr) + os.Remove(coverTempPath) + coverTempPath = "" + } + } } } } @@ -1734,9 +1764,6 @@ func ReEnrichFile(requestJSON string) (string, error) { } } - lower := strings.ToLower(req.FilePath) - isFlac := strings.HasSuffix(lower, ".flac") - // Build enriched metadata response for Dart (includes online search results) enrichedMeta := map[string]interface{}{ "track_name": req.TrackName, @@ -1772,8 +1799,24 @@ func ReEnrichFile(requestJSON string) (string, error) { Lyrics: lyricsLRC, } - if err := EmbedMetadata(req.FilePath, metadata, coverTempPath); err != nil { - return "", fmt.Errorf("failed to embed metadata: %w", err) + if len(coverDataBytes) > 0 { + if err := EmbedMetadataWithCoverData(req.FilePath, metadata, coverDataBytes); err != nil { + return "", fmt.Errorf("failed to embed metadata with cover: %w", err) + } + } else { + if err := EmbedMetadata(req.FilePath, metadata, ""); err != nil { + return "", fmt.Errorf("failed to embed metadata: %w", err) + } + } + if len(coverDataBytes) > 0 { + embeddedCover, err := ExtractCoverArt(req.FilePath) + if err != nil || len(embeddedCover) == 0 { + if err != nil { + return "", fmt.Errorf("metadata embedded but cover verification failed: %w", err) + } + return "", fmt.Errorf("metadata embedded but cover verification failed: empty embedded cover") + } + GoLog("[ReEnrich] Cover verified after embed (%d bytes)\n", len(embeddedCover)) } GoLog("[ReEnrich] FLAC metadata embedded successfully\n") diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 370218e4..e0034dbd 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -4,8 +4,13 @@ import ( "bytes" "encoding/binary" "fmt" + stdimage "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" "io" "os" + "path/filepath" "strconv" "strings" @@ -14,6 +19,82 @@ import ( "github.com/go-flac/go-flac/v2" ) +func detectCoverMIME(coverPath string, coverData []byte) string { + // Prefer magic-byte detection over file extension. + // Some providers return non-JPEG data behind .jpg URLs. + if len(coverData) >= 8 && + coverData[0] == 0x89 && + coverData[1] == 0x50 && + coverData[2] == 0x4E && + coverData[3] == 0x47 && + coverData[4] == 0x0D && + coverData[5] == 0x0A && + coverData[6] == 0x1A && + coverData[7] == 0x0A { + return "image/png" + } + if len(coverData) >= 3 && + coverData[0] == 0xFF && + coverData[1] == 0xD8 && + coverData[2] == 0xFF { + return "image/jpeg" + } + if len(coverData) >= 6 { + header := string(coverData[:6]) + if header == "GIF87a" || header == "GIF89a" { + return "image/gif" + } + } + if len(coverData) >= 12 && + string(coverData[:4]) == "RIFF" && + string(coverData[8:12]) == "WEBP" { + return "image/webp" + } + + switch strings.ToLower(filepath.Ext(strings.TrimSpace(coverPath))) { + case ".png": + return "image/png" + case ".jpg", ".jpeg": + return "image/jpeg" + case ".webp": + return "image/webp" + case ".gif": + return "image/gif" + } + + return "image/jpeg" +} + +func buildPictureBlock(coverPath string, coverData []byte) (flac.MetaDataBlock, error) { + if len(coverData) == 0 { + return flac.MetaDataBlock{}, fmt.Errorf("empty cover data") + } + + mime := detectCoverMIME(coverPath, coverData) + picture := &flacpicture.MetadataBlockPicture{ + PictureType: flacpicture.PictureTypeFrontCover, + MIME: mime, + Description: "Front Cover", + ImageData: coverData, + } + + // Width/height/depth are optional in practice; keep zero when decode fails. + if cfg, format, err := stdimage.DecodeConfig(bytes.NewReader(coverData)); err == nil { + picture.Width = uint32(cfg.Width) + picture.Height = uint32(cfg.Height) + switch format { + case "png": + picture.ColorDepth = 32 + case "jpeg": + picture.ColorDepth = 24 + default: + picture.ColorDepth = 0 + } + } + + return picture.Marshal(), nil +} + type Metadata struct { Title string Artist string @@ -127,19 +208,12 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { } } - picture, err := flacpicture.NewFromImageData( - flacpicture.PictureTypeFrontCover, - "Front Cover", - coverData, - "image/jpeg", - ) + picBlock, err := buildPictureBlock(coverPath, coverData) if err != nil { - fmt.Printf("[Metadata] Warning: Failed to create picture block: %v\n", err) - } else { - picBlock := picture.Marshal() - f.Meta = append(f.Meta, &picBlock) - fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData)) + return fmt.Errorf("failed to create picture block: %w", err) } + f.Meta = append(f.Meta, &picBlock) + fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData)) } } else { fmt.Printf("[Metadata] Warning: Cover file does not exist: %s\n", coverPath) @@ -238,19 +312,12 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData [] } } - picture, err := flacpicture.NewFromImageData( - flacpicture.PictureTypeFrontCover, - "Front Cover", - coverData, - "image/jpeg", - ) + picBlock, err := buildPictureBlock("", coverData) if err != nil { - fmt.Printf("[Metadata] Warning: Failed to create picture block: %v\n", err) - } else { - picBlock := picture.Marshal() - f.Meta = append(f.Meta, &picBlock) - fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData)) + return fmt.Errorf("failed to create picture block: %w", err) } + f.Meta = append(f.Meta, &picBlock) + fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData)) } return f.Save(filePath) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f05056d5..34e49062 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5068,6 +5068,12 @@ abstract class AppLocalizations { /// **'Fetch and save lyrics as .lrc file'** String get trackSaveLyricsSubtitle; + /// Snackbar while saving lyrics to file + /// + /// In en, this message translates to: + /// **'Saving lyrics...'** + String get trackSaveLyricsProgress; + /// Menu action - re-embed metadata into audio file /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 61c28595..b405e5b7 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2865,6 +2865,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index bafc4f3f..f8c318ff 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2851,6 +2851,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e2c130b7..cfd26679 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2851,6 +2851,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 6c658887..d07bc20f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2851,6 +2851,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 6a3ce249..4391e763 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2851,6 +2851,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 52cf8925..ae16675d 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2869,6 +2869,9 @@ class AppLocalizationsId extends AppLocalizations { String get trackSaveLyricsSubtitle => 'Ambil dan simpan lirik sebagai file .lrc'; + @override + String get trackSaveLyricsProgress => 'Menyimpan lirik...'; + @override String get trackReEnrich => 'Perkaya Ulang Metadata'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 3e9a1b3d..23d2d69c 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2837,6 +2837,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index a74e36d4..74b97adb 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2851,6 +2851,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 52c05aa9..2d2e32cf 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2851,6 +2851,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index cea6c5fe..5d2c37a9 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2851,6 +2851,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 79438058..a491c0e3 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2897,6 +2897,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 004e1119..b5fad48e 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2866,6 +2866,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index eb41c0d9..65a694d2 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2851,6 +2851,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + @override String get trackReEnrich => 'Re-enrich Metadata'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 03e26018..ecf0e423 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2146,6 +2146,8 @@ "@trackSaveLyrics": {"description": "Menu action - save lyrics as .lrc file"}, "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", "@trackSaveLyricsSubtitle": {"description": "Subtitle for save lyrics action"}, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": {"description": "Snackbar while saving lyrics to file"}, "trackReEnrich": "Re-enrich Metadata", "@trackReEnrich": {"description": "Menu action - re-embed metadata into audio file"}, "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 43b98f13..853f24fb 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -3164,11 +3164,13 @@ "@trackSaveCoverArt": {"description": "Menu action - save album cover art as file"}, "trackSaveCoverArtSubtitle": "Simpan cover album sebagai file .jpg", "@trackSaveCoverArtSubtitle": {"description": "Subtitle for save cover art action"}, - "trackSaveLyrics": "Simpan Lirik (.lrc)", - "@trackSaveLyrics": {"description": "Menu action - save lyrics as .lrc file"}, - "trackSaveLyricsSubtitle": "Ambil dan simpan lirik sebagai file .lrc", - "@trackSaveLyricsSubtitle": {"description": "Subtitle for save lyrics action"}, - "trackReEnrich": "Perkaya Ulang Metadata", + "trackSaveLyrics": "Simpan Lirik (.lrc)", + "@trackSaveLyrics": {"description": "Menu action - save lyrics as .lrc file"}, + "trackSaveLyricsSubtitle": "Ambil dan simpan lirik sebagai file .lrc", + "@trackSaveLyricsSubtitle": {"description": "Subtitle for save lyrics action"}, + "trackSaveLyricsProgress": "Menyimpan lirik...", + "@trackSaveLyricsProgress": {"description": "Snackbar while saving lyrics to file"}, + "trackReEnrich": "Perkaya Ulang Metadata", "@trackReEnrich": {"description": "Menu action - re-embed metadata into audio file"}, "trackReEnrichSubtitle": "Tanamkan ulang metadata tanpa mengunduh ulang", "@trackReEnrichSubtitle": {"description": "Subtitle for re-enrich metadata action"}, diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 857f4508..18fa4127 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:spotiflac_android/services/cover_cache_manager.dart'; import 'package:spotiflac_android/services/history_database.dart'; import 'package:spotiflac_android/services/library_database.dart'; @@ -47,6 +48,7 @@ class _TrackMetadataScreenState extends ConsumerState { bool _isInstrumental = false; // Track if detected as instrumental bool _isConverting = false; // Track convert operation in progress Map? _editedMetadata; // Overrides after metadata edit + String? _embeddedCoverPreviewPath; final ScrollController _scrollController = ScrollController(); static final RegExp _lrcTimestampPattern = RegExp( r'^\[\d{2}:\d{2}\.\d{2,3}\]', @@ -84,6 +86,7 @@ class _TrackMetadataScreenState extends ConsumerState { @override void dispose() { + _cleanupTempFileAndParentSync(_embeddedCoverPreviewPath); _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); @@ -124,6 +127,82 @@ class _TrackMetadataScreenState extends ConsumerState { } } + bool _hasPath(String? path) => path != null && path.trim().isNotEmpty; + + Future _cleanupTempFileAndParent(String? path) async { + if (!_hasPath(path)) return; + final file = File(path!); + try { + if (await file.exists()) { + await file.delete(); + } + } catch (_) {} + try { + final dir = file.parent; + if (await dir.exists()) { + await dir.delete(recursive: true); + } + } catch (_) {} + } + + void _cleanupTempFileAndParentSync(String? path) { + if (!_hasPath(path)) return; + final file = File(path!); + try { + if (file.existsSync()) { + file.deleteSync(); + } + } catch (_) {} + try { + final dir = file.parent; + if (dir.existsSync()) { + dir.deleteSync(recursive: true); + } + } catch (_) {} + } + + Future _refreshEmbeddedCoverPreview() async { + String? newPreviewPath; + try { + if (!_fileExists) { + await _cleanupTempFileAndParent(_embeddedCoverPreviewPath); + if (mounted) { + setState(() => _embeddedCoverPreviewPath = null); + } + return; + } + final tempDir = await Directory.systemTemp.createTemp( + 'track_cover_preview_', + ); + final outputPath = + '${tempDir.path}${Platform.pathSeparator}cover_preview.jpg'; + final result = await PlatformBridge.extractCoverToFile( + cleanFilePath, + outputPath, + ); + if (result['error'] == null && await File(outputPath).exists()) { + newPreviewPath = outputPath; + } else { + try { + await tempDir.delete(recursive: true); + } catch (_) {} + } + } catch (_) {} + + final oldPreviewPath = _embeddedCoverPreviewPath; + if (!mounted) { + if (newPreviewPath != null) { + await _cleanupTempFileAndParent(newPreviewPath); + } + return; + } + + setState(() => _embeddedCoverPreviewPath = newPreviewPath); + if (oldPreviewPath != null && oldPreviewPath != newPreviewPath) { + await _cleanupTempFileAndParent(oldPreviewPath); + } + } + bool get _isLocalItem => widget.localItem != null; DownloadHistoryItem? get _downloadItem => widget.item; LocalLibraryItem? get _localLibraryItem => widget.localItem; @@ -341,7 +420,13 @@ class _TrackMetadataScreenState extends ConsumerState { fit: StackFit.expand, children: [ // Blurred cover art background - if (_coverUrl != null) + if (_hasPath(_embeddedCoverPreviewPath)) + Image.file( + File(_embeddedCoverPreviewPath!), + fit: BoxFit.cover, + errorBuilder: (_, _, _) => Container(color: colorScheme.surface), + ) + else if (_coverUrl != null) CachedNetworkImage( imageUrl: _coverUrl!, fit: BoxFit.cover, @@ -410,7 +495,20 @@ class _TrackMetadataScreenState extends ConsumerState { ), child: ClipRRect( borderRadius: BorderRadius.circular(20), - child: _coverUrl != null + child: _hasPath(_embeddedCoverPreviewPath) + ? Image.file( + File(_embeddedCoverPreviewPath!), + fit: BoxFit.cover, + errorBuilder: (_, _, _) => Container( + color: colorScheme.surfaceContainerHighest, + child: Icon( + Icons.music_note, + size: 64, + color: colorScheme.onSurfaceVariant, + ), + ), + ) + : _coverUrl != null ? CachedNetworkImage( imageUrl: _coverUrl!, fit: BoxFit.cover, @@ -1492,6 +1590,13 @@ class _TrackMetadataScreenState extends ConsumerState { try { final baseName = _buildSaveBaseName(); final durationMs = (duration ?? 0) * 1000; + if (mounted) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar(content: Text(context.l10n.trackSaveLyricsProgress)), + ); + } if (_isSafFile) { // SAF file: save to temp, then copy to SAF tree @@ -1509,13 +1614,15 @@ class _TrackMetadataScreenState extends ConsumerState { if (result['error'] != null) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.trackSaveFailed(result['error'].toString()), + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text( + context.l10n.trackSaveFailed(result['error'].toString()), + ), ), - ), - ); + ); } try { await Directory(tempDir.path).delete(recursive: true); @@ -1539,19 +1646,25 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (_) {} if (mounted) { if (safUri != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.trackLyricsSaved(baseName)), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.trackSaveFailed('Failed to write to storage'), + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(context.l10n.trackLyricsSaved(baseName)), ), - ), - ); + ); + } else { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text( + context.l10n.trackSaveFailed( + 'Failed to write to storage', + ), + ), + ), + ); } } } else { @@ -1559,13 +1672,15 @@ class _TrackMetadataScreenState extends ConsumerState { await Directory(tempDir.path).delete(recursive: true); } catch (_) {} if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.trackSaveFailed('No storage access'), + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text( + context.l10n.trackSaveFailed('No storage access'), + ), ), - ), - ); + ); } } return; @@ -1585,24 +1700,30 @@ class _TrackMetadataScreenState extends ConsumerState { if (mounted) { if (result['error'] != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.trackSaveFailed(result['error'].toString()), + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text( + context.l10n.trackSaveFailed(result['error'].toString()), + ), ), - ), - ); + ); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackLyricsSaved(baseName))), - ); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar(content: Text(context.l10n.trackLyricsSaved(baseName))), + ); } } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackSaveFailed(e.toString()))), - ); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar(content: Text(context.l10n.trackSaveFailed(e.toString()))), + ); } } } @@ -1662,6 +1783,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (method == 'native') { // FLAC - handled natively by Go (SAF write-back handled in Kotlin) + await _refreshEmbeddedCoverPreview(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.trackReEnrichSuccess)), @@ -1674,7 +1796,30 @@ class _TrackMetadataScreenState extends ConsumerState { final safUri = result['saf_uri'] as String?; final ffmpegTarget = tempPath ?? cleanFilePath; - final coverPath = result['cover_path'] as String?; + final downloadedCoverPath = result['cover_path'] as String?; + String? effectiveCoverPath = downloadedCoverPath; + String? extractedCoverPath; + if (!_hasPath(effectiveCoverPath)) { + try { + final tempDir = await Directory.systemTemp.createTemp( + 'reenrich_cover_', + ); + final coverOutput = + '${tempDir.path}${Platform.pathSeparator}cover.jpg'; + final extracted = await PlatformBridge.extractCoverToFile( + ffmpegTarget, + coverOutput, + ); + if (extracted['error'] == null) { + effectiveCoverPath = coverOutput; + extractedCoverPath = coverOutput; + } else { + try { + await tempDir.delete(recursive: true); + } catch (_) {} + } + } catch (_) {} + } final metadata = (result['metadata'] as Map?)?.map( (k, v) => MapEntry(k, v.toString()), ); @@ -1684,13 +1829,13 @@ class _TrackMetadataScreenState extends ConsumerState { if (lower.endsWith('.mp3')) { ffmpegResult = await FFmpegService.embedMetadataToMp3( mp3Path: ffmpegTarget, - coverPath: coverPath, + coverPath: effectiveCoverPath, metadata: metadata, ); } else if (lower.endsWith('.opus') || lower.endsWith('.ogg')) { ffmpegResult = await FFmpegService.embedMetadataToOpus( opusPath: ffmpegTarget, - coverPath: coverPath, + coverPath: effectiveCoverPath, metadata: metadata, ); } @@ -1709,11 +1854,14 @@ class _TrackMetadataScreenState extends ConsumerState { ), ); // Cleanup temp files - if (coverPath != null && coverPath.isNotEmpty) { + if (_hasPath(downloadedCoverPath)) { try { - await File(coverPath).delete(); + await File(downloadedCoverPath!).delete(); } catch (_) {} } + if (_hasPath(extractedCoverPath)) { + await _cleanupTempFileAndParent(extractedCoverPath); + } if (tempPath.isNotEmpty) { try { await File(tempPath).delete(); @@ -1730,24 +1878,28 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (_) {} } - if (mounted) { - if (ffmpegResult != null) { + if (ffmpegResult != null) { + await _refreshEmbeddedCoverPreview(); + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.trackReEnrichSuccess)), ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.trackReEnrichFfmpegFailed)), - ); } + } else if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.trackReEnrichFfmpegFailed)), + ); } // Cleanup temp cover from Go backend - if (coverPath != null && coverPath.isNotEmpty) { + if (_hasPath(downloadedCoverPath)) { try { - await File(coverPath).delete(); + await File(downloadedCoverPath!).delete(); } catch (_) {} } + if (_hasPath(extractedCoverPath)) { + await _cleanupTempFileAndParent(extractedCoverPath); + } } else { if (mounted) { final error = result['error']?.toString() ?? 'Unknown error'; @@ -2531,6 +2683,7 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (_) { setState(() {}); } + await _refreshEmbeddedCoverPreview(); } } @@ -2708,6 +2861,9 @@ class _EditMetadataSheet extends StatefulWidget { class _EditMetadataSheetState extends State<_EditMetadataSheet> { bool _saving = false; bool _showAdvanced = false; + String? _selectedCoverPath; + String? _selectedCoverTempDir; + String? _selectedCoverName; late final TextEditingController _titleCtrl; late final TextEditingController _artistCtrl; @@ -2723,6 +2879,117 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { late final TextEditingController _composerCtrl; late final TextEditingController _commentCtrl; + String _resolveImageExtension(String? ext, Uint8List? bytes) { + final normalized = (ext ?? '').toLowerCase(); + if (normalized == 'png' || + normalized == 'jpg' || + normalized == 'jpeg' || + normalized == 'webp') { + return normalized == 'jpeg' ? 'jpg' : normalized; + } + if (bytes != null && bytes.length >= 8) { + if (bytes[0] == 0x89 && + bytes[1] == 0x50 && + bytes[2] == 0x4E && + bytes[3] == 0x47) { + return 'png'; + } + if (bytes[0] == 0xFF && bytes[1] == 0xD8) { + return 'jpg'; + } + if (bytes.length >= 12 && + bytes[0] == 0x52 && + bytes[1] == 0x49 && + bytes[2] == 0x46 && + bytes[3] == 0x46 && + bytes[8] == 0x57 && + bytes[9] == 0x45 && + bytes[10] == 0x42 && + bytes[11] == 0x50) { + return 'webp'; + } + } + return 'jpg'; + } + + Future _cleanupSelectedCoverTemp() async { + final dirPath = _selectedCoverTempDir; + _selectedCoverPath = null; + _selectedCoverTempDir = null; + _selectedCoverName = null; + if (dirPath == null || dirPath.isEmpty) return; + try { + final dir = Directory(dirPath); + if (await dir.exists()) { + await dir.delete(recursive: true); + } + } catch (_) {} + } + + void _cleanupSelectedCoverTempSync() { + final dirPath = _selectedCoverTempDir; + _selectedCoverPath = null; + _selectedCoverTempDir = null; + _selectedCoverName = null; + if (dirPath == null || dirPath.isEmpty) return; + try { + final dir = Directory(dirPath); + if (dir.existsSync()) { + dir.deleteSync(recursive: true); + } + } catch (_) {} + } + + Future _pickCoverImage() async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.image, + allowMultiple: false, + withData: true, + ); + if (result == null || result.files.isEmpty) return; + + final picked = result.files.first; + final bytes = picked.bytes; + final sourcePath = picked.path; + final extension = _resolveImageExtension(picked.extension, bytes); + + final tempDir = await Directory.systemTemp.createTemp('edit_cover_'); + final tempPath = + '${tempDir.path}${Platform.pathSeparator}cover.$extension'; + + if (bytes != null && bytes.isNotEmpty) { + await File(tempPath).writeAsBytes(bytes, flush: true); + } else if (sourcePath != null && sourcePath.isNotEmpty) { + final sourceFile = File(sourcePath); + if (!await sourceFile.exists()) { + throw Exception('Selected image is not accessible'); + } + await sourceFile.copy(tempPath); + } else { + throw Exception('Unable to read selected image'); + } + + await _cleanupSelectedCoverTemp(); + if (!mounted) { + try { + await tempDir.delete(recursive: true); + } catch (_) {} + return; + } + setState(() { + _selectedCoverPath = tempPath; + _selectedCoverTempDir = tempDir.path; + _selectedCoverName = picked.name; + }); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to pick cover: $e'))); + } + } + @override void initState() { super.initState(); @@ -2744,6 +3011,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { @override void dispose() { + _cleanupSelectedCoverTempSync(); _titleCtrl.dispose(); _artistCtrl.dispose(); _albumCtrl.dispose(); @@ -2777,6 +3045,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { 'copyright': _copyrightCtrl.text, 'composer': _composerCtrl.text, 'comment': _commentCtrl.text, + 'cover_path': _selectedCoverPath ?? '', }; try { @@ -2851,21 +3120,29 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { vorbisMap['COMMENT'] = metadata['comment']!; } - // Extract existing cover art before re-embedding metadata - String? existingCoverPath; - try { - final tempDir = await Directory.systemTemp.createTemp('cover_'); - final coverOutput = - '${tempDir.path}${Platform.pathSeparator}cover.jpg'; - final coverResult = await PlatformBridge.extractCoverToFile( - ffmpegTarget, - coverOutput, - ); - if (coverResult['error'] == null) { - existingCoverPath = coverOutput; + String? existingCoverPath = _selectedCoverPath; + String? extractedCoverPath; + if (existingCoverPath == null || existingCoverPath.isEmpty) { + // Preserve current embedded cover when user does not pick a new one. + try { + final tempDir = await Directory.systemTemp.createTemp('cover_'); + final coverOutput = + '${tempDir.path}${Platform.pathSeparator}cover.jpg'; + final coverResult = await PlatformBridge.extractCoverToFile( + ffmpegTarget, + coverOutput, + ); + if (coverResult['error'] == null) { + existingCoverPath = coverOutput; + extractedCoverPath = coverOutput; + } else { + try { + await tempDir.delete(recursive: true); + } catch (_) {} + } + } catch (_) { + // No cover to preserve, continue without } - } catch (_) { - // No cover to preserve, continue without } String? ffmpegResult; @@ -2883,10 +3160,17 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ); } - // Cleanup temp cover - if (existingCoverPath != null) { + // Cleanup extracted temp cover (manual selected cover is cleaned on dispose) + if (extractedCoverPath != null && extractedCoverPath.isNotEmpty) { + final extractedFile = File(extractedCoverPath); try { - await File(existingCoverPath).delete(); + await extractedFile.delete(); + } catch (_) {} + try { + final dir = extractedFile.parent; + if (await dir.exists()) { + await dir.delete(recursive: true); + } } catch (_) {} } @@ -3016,6 +3300,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ), _field('Genre', _genreCtrl), _field('ISRC', _isrcCtrl), + _buildCoverEditor(cs), // Advanced fields toggle Padding( padding: const EdgeInsets.only(top: 8, bottom: 4), @@ -3061,6 +3346,88 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ); } + Widget _buildCoverEditor(ColorScheme cs) { + final hasSelectedCover = + _selectedCoverPath != null && _selectedCoverPath!.isNotEmpty; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.5)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Cover Art', + style: Theme.of( + context, + ).textTheme.labelLarge?.copyWith(color: cs.onSurface), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _saving ? null : _pickCoverImage, + icon: const Icon(Icons.image_outlined), + label: Text( + hasSelectedCover ? 'Replace Cover' : 'Pick Cover', + ), + ), + ), + if (hasSelectedCover) ...[ + const SizedBox(width: 8), + IconButton( + tooltip: 'Clear selected cover', + onPressed: _saving + ? null + : () async { + await _cleanupSelectedCoverTemp(); + if (!mounted) return; + setState(() {}); + }, + icon: const Icon(Icons.close), + ), + ], + ], + ), + if (hasSelectedCover) ...[ + const SizedBox(height: 8), + Text( + _selectedCoverName ?? 'Selected cover', + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.file( + File(_selectedCoverPath!), + height: 120, + width: 120, + fit: BoxFit.cover, + errorBuilder: (_, _, _) => Container( + width: 120, + height: 120, + color: cs.surfaceContainerHighest, + child: Icon(Icons.broken_image, color: cs.onSurfaceVariant), + ), + ), + ), + ], + ], + ), + ), + ); + } + Widget _field( String label, TextEditingController controller, { From 8b3b39f39003b1a62b0672f48b385ac1a093641e Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 01:13:24 +0700 Subject: [PATCH 014/145] ui: improve cover preview in edit metadata sheet and user changes - Cover preview enlarged from 120x120 to 160x160 with shadow and better styling - Layout changed from Wrap to Row with Expanded for side-by-side covers - Label moved below image with labelMedium typography - Cover editor section moved to top of edit form - Added embedded cover preview cache with LRU eviction in metadata screen - Added current cover extraction and preview in edit metadata sheet - Added metadata sync to download history after edits - Added embedded cover extraction cache in queue tab for downloaded items - Added SAF mod-time tracking for cover refresh after metadata changes --- CHANGELOG.md | 23 ++ go_backend/audio_metadata.go | 9 +- lib/providers/download_queue_provider.dart | 141 +++++-- lib/screens/queue_tab.dart | 288 ++++++++++++++- lib/screens/track_metadata_screen.dart | 407 +++++++++++++++++++-- 5 files changed, 782 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 314a9930..61557dc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ - Automatic fallback in Spotify metadata fetch path when primary source fails - Lyrics extraction now supports MP3 (ID3v2) and Opus/OGG (Vorbis comments) in addition to FLAC - Includes heuristic detection of lyrics stored in Comment fields +- Edit Metadata now supports manual cover selection (pick/replace cover image) and embeds it into audio tags on save +- Save Lyrics now shows an immediate in-progress snackbar (`Saving lyrics...`) so users know the operation has started ### Changed @@ -43,6 +45,8 @@ - Amazon now uses the new `amazon.afkarxyz.fun` API flow (ASIN-based track endpoint + legacy fallback) with encrypted stream support - Amazon ASIN extraction rewritten with robust URL/query-param parsing and regex fallback - Amazon provider re-enabled in download service picker and download settings (alongside Tidal, Qobuz, and YouTube picker flow) +- Track Metadata cover UI now refreshes from the embedded file after Edit Metadata/Re-enrich, so the displayed art matches actual file tags +- Edit Metadata cover section moved to the top of the form and now previews current embedded cover before replacement (plus selected replacement preview) ### Fixed @@ -58,6 +62,25 @@ - Added shared Go response builder for `DownloadTrack` and `DownloadWithFallback` - Success responses now consistently include `genre`, `label`, `copyright`, and `lyrics_lrc` - YouTube success response now also includes extended metadata fields (`cover_url`, `genre`, `label`, `copyright`) for parity with other providers +- Fixed `Save Lyrics` crash on Android (`java.lang.Integer cannot be cast to java.lang.Long`) by normalizing `duration_ms` channel argument as `Number -> Long` +- Fixed FLAC Re-enrich cover edge case where metadata could be written without cover when temp cover file creation failed; FLAC cover embed now uses in-memory bytes and verifies cover after write +- Fixed FLAC picture-block embed robustness by detecting image MIME via magic bytes (JPEG/PNG/GIF/WEBP) instead of relying on filename extension +- Fixed MP3/Opus metadata rewrite flows to preserve existing embedded cover when no new cover is available +- Fixed Library tab cover not updating after manual cover edit/re-embed for downloaded tracks + - Queue/Library now prefers embedded cover art extracted from local files (not just cached `coverUrl`) + - Added per-track extraction cache with file-modification invalidation so updated embedded art is reflected in Library + - Extraction is now on-demand for edited tracks only (not full-library reload) + - Returning from Track Metadata now refreshes cover cache only for the affected track + - Cover refresh is now skipped when file modification time is unchanged, removing unnecessary flash when simply opening/closing metadata screen +- Fixed repeated cover preview extraction in Track Metadata screen (`track_cover_preview_*`) causing visible flash when reopening + - Added in-memory preview cache keyed by file path so reopening metadata reuses existing preview without re-extract + - Cache validation uses file modification time for filesystem paths; SAF paths are refreshed only after successful edit actions + - Queue/Library now also compares SAF file last-modified (`getSafFileModTimes`) before refreshing embedded-cover cache + - Preview cache key is now stable per track item (not volatile temp SAF path), eliminating false cache misses on SAF-backed files + - Track Metadata no longer auto-extracts cover preview on every screen open; extraction now runs only after actual edit/re-enrich changes (or when explicitly forced) +- Track metadata edits/re-enrich now sync updated tags back into `downloadHistoryProvider` + SQLite history rows + - Non-Library screens that read download history (Home/album/history views) now reflect updated title/artist/album/tags without manual rescan + - Track Metadata back-navigation now returns an explicit update result after successful edits/re-enrich, enabling History-tab cover refresh fallback when SAF timestamps are unreliable ### Technical diff --git a/go_backend/audio_metadata.go b/go_backend/audio_metadata.go index a681c88a..e743d352 100644 --- a/go_backend/audio_metadata.go +++ b/go_backend/audio_metadata.go @@ -911,9 +911,16 @@ func parseVorbisComments(data []byte, metadata *AudioMetadata) { break } - if commentLen > 10000 { + remaining := uint32(reader.Len()) + if commentLen > remaining { break } + // Large comment entries are typically METADATA_BLOCK_PICTURE. + // Skip them so we can continue parsing normal text tags after/before. + if commentLen > 512*1024 { + reader.Seek(int64(commentLen), io.SeekCurrent) + continue + } comment := make([]byte, commentLen) if _, err := reader.Read(comment); err != nil { diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 5d5de696..4cce155a 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -151,20 +151,37 @@ class DownloadHistoryItem { ); DownloadHistoryItem copyWith({ + String? trackName, + String? artistName, + String? albumName, + String? albumArtist, + String? coverUrl, String? filePath, String? storageMode, String? downloadTreeUri, String? safRelativeDir, String? safFileName, bool? safRepaired, + String? isrc, + String? spotifyId, + int? trackNumber, + int? discNumber, + int? duration, + String? releaseDate, + String? quality, + int? bitDepth, + int? sampleRate, + String? genre, + String? label, + String? copyright, }) { return DownloadHistoryItem( id: id, - trackName: trackName, - artistName: artistName, - albumName: albumName, - albumArtist: albumArtist, - coverUrl: coverUrl, + trackName: trackName ?? this.trackName, + artistName: artistName ?? this.artistName, + albumName: albumName ?? this.albumName, + albumArtist: albumArtist ?? this.albumArtist, + coverUrl: coverUrl ?? this.coverUrl, filePath: filePath ?? this.filePath, storageMode: storageMode ?? this.storageMode, downloadTreeUri: downloadTreeUri ?? this.downloadTreeUri, @@ -173,18 +190,18 @@ class DownloadHistoryItem { safRepaired: safRepaired ?? this.safRepaired, service: service, downloadedAt: downloadedAt, - isrc: isrc, - spotifyId: spotifyId, - trackNumber: trackNumber, - discNumber: discNumber, - duration: duration, - releaseDate: releaseDate, - quality: quality, - bitDepth: bitDepth, - sampleRate: sampleRate, - genre: genre, - label: label, - copyright: copyright, + isrc: isrc ?? this.isrc, + spotifyId: spotifyId ?? this.spotifyId, + trackNumber: trackNumber ?? this.trackNumber, + discNumber: discNumber ?? this.discNumber, + duration: duration ?? this.duration, + releaseDate: releaseDate ?? this.releaseDate, + quality: quality ?? this.quality, + bitDepth: bitDepth ?? this.bitDepth, + sampleRate: sampleRate ?? this.sampleRate, + genre: genre ?? this.genre, + label: label ?? this.label, + copyright: copyright ?? this.copyright, ); } } @@ -463,6 +480,44 @@ class DownloadHistoryNotifier extends Notifier { return DownloadHistoryItem.fromJson(json); } + Future updateMetadataForItem({ + required String id, + required String trackName, + required String artistName, + required String albumName, + String? albumArtist, + String? isrc, + int? trackNumber, + int? discNumber, + String? releaseDate, + String? genre, + String? label, + String? copyright, + }) async { + final index = state.items.indexWhere((item) => item.id == id); + if (index < 0) return; + + final current = state.items[index]; + final updated = current.copyWith( + trackName: trackName, + artistName: artistName, + albumName: albumName, + albumArtist: albumArtist, + isrc: isrc, + trackNumber: trackNumber, + discNumber: discNumber, + releaseDate: releaseDate, + genre: genre, + label: label, + copyright: copyright, + ); + + final updatedItems = [...state.items]; + updatedItems[index] = updated; + state = state.copyWith(items: updatedItems); + await _db.upsert(updated.toJson()); + } + /// Remove history entries where the file no longer exists on disk /// Returns the number of orphaned entries removed Future cleanupOrphanedDownloads() async { @@ -2670,8 +2725,13 @@ class DownloadQueueNotifier extends Notifier { if (spotifyId.startsWith('spotify:track:')) { spotifyId = spotifyId.split(':').last; } - _log.d('No Deezer ID, converting from Spotify via SongLink: $spotifyId'); - final deezerData = await PlatformBridge.convertSpotifyToDeezer('track', spotifyId); + _log.d( + 'No Deezer ID, converting from Spotify via SongLink: $spotifyId', + ); + final deezerData = await PlatformBridge.convertSpotifyToDeezer( + 'track', + spotifyId, + ); // Response is TrackResponse: {"track": {"spotify_id": "deezer:XXXXX", ...}} final trackData = deezerData['track']; if (trackData is Map) { @@ -2681,20 +2741,29 @@ class DownloadQueueNotifier extends Notifier { _log.d('Found Deezer track ID via SongLink: $deezerTrackId'); } else if (deezerData['id'] != null) { deezerTrackId = deezerData['id'].toString(); - _log.d('Found Deezer track ID via SongLink (legacy): $deezerTrackId'); + _log.d( + 'Found Deezer track ID via SongLink (legacy): $deezerTrackId', + ); } // Enrich track metadata from Deezer response (release_date, isrc, etc.) - final deezerReleaseDate = _normalizeOptionalString(trackData['release_date'] as String?); - final deezerIsrc = _normalizeOptionalString(trackData['isrc'] as String?); + final deezerReleaseDate = _normalizeOptionalString( + trackData['release_date'] as String?, + ); + final deezerIsrc = _normalizeOptionalString( + trackData['isrc'] as String?, + ); final deezerTrackNum = trackData['track_number'] as int?; final deezerDiscNum = trackData['disc_number'] as int?; final needsEnrich = - (trackToDownload.releaseDate == null && deezerReleaseDate != null) || + (trackToDownload.releaseDate == null && + deezerReleaseDate != null) || (trackToDownload.isrc == null && deezerIsrc != null) || - (!_isValidISRC(trackToDownload.isrc ?? '') && deezerIsrc != null) || - (trackToDownload.trackNumber == null && deezerTrackNum != null) || + (!_isValidISRC(trackToDownload.isrc ?? '') && + deezerIsrc != null) || + (trackToDownload.trackNumber == null && + deezerTrackNum != null) || (trackToDownload.discNumber == null && deezerDiscNum != null); if (needsEnrich) { @@ -2717,7 +2786,9 @@ class DownloadQueueNotifier extends Notifier { albumType: trackToDownload.albumType, source: trackToDownload.source, ); - _log.d('Enriched track from Deezer - date: ${trackToDownload.releaseDate}, ISRC: ${trackToDownload.isrc}, track: ${trackToDownload.trackNumber}, disc: ${trackToDownload.discNumber}'); + _log.d( + 'Enriched track from Deezer - date: ${trackToDownload.releaseDate}, ISRC: ${trackToDownload.isrc}, track: ${trackToDownload.trackNumber}, disc: ${trackToDownload.discNumber}', + ); } } else if (deezerData['id'] != null) { deezerTrackId = deezerData['id'].toString(); @@ -2909,14 +2980,8 @@ class DownloadQueueNotifier extends Notifier { decryptionKey.isNotEmpty && filePath != null && actualService == 'amazon') { - _log.i( - 'Amazon encrypted stream detected, decrypting via FFmpeg...', - ); - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.9, - ); + _log.i('Amazon encrypted stream detected, decrypting via FFmpeg...'); + updateItemStatus(item.id, DownloadStatus.downloading, progress: 0.9); if (effectiveSafMode && isContentUri(filePath)) { final currentFilePath = filePath; @@ -3485,14 +3550,14 @@ class DownloadQueueNotifier extends Notifier { } // YouTube downloads: embed metadata to raw Opus/MP3 files from Cobalt - if (!wasExisting && - item.service == 'youtube' && - filePath != null) { + if (!wasExisting && item.service == 'youtube' && filePath != null) { final isOpusFile = filePath.endsWith('.opus'); final isMp3File = filePath.endsWith('.mp3'); if (isOpusFile || isMp3File) { - _log.i('YouTube download: embedding metadata to ${isOpusFile ? 'Opus' : 'MP3'} file'); + _log.i( + 'YouTube download: embedding metadata to ${isOpusFile ? 'Opus' : 'MP3'} file', + ); updateItemStatus( item.id, DownloadStatus.downloading, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index ad967413..95c886dc 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -14,6 +14,7 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/services/library_database.dart'; +import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/screens/downloaded_album_screen.dart'; import 'package:spotiflac_android/screens/local_album_screen.dart'; @@ -105,6 +106,7 @@ class _GroupedAlbum { final String albumName; final String artistName; final String? coverUrl; + final String sampleFilePath; final List tracks; final DateTime latestDownload; final String searchKey; @@ -113,6 +115,7 @@ class _GroupedAlbum { required this.albumName, required this.artistName, this.coverUrl, + required this.sampleFilePath, required this.tracks, required this.latestDownload, }) : searchKey = '${albumName.toLowerCase()}|${artistName.toLowerCase()}'; @@ -290,6 +293,10 @@ class _QueueTabState extends ConsumerState { _HistoryStats? _historyStatsCache; final Map _searchIndexCache = {}; final Map _localSearchIndexCache = {}; + final Map _downloadedEmbeddedCoverCache = {}; + final Set _pendingDownloadedCoverExtract = {}; + final Set _pendingDownloadedCoverRefresh = {}; + final Set _failedDownloadedCoverExtract = {}; Map _historyItemsById = {}; List> _historyFilterEntries = const []; Map> _filteredHistoryCache = const {}; @@ -338,6 +345,13 @@ class _QueueTabState extends ConsumerState { @override void dispose() { + for (final coverPath in _downloadedEmbeddedCoverCache.values) { + _cleanupTempCoverPathSync(coverPath); + } + _downloadedEmbeddedCoverCache.clear(); + _pendingDownloadedCoverExtract.clear(); + _pendingDownloadedCoverRefresh.clear(); + _failedDownloadedCoverExtract.clear(); for (final notifier in _fileExistsNotifiers.values) { notifier.dispose(); } @@ -405,6 +419,19 @@ class _QueueTabState extends ConsumerState { '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; return [item.id, albumKey, searchKey]; }, growable: false); + + if (historyChanged) { + final validPaths = items + .map((item) => _cleanFilePath(item.filePath)) + .where((path) => path.isNotEmpty) + .toSet(); + final staleKeys = _downloadedEmbeddedCoverCache.keys + .where((path) => !validPaths.contains(path)) + .toList(growable: false); + for (final key in staleKeys) { + _invalidateDownloadedEmbeddedCover(key); + } + } _requestFilterRefresh(); } @@ -745,6 +772,149 @@ class _QueueTabState extends ConsumerState { return filePath; } + void _cleanupTempCoverPathSync(String? coverPath) { + if (coverPath == null || coverPath.isEmpty) return; + try { + final file = File(coverPath); + if (file.existsSync()) { + file.deleteSync(); + } + final parent = file.parent; + if (parent.existsSync()) { + parent.deleteSync(recursive: true); + } + } catch (_) {} + } + + void _invalidateDownloadedEmbeddedCover(String? filePath) { + final cleanPath = _cleanFilePath(filePath); + if (cleanPath.isEmpty) return; + + final cachedPath = _downloadedEmbeddedCoverCache.remove(cleanPath); + _pendingDownloadedCoverExtract.remove(cleanPath); + _pendingDownloadedCoverRefresh.remove(cleanPath); + _failedDownloadedCoverExtract.remove(cleanPath); + _cleanupTempCoverPathSync(cachedPath); + } + + Future _readFileModTimeMillis(String? filePath) async { + final cleanPath = _cleanFilePath(filePath); + if (cleanPath.isEmpty) return null; + + if (cleanPath.startsWith('content://')) { + try { + final modTimes = await PlatformBridge.getSafFileModTimes([cleanPath]); + return modTimes[cleanPath]; + } catch (_) { + return null; + } + } + + try { + return File(cleanPath).statSync().modified.millisecondsSinceEpoch; + } catch (_) { + return null; + } + } + + Future _scheduleDownloadedEmbeddedCoverRefreshForPath( + String? filePath, { + int? beforeModTime, + bool force = false, + }) async { + final cleanPath = _cleanFilePath(filePath); + if (cleanPath.isEmpty) return; + + if (!force) { + if (beforeModTime == null) { + return; + } + final afterModTime = await _readFileModTimeMillis(cleanPath); + if (afterModTime != null && afterModTime == beforeModTime) { + return; + } + } + + _pendingDownloadedCoverRefresh.add(cleanPath); + _failedDownloadedCoverExtract.remove(cleanPath); + if (mounted) { + setState(() {}); + } + } + + String? _resolveDownloadedEmbeddedCoverPath(String? filePath) { + final cleanPath = _cleanFilePath(filePath); + if (cleanPath.isEmpty) return null; + + if (_pendingDownloadedCoverRefresh.remove(cleanPath)) { + _ensureDownloadedEmbeddedCover(cleanPath, forceRefresh: true); + } + + final cachedPath = _downloadedEmbeddedCoverCache[cleanPath]; + if (cachedPath != null) { + if (File(cachedPath).existsSync()) { + return cachedPath; + } + _downloadedEmbeddedCoverCache.remove(cleanPath); + _cleanupTempCoverPathSync(cachedPath); + } + + return null; + } + + void _ensureDownloadedEmbeddedCover( + String cleanPath, { + bool forceRefresh = false, + }) { + if (cleanPath.isEmpty) return; + if (_pendingDownloadedCoverExtract.contains(cleanPath)) return; + if (!forceRefresh && _downloadedEmbeddedCoverCache.containsKey(cleanPath)) { + return; + } + if (!forceRefresh && _failedDownloadedCoverExtract.contains(cleanPath)) { + return; + } + + _pendingDownloadedCoverExtract.add(cleanPath); + Future.microtask(() async { + String? outputPath; + try { + final tempDir = await Directory.systemTemp.createTemp('library_cover_'); + outputPath = '${tempDir.path}${Platform.pathSeparator}cover.jpg'; + final result = await PlatformBridge.extractCoverToFile( + cleanPath, + outputPath, + ); + + final hasCover = + result['error'] == null && await File(outputPath).exists(); + if (!hasCover) { + _failedDownloadedCoverExtract.add(cleanPath); + _cleanupTempCoverPathSync(outputPath); + return; + } + + if (!mounted) { + _cleanupTempCoverPathSync(outputPath); + return; + } + + final previous = _downloadedEmbeddedCoverCache[cleanPath]; + _downloadedEmbeddedCoverCache[cleanPath] = outputPath; + _failedDownloadedCoverExtract.remove(cleanPath); + if (previous != null && previous != outputPath) { + _cleanupTempCoverPathSync(previous); + } + setState(() {}); + } catch (_) { + _failedDownloadedCoverExtract.add(cleanPath); + _cleanupTempCoverPathSync(outputPath); + } finally { + _pendingDownloadedCoverExtract.remove(cleanPath); + } + }); + } + ValueListenable _fileExistsListenable(String? filePath) { if (filePath == null) return _alwaysMissingFileNotifier; final cleanPath = _cleanFilePath(filePath); @@ -1293,7 +1463,7 @@ class _QueueTabState extends ConsumerState { ); } - void _navigateToMetadataScreen(DownloadItem item) { + Future _navigateToMetadataScreen(DownloadItem item) async { final historyItem = ref .read(downloadHistoryProvider) .items @@ -1311,10 +1481,12 @@ class _QueueTabState extends ConsumerState { ), ); + final navigator = Navigator.of(context); _precacheCover(historyItem.coverUrl); _searchFocusNode.unfocus(); - Navigator.push( - context, + final beforeModTime = await _readFileModTimeMillis(historyItem.filePath); + if (!mounted) return; + final result = await navigator.push( PageRouteBuilder( transitionDuration: const Duration(milliseconds: 300), reverseTransitionDuration: const Duration(milliseconds: 250), @@ -1323,14 +1495,31 @@ class _QueueTabState extends ConsumerState { transitionsBuilder: (context, animation, secondaryAnimation, child) => FadeTransition(opacity: animation, child: child), ), - ).then((_) => _searchFocusNode.unfocus()); + ); + _searchFocusNode.unfocus(); + if (result == true) { + await _scheduleDownloadedEmbeddedCoverRefreshForPath( + historyItem.filePath, + beforeModTime: beforeModTime, + force: true, + ); + return; + } + await _scheduleDownloadedEmbeddedCoverRefreshForPath( + historyItem.filePath, + beforeModTime: beforeModTime, + ); } - void _navigateToHistoryMetadataScreen(DownloadHistoryItem item) { + Future _navigateToHistoryMetadataScreen( + DownloadHistoryItem item, + ) async { + final navigator = Navigator.of(context); _precacheCover(item.coverUrl); _searchFocusNode.unfocus(); - Navigator.push( - context, + final beforeModTime = await _readFileModTimeMillis(item.filePath); + if (!mounted) return; + final result = await navigator.push( PageRouteBuilder( transitionDuration: const Duration(milliseconds: 300), reverseTransitionDuration: const Duration(milliseconds: 250), @@ -1339,7 +1528,20 @@ class _QueueTabState extends ConsumerState { transitionsBuilder: (context, animation, secondaryAnimation, child) => FadeTransition(opacity: animation, child: child), ), - ).then((_) => _searchFocusNode.unfocus()); + ); + _searchFocusNode.unfocus(); + if (result == true) { + await _scheduleDownloadedEmbeddedCoverRefreshForPath( + item.filePath, + beforeModTime: beforeModTime, + force: true, + ); + return; + } + await _scheduleDownloadedEmbeddedCoverRefreshForPath( + item.filePath, + beforeModTime: beforeModTime, + ); } void _navigateToLocalMetadataScreen(LocalLibraryItem item) { @@ -1434,6 +1636,7 @@ class _QueueTabState extends ConsumerState { albumName: tracks.first.albumName, artistName: tracks.first.albumArtist ?? tracks.first.artistName, coverUrl: tracks.first.coverUrl, + sampleFilePath: tracks.first.filePath, tracks: tracks, latestDownload: tracks .map((t) => t.downloadedAt) @@ -2574,6 +2777,9 @@ class _QueueTabState extends ConsumerState { _GroupedAlbum album, ColorScheme colorScheme, ) { + final embeddedCoverPath = _resolveDownloadedEmbeddedCoverPath( + album.sampleFilePath, + ); return GestureDetector( onTap: () => _navigateToDownloadedAlbum(album), child: Column( @@ -2584,7 +2790,27 @@ class _QueueTabState extends ConsumerState { children: [ ClipRRect( borderRadius: BorderRadius.circular(12), - child: album.coverUrl != null + child: embeddedCoverPath != null + ? Image.file( + File(embeddedCoverPath), + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + cacheWidth: 300, + cacheHeight: 300, + errorBuilder: (context, error, stackTrace) => + Container( + color: colorScheme.surfaceContainerHighest, + child: Center( + child: Icon( + Icons.album, + color: colorScheme.onSurfaceVariant, + size: 48, + ), + ), + ), + ) + : album.coverUrl != null ? CachedNetworkImage( imageUrl: album.coverUrl!, fit: BoxFit.cover, @@ -3154,6 +3380,26 @@ class _QueueTabState extends ConsumerState { double size, ) { final isDownloaded = item.source == LibraryItemSource.downloaded; + if (isDownloaded) { + final embeddedCoverPath = _resolveDownloadedEmbeddedCoverPath( + item.filePath, + ); + if (embeddedCoverPath != null) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(embeddedCoverPath), + width: size, + height: size, + fit: BoxFit.cover, + cacheWidth: (size * 2).toInt(), + cacheHeight: (size * 2).toInt(), + errorBuilder: (context, error, stackTrace) => + _buildPlaceholderCover(colorScheme, size, isDownloaded), + ), + ); + } + } // Network URL cover (downloaded items) if (item.coverUrl != null) { @@ -3235,6 +3481,30 @@ class _QueueTabState extends ConsumerState { ColorScheme colorScheme, ) { final isDownloaded = item.source == LibraryItemSource.downloaded; + if (isDownloaded) { + final embeddedCoverPath = _resolveDownloadedEmbeddedCoverPath( + item.filePath, + ); + if (embeddedCoverPath != null) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(embeddedCoverPath), + fit: BoxFit.cover, + cacheWidth: 200, + cacheHeight: 200, + errorBuilder: (context, error, stackTrace) => Container( + color: colorScheme.surfaceContainerHighest, + child: Icon( + Icons.music_note, + color: colorScheme.onSurfaceVariant, + size: 32, + ), + ), + ), + ); + } + } // Network URL cover (downloaded items) if (item.coverUrl != null) { diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 18fa4127..730e7f37 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -20,6 +20,16 @@ import 'package:spotiflac_android/utils/logger.dart'; final _log = AppLogger('TrackMetadata'); +class _EmbeddedCoverPreviewCacheEntry { + final String previewPath; + final int? fileModTime; + + const _EmbeddedCoverPreviewCacheEntry({ + required this.previewPath, + this.fileModTime, + }); +} + class TrackMetadataScreen extends ConsumerStatefulWidget { final DownloadHistoryItem? item; final LocalLibraryItem? localItem; @@ -36,6 +46,10 @@ class TrackMetadataScreen extends ConsumerStatefulWidget { } class _TrackMetadataScreenState extends ConsumerState { + static const int _maxCoverPreviewCacheEntries = 96; + static final Map + _embeddedCoverPreviewCache = {}; + bool _fileExists = false; int? _fileSize; String? _lyrics; // Cleaned lyrics for display (no timestamps) @@ -47,6 +61,7 @@ class _TrackMetadataScreenState extends ConsumerState { bool _isEmbedding = false; // Track embed operation in progress bool _isInstrumental = false; // Track if detected as instrumental bool _isConverting = false; // Track convert operation in progress + bool _hasMetadataChanges = false; Map? _editedMetadata; // Overrides after metadata edit String? _embeddedCoverPreviewPath; final ScrollController _scrollController = ScrollController(); @@ -69,6 +84,93 @@ class _TrackMetadataScreenState extends ConsumerState { 'Dec', ]; + String get _coverCacheKey => _itemId; + + bool _isCacheTrackedPath(String? path) { + if (!_hasPath(path)) return false; + return _embeddedCoverPreviewCache.values.any( + (entry) => entry.previewPath == path, + ); + } + + bool _isVolatileSafTempPath(String path) { + if (path.isEmpty) return false; + return path.contains( + '${Platform.pathSeparator}cache${Platform.pathSeparator}saf_', + ); + } + + int? _readLocalFileModTimeMsSync(String path) { + if (path.isEmpty || isContentUri(path) || _isVolatileSafTempPath(path)) { + return null; + } + try { + return File(path).statSync().modified.millisecondsSinceEpoch; + } catch (_) { + return null; + } + } + + void _cacheEmbeddedCoverPreview( + String cacheKey, + String sourcePath, + String previewPath, + ) { + final fileModTime = _readLocalFileModTimeMsSync(sourcePath); + final existing = _embeddedCoverPreviewCache[cacheKey]; + _embeddedCoverPreviewCache[cacheKey] = _EmbeddedCoverPreviewCacheEntry( + previewPath: previewPath, + fileModTime: fileModTime, + ); + if (existing != null && existing.previewPath != previewPath) { + _cleanupTempFileAndParentSyncIfNotCached(existing.previewPath); + } + + while (_embeddedCoverPreviewCache.length > _maxCoverPreviewCacheEntries) { + final oldestKey = _embeddedCoverPreviewCache.keys.first; + final removed = _embeddedCoverPreviewCache.remove(oldestKey); + if (removed != null) { + _cleanupTempFileAndParentSyncIfNotCached(removed.previewPath); + } + } + } + + void _invalidateEmbeddedCoverPreviewCacheForPath(String cacheKey) { + if (cacheKey.isEmpty) return; + final removed = _embeddedCoverPreviewCache.remove(cacheKey); + if (removed != null) { + _cleanupTempFileAndParentSyncIfNotCached(removed.previewPath); + } + } + + String? _getCachedEmbeddedCoverPreviewPathIfValid( + String cacheKey, + String sourcePath, + ) { + if (cacheKey.isEmpty) return null; + final cached = _embeddedCoverPreviewCache[cacheKey]; + if (cached == null) return null; + + final previewFile = File(cached.previewPath); + if (!previewFile.existsSync()) { + _embeddedCoverPreviewCache.remove(cacheKey); + return null; + } + + if (!isContentUri(sourcePath) && !_isVolatileSafTempPath(sourcePath)) { + final currentModTime = _readLocalFileModTimeMsSync(sourcePath); + if (currentModTime != null && + cached.fileModTime != null && + currentModTime != cached.fileModTime) { + _embeddedCoverPreviewCache.remove(cacheKey); + _cleanupTempFileAndParentSyncIfNotCached(cached.previewPath); + return null; + } + } + + return cached.previewPath; + } + String? _normalizeOptionalString(String? value) { if (value == null) return null; final trimmed = value.trim(); @@ -86,7 +188,7 @@ class _TrackMetadataScreenState extends ConsumerState { @override void dispose() { - _cleanupTempFileAndParentSync(_embeddedCoverPreviewPath); + _cleanupTempFileAndParentSyncIfNotCached(_embeddedCoverPreviewPath); _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); @@ -125,6 +227,15 @@ class _TrackMetadataScreenState extends ConsumerState { if (mounted && exists && _lyrics == null && !_lyricsLoading) { _fetchLyrics(); } + if (mounted && exists && !_hasPath(_embeddedCoverPreviewPath)) { + final cachedPath = _getCachedEmbeddedCoverPreviewPathIfValid( + _coverCacheKey, + cleanFilePath, + ); + if (_hasPath(cachedPath)) { + setState(() => _embeddedCoverPreviewPath = cachedPath); + } + } } bool _hasPath(String? path) => path != null && path.trim().isNotEmpty; @@ -145,6 +256,11 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (_) {} } + Future _cleanupTempFileAndParentIfNotCached(String? path) async { + if (_isCacheTrackedPath(path)) return; + await _cleanupTempFileAndParent(path); + } + void _cleanupTempFileAndParentSync(String? path) { if (!_hasPath(path)) return; final file = File(path!); @@ -161,27 +277,52 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (_) {} } - Future _refreshEmbeddedCoverPreview() async { + void _cleanupTempFileAndParentSyncIfNotCached(String? path) { + if (_isCacheTrackedPath(path)) return; + _cleanupTempFileAndParentSync(path); + } + + Future _refreshEmbeddedCoverPreview({bool force = false}) async { + final cacheKey = _coverCacheKey; + final sourcePath = cleanFilePath; + if (!force) { + final cachedPath = _getCachedEmbeddedCoverPreviewPathIfValid( + cacheKey, + sourcePath, + ); + if (_hasPath(cachedPath)) { + if (mounted && _embeddedCoverPreviewPath != cachedPath) { + setState(() => _embeddedCoverPreviewPath = cachedPath); + } + return; + } + } + String? newPreviewPath; try { if (!_fileExists) { - await _cleanupTempFileAndParent(_embeddedCoverPreviewPath); + _invalidateEmbeddedCoverPreviewCacheForPath(cacheKey); + await _cleanupTempFileAndParentIfNotCached(_embeddedCoverPreviewPath); if (mounted) { setState(() => _embeddedCoverPreviewPath = null); } return; } + if (force) { + _invalidateEmbeddedCoverPreviewCacheForPath(cacheKey); + } final tempDir = await Directory.systemTemp.createTemp( 'track_cover_preview_', ); final outputPath = '${tempDir.path}${Platform.pathSeparator}cover_preview.jpg'; final result = await PlatformBridge.extractCoverToFile( - cleanFilePath, + sourcePath, outputPath, ); if (result['error'] == null && await File(outputPath).exists()) { newPreviewPath = outputPath; + _cacheEmbeddedCoverPreview(cacheKey, sourcePath, outputPath); } else { try { await tempDir.delete(recursive: true); @@ -192,14 +333,14 @@ class _TrackMetadataScreenState extends ConsumerState { final oldPreviewPath = _embeddedCoverPreviewPath; if (!mounted) { if (newPreviewPath != null) { - await _cleanupTempFileAndParent(newPreviewPath); + await _cleanupTempFileAndParentIfNotCached(newPreviewPath); } return; } setState(() => _embeddedCoverPreviewPath = newPreviewPath); if (oldPreviewPath != null && oldPreviewPath != newPreviewPath) { - await _cleanupTempFileAndParent(oldPreviewPath); + await _cleanupTempFileAndParentIfNotCached(oldPreviewPath); } } @@ -292,6 +433,14 @@ class _TrackMetadataScreenState extends ConsumerState { return path.startsWith('EXISTS:') ? path.substring(7) : path; } + void _markMetadataChanged() { + _hasMetadataChanges = true; + } + + void _popWithMetadataResult() { + Navigator.pop(context, _hasMetadataChanges ? true : null); + } + @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; @@ -354,7 +503,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), child: Icon(Icons.arrow_back, color: colorScheme.onSurface), ), - onPressed: () => Navigator.pop(context), + onPressed: _popWithMetadataResult, ), actions: [ IconButton( @@ -1783,7 +1932,9 @@ class _TrackMetadataScreenState extends ConsumerState { if (method == 'native') { // FLAC - handled natively by Go (SAF write-back handled in Kotlin) - await _refreshEmbeddedCoverPreview(); + await _refreshEmbeddedCoverPreview(force: true); + _markMetadataChanged(); + await _syncDownloadHistoryMetadata(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.trackReEnrichSuccess)), @@ -1879,7 +2030,9 @@ class _TrackMetadataScreenState extends ConsumerState { } if (ffmpegResult != null) { - await _refreshEmbeddedCoverPreview(); + await _refreshEmbeddedCoverPreview(force: true); + _markMetadataChanged(); + await _syncDownloadHistoryMetadata(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.trackReEnrichSuccess)), @@ -1917,6 +2070,38 @@ class _TrackMetadataScreenState extends ConsumerState { } } + Future _syncDownloadHistoryMetadata() async { + if (_isLocalItem || _downloadItem == null) return; + + String? normalizedOrNull(String? value) { + if (value == null) return null; + final trimmed = value.trim(); + if (trimmed.isEmpty) return null; + return trimmed; + } + + try { + await ref + .read(downloadHistoryProvider.notifier) + .updateMetadataForItem( + id: _downloadItem!.id, + trackName: trackName, + artistName: artistName, + albumName: albumName, + albumArtist: normalizedOrNull(albumArtist), + isrc: normalizedOrNull(isrc), + trackNumber: trackNumber, + discNumber: discNumber, + releaseDate: normalizedOrNull(releaseDate), + genre: normalizedOrNull(genre), + label: normalizedOrNull(label), + copyright: normalizedOrNull(copyright), + ); + } catch (e) { + _log.w('Failed to sync download history metadata: $e'); + } + } + String _cleanLrcForDisplay(String lrc) { final lines = lrc.split('\n'); final cleanLines = []; @@ -2683,7 +2868,9 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (_) { setState(() {}); } - await _refreshEmbeddedCoverPreview(); + await _refreshEmbeddedCoverPreview(force: true); + _markMetadataChanged(); + await _syncDownloadHistoryMetadata(); } } @@ -2864,6 +3051,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { String? _selectedCoverPath; String? _selectedCoverTempDir; String? _selectedCoverName; + String? _currentCoverPath; + String? _currentCoverTempDir; + bool _loadingCurrentCover = false; late final TextEditingController _titleCtrl; late final TextEditingController _artistCtrl; @@ -2879,6 +3069,8 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { late final TextEditingController _composerCtrl; late final TextEditingController _commentCtrl; + bool _hasValue(String? value) => value != null && value.trim().isNotEmpty; + String _resolveImageExtension(String? ext, Uint8List? bytes) { final normalized = (ext ?? '').toLowerCase(); if (normalized == 'png' || @@ -2940,6 +3132,72 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } catch (_) {} } + void _cleanupCurrentCoverTempSync() { + final dirPath = _currentCoverTempDir; + _currentCoverPath = null; + _currentCoverTempDir = null; + if (dirPath == null || dirPath.isEmpty) return; + try { + final dir = Directory(dirPath); + if (dir.existsSync()) { + dir.deleteSync(recursive: true); + } + } catch (_) {} + } + + Future _loadCurrentCoverPreview() async { + if (_loadingCurrentCover) return; + setState(() => _loadingCurrentCover = true); + String? newCoverPath; + String? newCoverDir; + try { + final tempDir = await Directory.systemTemp.createTemp( + 'edit_existing_cover_', + ); + final coverOutput = + '${tempDir.path}${Platform.pathSeparator}existing_cover.jpg'; + final coverResult = await PlatformBridge.extractCoverToFile( + widget.filePath, + coverOutput, + ); + if (coverResult['error'] == null && await File(coverOutput).exists()) { + newCoverPath = coverOutput; + newCoverDir = tempDir.path; + } else { + try { + await tempDir.delete(recursive: true); + } catch (_) {} + } + } catch (_) {} + + if (!mounted) { + if (newCoverDir != null) { + try { + final dir = Directory(newCoverDir); + if (await dir.exists()) { + await dir.delete(recursive: true); + } + } catch (_) {} + } + return; + } + + final oldDir = _currentCoverTempDir; + setState(() { + _currentCoverPath = newCoverPath; + _currentCoverTempDir = newCoverDir; + _loadingCurrentCover = false; + }); + if (oldDir != null && oldDir.isNotEmpty && oldDir != newCoverDir) { + try { + final dir = Directory(oldDir); + if (await dir.exists()) { + await dir.delete(recursive: true); + } + } catch (_) {} + } + } + Future _pickCoverImage() async { try { final result = await FilePicker.platform.pickFiles( @@ -3007,11 +3265,13 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { _copyrightCtrl = TextEditingController(text: v['copyright'] ?? ''); _composerCtrl = TextEditingController(text: v['composer'] ?? ''); _commentCtrl = TextEditingController(text: v['comment'] ?? ''); + _loadCurrentCoverPreview(); } @override void dispose() { _cleanupSelectedCoverTempSync(); + _cleanupCurrentCoverTempSync(); _titleCtrl.dispose(); _artistCtrl.dispose(); _albumCtrl.dispose(); @@ -3120,7 +3380,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { vorbisMap['COMMENT'] = metadata['comment']!; } - String? existingCoverPath = _selectedCoverPath; + String? existingCoverPath = _selectedCoverPath ?? _currentCoverPath; String? extractedCoverPath; if (existingCoverPath == null || existingCoverPath.isEmpty) { // Preserve current embedded cover when user does not pick a new one. @@ -3274,6 +3534,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { padding: const EdgeInsets.symmetric(horizontal: 24), children: [ const SizedBox(height: 6), + _buildCoverEditor(cs), _field('Title', _titleCtrl), _field('Artist', _artistCtrl), _field('Album', _albumCtrl), @@ -3300,7 +3561,6 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ), _field('Genre', _genreCtrl), _field('ISRC', _isrcCtrl), - _buildCoverEditor(cs), // Advanced fields toggle Padding( padding: const EdgeInsets.only(top: 8, bottom: 4), @@ -3347,8 +3607,8 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } Widget _buildCoverEditor(ColorScheme cs) { - final hasSelectedCover = - _selectedCoverPath != null && _selectedCoverPath!.isNotEmpty; + final hasSelectedCover = _hasValue(_selectedCoverPath); + final hasCurrentCover = _hasValue(_currentCoverPath); return Padding( padding: const EdgeInsets.only(bottom: 12), child: Container( @@ -3367,6 +3627,16 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { context, ).textTheme.labelLarge?.copyWith(color: cs.onSurface), ), + const SizedBox(height: 6), + if (_loadingCurrentCover) + const LinearProgressIndicator(minHeight: 2) + else if (!hasCurrentCover) + Text( + 'No embedded album art found', + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant), + ), const SizedBox(height: 8), Row( children: [ @@ -3395,32 +3665,39 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ], ], ), - if (hasSelectedCover) ...[ - const SizedBox(height: 8), - Text( - _selectedCoverName ?? 'Selected cover', - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant), - maxLines: 1, - overflow: TextOverflow.ellipsis, + if (hasCurrentCover || hasSelectedCover) ...[ + const SizedBox(height: 12), + Row( + children: [ + if (hasCurrentCover) + Expanded( + child: _buildCoverPreviewTile( + cs: cs, + path: _currentCoverPath!, + label: 'Current cover', + ), + ), + if (hasCurrentCover && hasSelectedCover) + const SizedBox(width: 12), + if (hasSelectedCover) + Expanded( + child: _buildCoverPreviewTile( + cs: cs, + path: _selectedCoverPath!, + label: _selectedCoverName ?? 'Selected cover', + ), + ), + ], ), - const SizedBox(height: 8), - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.file( - File(_selectedCoverPath!), - height: 120, - width: 120, - fit: BoxFit.cover, - errorBuilder: (_, _, _) => Container( - width: 120, - height: 120, - color: cs.surfaceContainerHighest, - child: Icon(Icons.broken_image, color: cs.onSurfaceVariant), - ), + if (hasSelectedCover) ...[ + const SizedBox(height: 8), + Text( + 'The selected cover will replace the current embedded cover when you tap Save.', + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant), ), - ), + ], ], ], ), @@ -3428,6 +3705,60 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ); } + Widget _buildCoverPreviewTile({ + required ColorScheme cs, + required String path, + required String label, + }) { + return Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: cs.shadow.withValues(alpha: 0.15), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.file( + File(path), + height: 160, + width: 160, + fit: BoxFit.cover, + errorBuilder: (_, _, _) => Container( + width: 160, + height: 160, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.broken_image, + color: cs.onSurfaceVariant, + size: 32, + ), + ), + ), + ), + ), + const SizedBox(height: 8), + Text( + label, + style: Theme.of( + context, + ).textTheme.labelMedium?.copyWith(color: cs.onSurfaceVariant), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ); + } + Widget _field( String label, TextEditingController controller, { From ca4c2a661e8c9e3c3e9e198ead5e70b644664ff1 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 01:44:05 +0700 Subject: [PATCH 015/145] perf: memory and rebuild optimizations across app - Bound Deezer cache with LRU eviction and periodic cleanup - Configure Flutter image cache limits (240 entries / 60 MiB) - Add ResizeImage wrapper for precacheImage calls - Add memCacheWidth/cacheWidth to cover images across screens - Add DownloadedEmbeddedCoverResolver as centralized cover service - Throttle download progress notifications with dedup checks - Normalize progress/speed/bytes values to reduce UI rebuilds - Optimize queue list with per-item ConsumerWidget and RepaintBoundary - Preserve derived indexes in LocalLibraryState.copyWith - Skip non-error logs when detailed logging disabled - Use async file stat and early-break loops in queue filters --- go_backend/deezer.go | 144 +++++++++-- lib/main.dart | 9 + lib/providers/download_queue_provider.dart | 98 +++++++- lib/providers/local_library_provider.dart | 68 +++-- lib/screens/album_screen.dart | 8 + lib/screens/downloaded_album_screen.dart | 90 ++++++- lib/screens/home_tab.dart | 144 ++++++++--- lib/screens/playlist_screen.dart | 8 + lib/screens/queue_tab.dart | 145 ++++++++--- lib/screens/search_screen.dart | 47 ++-- lib/screens/settings/about_page.dart | 2 + lib/screens/track_metadata_screen.dart | 22 +- .../downloaded_embedded_cover_resolver.dart | 235 ++++++++++++++++++ lib/utils/logger.dart | 11 +- 14 files changed, 891 insertions(+), 140 deletions(-) create mode 100644 lib/services/downloaded_embedded_cover_resolver.dart diff --git a/go_backend/deezer.go b/go_backend/deezer.go index dfddb890..09254fc2 100644 --- a/go_backend/deezer.go +++ b/go_backend/deezer.go @@ -28,15 +28,23 @@ const ( deezerAPITimeoutMobile = 25 * time.Second deezerMaxRetries = 2 deezerRetryDelay = 500 * time.Millisecond + + deezerMaxSearchCacheEntries = 300 + deezerMaxAlbumCacheEntries = 200 + deezerMaxArtistCacheEntries = 200 + deezerMaxISRCCacheEntries = 4000 + deezerCacheCleanupInterval = 5 * time.Minute ) type DeezerClient struct { - httpClient *http.Client - searchCache map[string]*cacheEntry - albumCache map[string]*cacheEntry - artistCache map[string]*cacheEntry - isrcCache map[string]string - cacheMu sync.RWMutex + httpClient *http.Client + searchCache map[string]*cacheEntry + albumCache map[string]*cacheEntry + artistCache map[string]*cacheEntry + isrcCache map[string]string + cacheMu sync.RWMutex + lastCacheCleanup time.Time + cacheCleanupInterval time.Duration } var ( @@ -47,16 +55,111 @@ var ( func GetDeezerClient() *DeezerClient { deezerClientOnce.Do(func() { deezerClient = &DeezerClient{ - httpClient: NewMetadataHTTPClient(deezerAPITimeoutMobile), - searchCache: make(map[string]*cacheEntry), - albumCache: make(map[string]*cacheEntry), - artistCache: make(map[string]*cacheEntry), - isrcCache: make(map[string]string), + httpClient: NewMetadataHTTPClient(deezerAPITimeoutMobile), + searchCache: make(map[string]*cacheEntry), + albumCache: make(map[string]*cacheEntry), + artistCache: make(map[string]*cacheEntry), + isrcCache: make(map[string]string), + cacheCleanupInterval: deezerCacheCleanupInterval, } }) return deezerClient } +func (c *DeezerClient) pruneExpiredCacheEntriesLocked( + cache map[string]*cacheEntry, + now time.Time, +) { + for key, entry := range cache { + if entry == nil || now.After(entry.expiresAt) { + delete(cache, key) + } + } +} + +func (c *DeezerClient) trimCacheEntriesLocked( + cache map[string]*cacheEntry, + maxEntries int, +) { + if maxEntries <= 0 || len(cache) <= maxEntries { + return + } + + for len(cache) > maxEntries { + var oldestKey string + var oldestExpiry time.Time + first := true + for key, entry := range cache { + expiry := time.Time{} + if entry != nil { + expiry = entry.expiresAt + } + if first || expiry.Before(oldestExpiry) { + first = false + oldestKey = key + oldestExpiry = expiry + } + } + if oldestKey == "" { + return + } + delete(cache, oldestKey) + } +} + +func (c *DeezerClient) trimStringCacheEntriesLocked( + cache map[string]string, + maxEntries int, +) { + if maxEntries <= 0 || len(cache) <= maxEntries { + return + } + + toRemove := len(cache) - maxEntries + for key := range cache { + delete(cache, key) + toRemove-- + if toRemove <= 0 { + return + } + } +} + +func (c *DeezerClient) maybeCleanupCachesLocked(now time.Time) { + periodicCleanupDue := c.cacheCleanupInterval > 0 && + (c.lastCacheCleanup.IsZero() || + now.Sub(c.lastCacheCleanup) >= c.cacheCleanupInterval) + + if periodicCleanupDue { + c.pruneExpiredCacheEntriesLocked(c.searchCache, now) + c.pruneExpiredCacheEntriesLocked(c.albumCache, now) + c.pruneExpiredCacheEntriesLocked(c.artistCache, now) + c.lastCacheCleanup = now + } + + if len(c.searchCache) > deezerMaxSearchCacheEntries { + if !periodicCleanupDue { + c.pruneExpiredCacheEntriesLocked(c.searchCache, now) + } + c.trimCacheEntriesLocked(c.searchCache, deezerMaxSearchCacheEntries) + } + if len(c.albumCache) > deezerMaxAlbumCacheEntries { + if !periodicCleanupDue { + c.pruneExpiredCacheEntriesLocked(c.albumCache, now) + } + c.trimCacheEntriesLocked(c.albumCache, deezerMaxAlbumCacheEntries) + } + if len(c.artistCache) > deezerMaxArtistCacheEntries { + if !periodicCleanupDue { + c.pruneExpiredCacheEntriesLocked(c.artistCache, now) + } + c.trimCacheEntriesLocked(c.artistCache, deezerMaxArtistCacheEntries) + } + if len(c.isrcCache) > deezerMaxISRCCacheEntries { + c.trimStringCacheEntriesLocked(c.isrcCache, deezerMaxISRCCacheEntries) + } +} + type deezerTrack struct { ID int64 `json:"id"` Title string `json:"title"` @@ -414,10 +517,12 @@ func (c *DeezerClient) SearchAll(ctx context.Context, query string, trackLimit, GoLog("[Deezer] SearchAll complete: %d tracks, %d artists, %d albums, %d playlists\n", len(result.Tracks), len(result.Artists), len(result.Albums), len(result.Playlists)) c.cacheMu.Lock() + now := time.Now() c.searchCache[cacheKey] = &cacheEntry{ data: result, - expiresAt: time.Now().Add(deezerCacheTTL), + expiresAt: now.Add(deezerCacheTTL), } + c.maybeCleanupCachesLocked(now) c.cacheMu.Unlock() return result, nil @@ -555,10 +660,12 @@ func (c *DeezerClient) GetAlbum(ctx context.Context, albumID string) (*AlbumResp } c.cacheMu.Lock() + now := time.Now() c.albumCache[albumID] = &cacheEntry{ data: result, - expiresAt: time.Now().Add(deezerCacheTTL), + expiresAt: now.Add(deezerCacheTTL), } + c.maybeCleanupCachesLocked(now) c.cacheMu.Unlock() return result, nil @@ -638,10 +745,12 @@ func (c *DeezerClient) GetArtist(ctx context.Context, artistID string) (*ArtistR } c.cacheMu.Lock() + now := time.Now() c.artistCache[artistID] = &cacheEntry{ data: result, - expiresAt: time.Now().Add(deezerCacheTTL), + expiresAt: now.Add(deezerCacheTTL), } + c.maybeCleanupCachesLocked(now) c.cacheMu.Unlock() return result, nil @@ -807,6 +916,7 @@ func (c *DeezerClient) fetchISRCsParallel(ctx context.Context, tracks []deezerTr for trackIDStr, isrc := range directISRCs { c.isrcCache[trackIDStr] = isrc } + c.maybeCleanupCachesLocked(time.Now()) c.cacheMu.Unlock() } @@ -841,6 +951,7 @@ func (c *DeezerClient) fetchISRCsParallel(ctx context.Context, tracks []deezerTr c.cacheMu.Lock() c.isrcCache[trackIDStr] = fullTrack.ISRC + c.maybeCleanupCachesLocked(time.Now()) c.cacheMu.Unlock() }(track) } @@ -864,6 +975,7 @@ func (c *DeezerClient) GetTrackISRC(ctx context.Context, trackID string) (string c.cacheMu.Lock() c.isrcCache[trackID] = fullTrack.ISRC + c.maybeCleanupCachesLocked(time.Now()) c.cacheMu.Unlock() return fullTrack.ISRC, nil @@ -946,10 +1058,12 @@ func (c *DeezerClient) GetAlbumExtendedMetadata(ctx context.Context, albumID str } c.cacheMu.Lock() + now := time.Now() c.searchCache[cacheKey] = &cacheEntry{ data: result, - expiresAt: time.Now().Add(deezerCacheTTL), + expiresAt: now.Add(deezerCacheTTL), } + c.maybeCleanupCachesLocked(now) c.cacheMu.Unlock() GoLog("[Deezer] Album metadata fetched - Genre: %s, Label: %s\n", result.Genre, result.Label) diff --git a/lib/main.dart b/lib/main.dart index c2226f63..2e19a7a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,12 +11,21 @@ import 'package:spotiflac_android/services/cover_cache_manager.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + _configureImageCache(); runApp( ProviderScope(child: const _EagerInitialization(child: SpotiFLACApp())), ); } +void _configureImageCache() { + final imageCache = PaintingBinding.instance.imageCache; + // Keep memory cache bounded so cover-heavy pages don't retain too many + // full-resolution images simultaneously. + imageCache.maximumSize = 240; + imageCache.maximumSizeBytes = 60 << 20; // 60 MiB +} + /// Widget to eagerly initialize providers that need to load data on startup class _EagerInitialization extends ConsumerStatefulWidget { const _EagerInitialization({required this.child}); diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 4cce155a..9a74729c 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -673,6 +673,7 @@ class DownloadQueueNotifier extends Notifier { static const _queueStorageKey = 'download_queue'; static const _progressPollingInterval = Duration(milliseconds: 800); static const _queueSchedulingInterval = Duration(milliseconds: 250); + static const _bytesUiStep = 104857; // ~0.1 MiB, matches one-decimal MB UI. final NotificationService _notificationService = NotificationService(); final Future _prefs = SharedPreferences.getInstance(); int _totalQueuedAtStart = 0; @@ -686,6 +687,55 @@ class DownloadQueueNotifier extends Notifier { int _lastServicePercent = -1; int _lastServiceQueueCount = -1; DateTime _lastServiceUpdateAt = DateTime.fromMillisecondsSinceEpoch(0); + String? _lastFinalizingTrackName; + String? _lastFinalizingArtistName; + String? _lastNotifTrackName; + String? _lastNotifArtistName; + int _lastNotifPercent = -1; + int _lastNotifQueueCount = -1; + + double _normalizeProgressForUi(double value) { + final clamped = value.clamp(0.0, 1.0).toDouble(); + if (clamped <= 0) return 0; + if (clamped >= 1) return 1; + final rounded = double.parse(clamped.toStringAsFixed(2)); + return rounded == 0 ? 0.01 : rounded; + } + + double _normalizeSpeedForUi(double value) { + if (value <= 0) return 0; + return double.parse(value.toStringAsFixed(1)); + } + + int _normalizeBytesForUi(int value) { + if (value <= 0) return 0; + return (value ~/ _bytesUiStep) * _bytesUiStep; + } + + bool _shouldUpdateProgressNotification({ + required String trackName, + required String artistName, + required int progress, + required int total, + required int queueCount, + }) { + final safeTotal = total > 0 ? total : 1; + final percent = ((progress * 100) / safeTotal).round().clamp(0, 100); + final changed = + trackName != _lastNotifTrackName || + artistName != _lastNotifArtistName || + percent != _lastNotifPercent || + queueCount != _lastNotifQueueCount; + if (!changed) { + return false; + } + + _lastNotifTrackName = trackName; + _lastNotifArtistName = artistName; + _lastNotifPercent = percent; + _lastNotifQueueCount = queueCount; + return true; + } @override DownloadQueueState build() { @@ -854,12 +904,15 @@ class DownloadQueueNotifier extends Notifier { } else { percentage = progressFromBackend; } + final normalizedProgress = _normalizeProgressForUi(percentage); + final normalizedSpeed = _normalizeSpeedForUi(speedMBps); + final normalizedBytes = _normalizeBytesForUi(bytesReceived); progressUpdates[itemId] = _ProgressUpdate( status: DownloadStatus.downloading, - progress: percentage, - speedMBps: speedMBps, - bytesReceived: bytesReceived, + progress: normalizedProgress, + speedMBps: normalizedSpeed, + bytesReceived: normalizedBytes, ); final mbReceived = bytesReceived / (1024 * 1024); @@ -914,12 +967,20 @@ class DownloadQueueNotifier extends Notifier { } if (hasFinalizingItem && finalizingTrackName != null) { - _notificationService.showDownloadFinalizing( - trackName: finalizingTrackName, - artistName: finalizingArtistName ?? '', - ); + final safeArtistName = finalizingArtistName ?? ''; + if (finalizingTrackName != _lastFinalizingTrackName || + safeArtistName != _lastFinalizingArtistName) { + _notificationService.showDownloadFinalizing( + trackName: finalizingTrackName, + artistName: safeArtistName, + ); + _lastFinalizingTrackName = finalizingTrackName; + _lastFinalizingArtistName = safeArtistName; + } return; } + _lastFinalizingTrackName = null; + _lastFinalizingArtistName = null; if (items.isNotEmpty) { final firstEntry = items.entries.first; @@ -945,19 +1006,28 @@ class DownloadQueueNotifier extends Notifier { notifTotal = 100; } - _notificationService.showDownloadProgress( + final safeNotifTotal = notifTotal > 0 ? notifTotal : 1; + if (_shouldUpdateProgressNotification( trackName: trackName, artistName: artistName, progress: notifProgress, - total: notifTotal > 0 ? notifTotal : 1, - ); + total: safeNotifTotal, + queueCount: queuedCount, + )) { + _notificationService.showDownloadProgress( + trackName: trackName, + artistName: artistName, + progress: notifProgress, + total: safeNotifTotal, + ); + } if (Platform.isAndroid) { _maybeUpdateAndroidDownloadService( trackName: firstDownloading.track.name, artistName: firstDownloading.track.artistName, progress: notifProgress, - total: notifTotal > 0 ? notifTotal : 1, + total: safeNotifTotal, queueCount: queuedCount, ); } @@ -1023,6 +1093,12 @@ class DownloadQueueNotifier extends Notifier { _lastServicePercent = -1; _lastServiceQueueCount = -1; _lastServiceUpdateAt = DateTime.fromMillisecondsSinceEpoch(0); + _lastFinalizingTrackName = null; + _lastFinalizingArtistName = null; + _lastNotifTrackName = null; + _lastNotifArtistName = null; + _lastNotifPercent = -1; + _lastNotifQueueCount = -1; } Future _initOutputDir() async { diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index a42f2b17..08764056 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -39,16 +39,23 @@ class LocalLibraryState { this.scanWasCancelled = false, this.lastScannedAt, this.excludedDownloadedCount = 0, - }) : _isrcSet = items - .where((item) => item.isrc != null && item.isrc!.isNotEmpty) - .map((item) => item.isrc!) - .toSet(), - _trackKeySet = items.map((item) => item.matchKey).toSet(), - _byIsrc = Map.fromEntries( - items - .where((item) => item.isrc != null && item.isrc!.isNotEmpty) - .map((item) => MapEntry(item.isrc!, item)), - ); + Set? isrcSet, + Set? trackKeySet, + Map? byIsrc, + }) : _isrcSet = + isrcSet ?? + items + .where((item) => item.isrc != null && item.isrc!.isNotEmpty) + .map((item) => item.isrc!) + .toSet(), + _trackKeySet = trackKeySet ?? items.map((item) => item.matchKey).toSet(), + _byIsrc = + byIsrc ?? + Map.fromEntries( + items + .where((item) => item.isrc != null && item.isrc!.isNotEmpty) + .map((item) => MapEntry(item.isrc!, item)), + ); bool hasIsrc(String isrc) => _isrcSet.contains(isrc); @@ -86,8 +93,11 @@ class LocalLibraryState { DateTime? lastScannedAt, int? excludedDownloadedCount, }) { + final nextItems = items ?? this.items; + final keepDerivedIndex = identical(nextItems, this.items); + return LocalLibraryState( - items: items ?? this.items, + items: nextItems, isScanning: isScanning ?? this.isScanning, scanProgress: scanProgress ?? this.scanProgress, scanCurrentFile: scanCurrentFile ?? this.scanCurrentFile, @@ -98,6 +108,9 @@ class LocalLibraryState { lastScannedAt: lastScannedAt ?? this.lastScannedAt, excludedDownloadedCount: excludedDownloadedCount ?? this.excludedDownloadedCount, + isrcSet: keepDerivedIndex ? _isrcSet : null, + trackKeySet: keepDerivedIndex ? _trackKeySet : null, + byIsrc: keepDerivedIndex ? _byIsrc : null, ); } } @@ -397,14 +410,33 @@ class LocalLibraryNotifier extends Notifier { _progressTimer = Timer.periodic(_progressPollingInterval, (_) async { try { final progress = await PlatformBridge.getLibraryScanProgress(); - - state = state.copyWith( - scanProgress: (progress['progress_pct'] as num?)?.toDouble() ?? 0, - scanCurrentFile: progress['current_file'] as String?, - scanTotalFiles: progress['total_files'] as int? ?? 0, - scannedFiles: progress['scanned_files'] as int? ?? 0, - scanErrorCount: progress['error_count'] as int? ?? 0, + final nextProgress = + (progress['progress_pct'] as num?)?.toDouble() ?? 0; + final normalizedProgress = ((nextProgress * 10).round() / 10).clamp( + 0.0, + 100.0, ); + final currentFile = progress['current_file'] as String?; + final totalFiles = progress['total_files'] as int? ?? 0; + final scannedFiles = progress['scanned_files'] as int? ?? 0; + final errorCount = progress['error_count'] as int? ?? 0; + + final shouldUpdateState = + state.scanProgress != normalizedProgress || + state.scanCurrentFile != currentFile || + state.scanTotalFiles != totalFiles || + state.scannedFiles != scannedFiles || + state.scanErrorCount != errorCount; + + if (shouldUpdateState) { + state = state.copyWith( + scanProgress: normalizedProgress, + scanCurrentFile: currentFile, + scanTotalFiles: totalFiles, + scannedFiles: scannedFiles, + scanErrorCount: errorCount, + ); + } if (progress['is_complete'] == true) { _stopProgressPolling(); diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index 6e2229ac..8224c2b5 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -268,6 +268,13 @@ class _AlbumScreenState extends ConsumerState { (constraints.maxHeight - kToolbarHeight) / (expandedHeight - kToolbarHeight); final showContent = collapseRatio > 0.3; + final dpr = MediaQuery.devicePixelRatioOf( + context, + ).clamp(1.0, 3.0).toDouble(); + final backgroundMemCacheWidth = (constraints.maxWidth * dpr) + .round() + .clamp(720, 1440) + .toInt(); return FlexibleSpaceBar( collapseMode: CollapseMode.none, @@ -279,6 +286,7 @@ class _AlbumScreenState extends ConsumerState { CachedNetworkImage( imageUrl: widget.coverUrl!, fit: BoxFit.cover, + memCacheWidth: backgroundMemCacheWidth, cacheManager: CoverCacheManager.instance, placeholder: (_, _) => Container(color: colorScheme.surface), diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 28ff7e2f..d6950a9e 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,6 +9,7 @@ import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; +import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart'; /// Screen to display downloaded tracks from a specific album class DownloadedAlbumScreen extends ConsumerStatefulWidget { @@ -191,10 +193,21 @@ class _DownloadedAlbumScreenState extends ConsumerState { } } - void _navigateToMetadataScreen(DownloadHistoryItem item) { + void _onEmbeddedCoverChanged() { + if (!mounted) return; + setState(() {}); + } + + Future _navigateToMetadataScreen(DownloadHistoryItem item) async { + final navigator = Navigator.of(context); _precacheCover(item.coverUrl); - Navigator.push( - context, + final beforeModTime = + await DownloadedEmbeddedCoverResolver.readFileModTimeMillis( + item.filePath, + ); + if (!mounted) return; + + final result = await navigator.push( PageRouteBuilder( transitionDuration: const Duration(milliseconds: 300), reverseTransitionDuration: const Duration(milliseconds: 250), @@ -204,6 +217,12 @@ class _DownloadedAlbumScreenState extends ConsumerState { FadeTransition(opacity: animation, child: child), ), ); + await DownloadedEmbeddedCoverResolver.scheduleRefreshForPath( + item.filePath, + beforeModTime: beforeModTime, + force: result == true, + onChanged: _onEmbeddedCoverChanged, + ); } void _precacheCover(String? url) { @@ -211,8 +230,19 @@ class _DownloadedAlbumScreenState extends ConsumerState { if (!url.startsWith('http://') && !url.startsWith('https://')) { return; } + final dpr = MediaQuery.devicePixelRatioOf( + context, + ).clamp(1.0, 3.0).toDouble(); + final targetSize = (360 * dpr).round().clamp(512, 1024).toInt(); precacheImage( - CachedNetworkImageProvider(url, cacheManager: CoverCacheManager.instance), + ResizeImage( + CachedNetworkImageProvider( + url, + cacheManager: CoverCacheManager.instance, + ), + width: targetSize, + height: targetSize, + ), context, ); } @@ -256,7 +286,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { CustomScrollView( controller: _scrollController, slivers: [ - _buildAppBar(context, colorScheme), + _buildAppBar(context, colorScheme, tracks), _buildInfoCard(context, colorScheme, tracks), _buildTrackListHeader(context, colorScheme, tracks), _buildTrackList(context, colorScheme, tracks), @@ -285,7 +315,19 @@ class _DownloadedAlbumScreenState extends ConsumerState { ); } - Widget _buildAppBar(BuildContext context, ColorScheme colorScheme) { + String? _resolveAlbumEmbeddedCoverPath(List tracks) { + if (tracks.isEmpty) return null; + return DownloadedEmbeddedCoverResolver.resolve( + tracks.first.filePath, + onChanged: _onEmbeddedCoverChanged, + ); + } + + Widget _buildAppBar( + BuildContext context, + ColorScheme colorScheme, + List tracks, + ) { final mediaSize = MediaQuery.of(context).size; final screenWidth = mediaSize.width; final shortestSide = mediaSize.shortestSide; @@ -294,6 +336,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { final bottomGradientHeight = (shortestSide * 0.2).clamp(56.0, 80.0); final coverTopPadding = (shortestSide * 0.14).clamp(40.0, 60.0); final fallbackIconSize = (coverSize * 0.32).clamp(44.0, 64.0); + final embeddedCoverPath = _resolveAlbumEmbeddedCoverPath(tracks); return SliverAppBar( expandedHeight: expandedHeight, @@ -322,6 +365,13 @@ class _DownloadedAlbumScreenState extends ConsumerState { (constraints.maxHeight - kToolbarHeight) / (expandedHeight - kToolbarHeight); final showContent = collapseRatio > 0.3; + final dpr = MediaQuery.devicePixelRatioOf( + context, + ).clamp(1.0, 3.0).toDouble(); + final backgroundMemCacheWidth = (constraints.maxWidth * dpr) + .round() + .clamp(720, 1440) + .toInt(); return FlexibleSpaceBar( collapseMode: CollapseMode.none, @@ -329,10 +379,19 @@ class _DownloadedAlbumScreenState extends ConsumerState { fit: StackFit.expand, children: [ // Blurred cover background - if (widget.coverUrl != null) + if (embeddedCoverPath != null) + Image.file( + File(embeddedCoverPath), + fit: BoxFit.cover, + cacheWidth: backgroundMemCacheWidth, + errorBuilder: (_, _, _) => + Container(color: colorScheme.surface), + ) + else if (widget.coverUrl != null) CachedNetworkImage( imageUrl: widget.coverUrl!, fit: BoxFit.cover, + memCacheWidth: backgroundMemCacheWidth, cacheManager: CoverCacheManager.instance, placeholder: (_, _) => Container(color: colorScheme.surface), @@ -389,7 +448,22 @@ class _DownloadedAlbumScreenState extends ConsumerState { ), child: ClipRRect( borderRadius: BorderRadius.circular(20), - child: widget.coverUrl != null + child: embeddedCoverPath != null + ? Image.file( + File(embeddedCoverPath), + fit: BoxFit.cover, + cacheWidth: (coverSize * 2).toInt(), + cacheHeight: (coverSize * 2).toInt(), + errorBuilder: (_, _, _) => Container( + color: colorScheme.surfaceContainerHighest, + child: Icon( + Icons.album, + size: fallbackIconSize, + color: colorScheme.onSurfaceVariant, + ), + ), + ) + : widget.coverUrl != null ? CachedNetworkImage( imageUrl: widget.coverUrl!, fit: BoxFit.cover, diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 35207f2e..aa5c8d79 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -18,6 +18,7 @@ import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/screens/album_screen.dart'; import 'package:spotiflac_android/screens/artist_screen.dart'; import 'package:spotiflac_android/services/csv_import_service.dart'; +import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/app_bar_layout.dart'; import 'package:spotiflac_android/utils/file_access.dart'; @@ -35,11 +36,13 @@ class HomeTab extends ConsumerStatefulWidget { class _RecentAccessView { final List uniqueItems; final List downloadItems; + final Map downloadFilePathByRecentKey; final bool hasHiddenDownloads; const _RecentAccessView({ required this.uniqueItems, required this.downloadItems, + required this.downloadFilePathByRecentKey, required this.hasHiddenDownloads, }); } @@ -932,7 +935,9 @@ class _HomeTabState extends ConsumerState ), // Search filter bar (only shown when has search results) - if (searchFilters.isNotEmpty && hasActualResults && !showRecentAccess) + if (searchFilters.isNotEmpty && + hasActualResults && + !showRecentAccess) SliverToBoxAdapter( child: _buildSearchFilterBar( searchFilters, @@ -1022,6 +1027,11 @@ class _HomeTabState extends ConsumerState ); } + void _onEmbeddedCoverChanged() { + if (!mounted) return; + setState(() {}); + } + Widget _buildRecentDownloads( List items, ColorScheme colorScheme, @@ -1049,6 +1059,10 @@ class _HomeTabState extends ConsumerState itemCount: itemCount, itemBuilder: (context, index) { final item = items[index]; + final embeddedCoverPath = DownloadedEmbeddedCoverResolver.resolve( + item.filePath, + onChanged: _onEmbeddedCoverChanged, + ); return KeyedSubtree( key: ValueKey(item.id), child: GestureDetector( @@ -1060,7 +1074,26 @@ class _HomeTabState extends ConsumerState children: [ ClipRRect( borderRadius: BorderRadius.circular(12), - child: item.coverUrl != null + child: embeddedCoverPath != null + ? Image.file( + File(embeddedCoverPath), + width: coverSize, + height: coverSize, + fit: BoxFit.cover, + cacheWidth: (coverSize * 2).round(), + cacheHeight: (coverSize * 2).round(), + errorBuilder: (_, _, _) => Container( + width: coverSize, + height: coverSize, + color: colorScheme.surfaceContainerHighest, + child: Icon( + Icons.music_note, + color: colorScheme.onSurfaceVariant, + size: 32, + ), + ), + ) + : item.coverUrl != null ? CachedNetworkImage( imageUrl: item.coverUrl!, width: coverSize, @@ -1125,6 +1158,7 @@ class _HomeTabState extends ConsumerState } final downloadItems = []; + final downloadFilePathByRecentKey = {}; for (final entry in albumGroups.entries) { final tracks = entry.value; final mostRecent = tracks.reduce( @@ -1136,29 +1170,31 @@ class _HomeTabState extends ConsumerState : mostRecent.artistName; if (tracks.length == 1) { - downloadItems.add( - RecentAccessItem( - id: mostRecent.spotifyId ?? mostRecent.id, - name: mostRecent.trackName, - subtitle: mostRecent.artistName, - imageUrl: mostRecent.coverUrl, - type: RecentAccessType.track, - accessedAt: mostRecent.downloadedAt, - providerId: 'download', - ), + final recent = RecentAccessItem( + id: mostRecent.spotifyId ?? mostRecent.id, + name: mostRecent.trackName, + subtitle: mostRecent.artistName, + imageUrl: mostRecent.coverUrl, + type: RecentAccessType.track, + accessedAt: mostRecent.downloadedAt, + providerId: 'download', ); + downloadItems.add(recent); + downloadFilePathByRecentKey['${recent.type.name}:${recent.id}'] = + mostRecent.filePath; } else { - downloadItems.add( - RecentAccessItem( - id: '${mostRecent.albumName}|$artistForKey', - name: mostRecent.albumName, - subtitle: artistForKey, - imageUrl: mostRecent.coverUrl, - type: RecentAccessType.album, - accessedAt: mostRecent.downloadedAt, - providerId: 'download', - ), + final recent = RecentAccessItem( + id: '${mostRecent.albumName}|$artistForKey', + name: mostRecent.albumName, + subtitle: artistForKey, + imageUrl: mostRecent.coverUrl, + type: RecentAccessType.album, + accessedAt: mostRecent.downloadedAt, + providerId: 'download', ); + downloadItems.add(recent); + downloadFilePathByRecentKey['${recent.type.name}:${recent.id}'] = + mostRecent.filePath; } } @@ -1192,6 +1228,7 @@ class _HomeTabState extends ConsumerState final view = _RecentAccessView( uniqueItems: uniqueItems, downloadItems: downloadItems, + downloadFilePathByRecentKey: downloadFilePathByRecentKey, hasHiddenDownloads: hiddenIds.isNotEmpty, ); @@ -1680,7 +1717,11 @@ class _HomeTabState extends ConsumerState ) else ...uniqueItems.map( - (item) => _buildRecentAccessItem(item, colorScheme), + (item) => _buildRecentAccessItem( + item, + colorScheme, + view.downloadFilePathByRecentKey, + ), ), ], ), @@ -1690,10 +1731,17 @@ class _HomeTabState extends ConsumerState Widget _buildRecentAccessItem( RecentAccessItem item, ColorScheme colorScheme, + Map downloadFilePathByRecentKey, ) { IconData typeIcon; String typeLabel; final isDownloaded = item.providerId == 'download'; + final embeddedCoverPath = isDownloaded + ? DownloadedEmbeddedCoverResolver.resolve( + downloadFilePathByRecentKey['${item.type.name}:${item.id}'], + onChanged: _onEmbeddedCoverChanged, + ) + : null; switch (item.type) { case RecentAccessType.artist: @@ -1723,7 +1771,25 @@ class _HomeTabState extends ConsumerState borderRadius: BorderRadius.circular( item.type == RecentAccessType.artist ? 28 : 4, ), - child: item.imageUrl != null && item.imageUrl!.isNotEmpty + child: embeddedCoverPath != null + ? Image.file( + File(embeddedCoverPath), + width: 56, + height: 56, + fit: BoxFit.cover, + cacheWidth: 112, + cacheHeight: 112, + errorBuilder: (context, error, stackTrace) => Container( + width: 56, + height: 56, + color: colorScheme.surfaceContainerHighest, + child: Icon( + typeIcon, + color: colorScheme.onSurfaceVariant, + ), + ), + ) + : item.imageUrl != null && item.imageUrl!.isNotEmpty ? CachedNetworkImage( imageUrl: item.imageUrl!, width: 56, @@ -1896,10 +1962,15 @@ class _HomeTabState extends ConsumerState } } - void _navigateToMetadataScreen(DownloadHistoryItem item) { + Future _navigateToMetadataScreen(DownloadHistoryItem item) async { + final navigator = Navigator.of(context); _precacheCover(item.coverUrl); - Navigator.push( - context, + final beforeModTime = + await DownloadedEmbeddedCoverResolver.readFileModTimeMillis( + item.filePath, + ); + if (!mounted) return; + final result = await navigator.push( PageRouteBuilder( transitionDuration: const Duration(milliseconds: 300), reverseTransitionDuration: const Duration(milliseconds: 250), @@ -1909,6 +1980,12 @@ class _HomeTabState extends ConsumerState FadeTransition(opacity: animation, child: child), ), ); + await DownloadedEmbeddedCoverResolver.scheduleRefreshForPath( + item.filePath, + beforeModTime: beforeModTime, + force: result == true, + onChanged: _onEmbeddedCoverChanged, + ); } void _precacheCover(String? url) { @@ -1916,8 +1993,19 @@ class _HomeTabState extends ConsumerState if (!url.startsWith('http://') && !url.startsWith('https://')) { return; } + final dpr = MediaQuery.devicePixelRatioOf( + context, + ).clamp(1.0, 3.0).toDouble(); + final targetSize = (360 * dpr).round().clamp(512, 1024).toInt(); precacheImage( - CachedNetworkImageProvider(url, cacheManager: CoverCacheManager.instance), + ResizeImage( + CachedNetworkImageProvider( + url, + cacheManager: CoverCacheManager.instance, + ), + width: targetSize, + height: targetSize, + ), context, ); } diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 933fdd2c..ef7d488b 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -181,6 +181,13 @@ class _PlaylistScreenState extends ConsumerState { (constraints.maxHeight - kToolbarHeight) / (expandedHeight - kToolbarHeight); final showContent = collapseRatio > 0.3; + final dpr = MediaQuery.devicePixelRatioOf( + context, + ).clamp(1.0, 3.0).toDouble(); + final backgroundMemCacheWidth = (constraints.maxWidth * dpr) + .round() + .clamp(720, 1440) + .toInt(); return FlexibleSpaceBar( collapseMode: CollapseMode.none, @@ -192,6 +199,7 @@ class _PlaylistScreenState extends ConsumerState { CachedNetworkImage( imageUrl: widget.coverUrl!, fit: BoxFit.cover, + memCacheWidth: backgroundMemCacheWidth, cacheManager: CoverCacheManager.instance, placeholder: (_, _) => Container(color: colorScheme.surface), diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 95c886dc..25f4e9a9 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -210,6 +210,20 @@ class _UnifiedCacheEntry { }); } +class _QueueItemIdsSnapshot { + final List ids; + + const _QueueItemIdsSnapshot(this.ids); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _QueueItemIdsSnapshot && listEquals(ids, other.ids); + + @override + int get hashCode => Object.hashAll(ids); +} + Map> _filterHistoryInIsolate(Map payload) { final entries = (payload['entries'] as List).cast(); final albumCounts = (payload['albumCounts'] as Map).cast(); @@ -722,10 +736,11 @@ class _QueueTabState extends ConsumerState { if (confirmed == true && mounted) { final historyNotifier = ref.read(downloadHistoryProvider.notifier); final localLibraryDb = LibraryDatabase.instance; + final itemsById = {for (final item in allItems) item.id: item}; int deletedCount = 0; for (final id in _selectedIds) { - final item = allItems.where((e) => e.id == id).firstOrNull; + final item = itemsById[id]; if (item != null) { try { final cleanPath = _cleanFilePath(item.filePath); @@ -811,7 +826,8 @@ class _QueueTabState extends ConsumerState { } try { - return File(cleanPath).statSync().modified.millisecondsSinceEpoch; + final stat = await File(cleanPath).stat(); + return stat.modified.millisecondsSinceEpoch; } catch (_) { return null; } @@ -987,6 +1003,21 @@ class _QueueTabState extends ConsumerState { }); } + String _fileExtLower(String filePath) { + final dotIndex = filePath.lastIndexOf('.'); + if (dotIndex < 0 || dotIndex == filePath.length - 1) { + return ''; + } + return filePath.substring(dotIndex + 1).toLowerCase(); + } + + String? _localQualityLabel(LocalLibraryItem item) { + if (item.bitDepth == null || item.sampleRate == null) { + return null; + } + return '${item.bitDepth}bit/${(item.sampleRate! / 1000).toStringAsFixed(1)}kHz'; + } + List _applyAdvancedFilters( List items, ) { @@ -1024,7 +1055,7 @@ class _QueueTabState extends ConsumerState { } if (_filterFormat != null) { - final ext = item.filePath.split('.').last.toLowerCase(); + final ext = _fileExtLower(item.filePath); if (ext != _filterFormat) return false; } @@ -1080,7 +1111,7 @@ class _QueueTabState extends ConsumerState { /// Check if a file path passes the current format filter bool _passesFormatFilter(String filePath) { if (_filterFormat == null) return true; - return filePath.split('.').last.toLowerCase() == _filterFormat; + return _fileExtLower(filePath) == _filterFormat; } /// Filter grouped download albums by search query + advanced filters @@ -1105,15 +1136,15 @@ class _QueueTabState extends ConsumerState { // Filter tracks within the album by advanced filters if (_filterQuality != null || _filterFormat != null) { - final filteredTracks = album.tracks - .where((track) { - if (!_passesQualityFilter(track.quality)) return false; - if (!_passesFormatFilter(track.filePath)) return false; - return true; - }) - .toList(growable: false); + var hasMatchingTrack = false; + for (final track in album.tracks) { + if (!_passesQualityFilter(track.quality)) continue; + if (!_passesFormatFilter(track.filePath)) continue; + hasMatchingTrack = true; + break; + } - if (filteredTracks.isEmpty) continue; + if (!hasMatchingTrack) continue; } result.add(album); @@ -1162,20 +1193,15 @@ class _QueueTabState extends ConsumerState { // Filter tracks within the album by advanced filters if (_filterQuality != null || _filterFormat != null) { - final filteredTracks = album.tracks - .where((track) { - String? quality; - if (track.bitDepth != null && track.sampleRate != null) { - quality = - '${track.bitDepth}bit/${(track.sampleRate! / 1000).toStringAsFixed(1)}kHz'; - } - if (!_passesQualityFilter(quality)) return false; - if (!_passesFormatFilter(track.filePath)) return false; - return true; - }) - .toList(growable: false); + var hasMatchingTrack = false; + for (final track in album.tracks) { + if (!_passesQualityFilter(_localQualityLabel(track))) continue; + if (!_passesFormatFilter(track.filePath)) continue; + hasMatchingTrack = true; + break; + } - if (filteredTracks.isEmpty) continue; + if (!hasMatchingTrack) continue; } result.add(album); @@ -1205,7 +1231,7 @@ class _QueueTabState extends ConsumerState { Set _getAvailableFormats(List items) { final formats = {}; for (final item in items) { - final ext = item.filePath.split('.').last.toLowerCase(); + final ext = _fileExtLower(item.filePath); if (['flac', 'mp3', 'm4a', 'opus', 'ogg', 'wav', 'aiff'].contains(ext)) { formats.add(ext); } @@ -1457,8 +1483,19 @@ class _QueueTabState extends ConsumerState { if (!url.startsWith('http://') && !url.startsWith('https://')) { return; } + final dpr = MediaQuery.devicePixelRatioOf( + context, + ).clamp(1.0, 3.0).toDouble(); + final targetSize = (360 * dpr).round().clamp(512, 1024).toInt(); precacheImage( - CachedNetworkImageProvider(url, cacheManager: CoverCacheManager.instance), + ResizeImage( + CachedNetworkImageProvider( + url, + cacheManager: CoverCacheManager.instance, + ), + width: targetSize, + height: targetSize, + ), context, ); } @@ -2272,20 +2309,26 @@ class _QueueTabState extends ConsumerState { Widget _buildQueueItemsSliver(BuildContext context, ColorScheme colorScheme) { return Consumer( builder: (context, ref, child) { - final queueItems = ref.watch( - downloadQueueProvider.select((s) => s.items), + final queueIdsSnapshot = ref.watch( + downloadQueueProvider.select( + (s) => _QueueItemIdsSnapshot( + s.items.map((item) => item.id).toList(growable: false), + ), + ), ); - if (queueItems.isEmpty) { + if (queueIdsSnapshot.ids.isEmpty) { return const SliverToBoxAdapter(child: SizedBox.shrink()); } return SliverList( delegate: SliverChildBuilderDelegate((context, index) { - final item = queueItems[index]; - return KeyedSubtree( - key: ValueKey(item.id), - child: _buildQueueItem(context, item, colorScheme), + final itemId = queueIdsSnapshot.ids[index]; + return _QueueItemSliverRow( + key: ValueKey(itemId), + itemId: itemId, + colorScheme: colorScheme, + itemBuilder: _buildQueueItem, ); - }, childCount: queueItems.length), + }, childCount: queueIdsSnapshot.ids.length), ); }, ); @@ -3953,6 +3996,38 @@ class _QueueTabState extends ConsumerState { } } +class _QueueItemSliverRow extends ConsumerWidget { + final String itemId; + final ColorScheme colorScheme; + final Widget Function(BuildContext, DownloadItem, ColorScheme) itemBuilder; + + const _QueueItemSliverRow({ + super.key, + required this.itemId, + required this.colorScheme, + required this.itemBuilder, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final item = ref.watch( + downloadQueueProvider.select((state) { + for (final current in state.items) { + if (current.id == itemId) { + return current; + } + } + return null; + }), + ); + if (item == null) { + return const SizedBox.shrink(); + } + + return RepaintBoundary(child: itemBuilder(context, item, colorScheme)); + } +} + class _FilterChip extends StatelessWidget { final String label; final int count; diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 0788ecb9..d85d447f 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -26,7 +26,9 @@ class _SearchScreenState extends ConsumerState { if (widget.query.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { final settings = ref.read(settingsProvider); - ref.read(trackProvider.notifier).search(widget.query, metadataSource: settings.metadataSource); + ref + .read(trackProvider.notifier) + .search(widget.query, metadataSource: settings.metadataSource); }); } } @@ -41,19 +43,20 @@ class _SearchScreenState extends ConsumerState { final query = _searchController.text.trim(); if (query.isNotEmpty) { final settings = ref.read(settingsProvider); - ref.read(trackProvider.notifier).search(query, metadataSource: settings.metadataSource); + ref + .read(trackProvider.notifier) + .search(query, metadataSource: settings.metadataSource); } } void _downloadTrack(Track track) { final settings = ref.read(settingsProvider); - ref.read(downloadQueueProvider.notifier).addToQueue( - track, - settings.defaultService, - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Added "${track.name}" to queue')), - ); + ref + .read(downloadQueueProvider.notifier) + .addToQueue(track, settings.defaultService); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); } @override @@ -78,10 +81,7 @@ class _SearchScreenState extends ConsumerState { autofocus: widget.query.isEmpty, ), actions: [ - IconButton( - icon: const Icon(Icons.search), - onPressed: _search, - ), + IconButton(icon: const Icon(Icons.search), onPressed: _search), ], ), body: Column( @@ -92,7 +92,7 @@ class _SearchScreenState extends ConsumerState { Padding( padding: const EdgeInsets.all(16.0), child: Text( - trackState.error!, + trackState.error!, style: TextStyle(color: colorScheme.error), ), ), @@ -115,11 +115,7 @@ class _SearchScreenState extends ConsumerState { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.search, - size: 64, - color: colorScheme.onSurfaceVariant, - ), + Icon(Icons.search, size: 64, color: colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text( 'Search for tracks', @@ -137,11 +133,13 @@ class _SearchScreenState extends ConsumerState { leading: track.coverUrl != null ? ClipRRect( borderRadius: BorderRadius.circular(8), -child: CachedNetworkImage( + child: CachedNetworkImage( imageUrl: track.coverUrl!, width: 48, height: 48, fit: BoxFit.cover, + memCacheWidth: 144, + memCacheHeight: 144, cacheManager: CoverCacheManager.instance, ), ) @@ -152,15 +150,18 @@ child: CachedNetworkImage( color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), - child: Icon(Icons.music_note, color: colorScheme.onSurfaceVariant), + child: Icon( + Icons.music_note, + color: colorScheme.onSurfaceVariant, + ), ), title: Text(track.name, maxLines: 1, overflow: TextOverflow.ellipsis), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - track.artistName, - maxLines: 1, + track.artistName, + maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: colorScheme.onSurfaceVariant), ), diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 64c88eb8..4131282c 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -384,6 +384,8 @@ class _ContributorItem extends StatelessWidget { width: 40, height: 40, fit: BoxFit.cover, + memCacheWidth: 120, + memCacheHeight: 120, cacheManager: CoverCacheManager.instance, placeholder: (context, url) => Container( width: 40, diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 730e7f37..1d87551e 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -565,6 +565,13 @@ class _TrackMetadataScreenState extends ConsumerState { double coverSize, bool showContent, ) { + final screenSize = MediaQuery.sizeOf(context); + final pixelRatio = MediaQuery.devicePixelRatioOf(context); + final backgroundCacheWidth = (screenSize.width * pixelRatio).round(); + final backgroundCacheHeight = (screenSize.height * 0.65 * pixelRatio) + .round(); + final coverCacheSize = (coverSize * pixelRatio).round(); + return Stack( fit: StackFit.expand, children: [ @@ -573,12 +580,16 @@ class _TrackMetadataScreenState extends ConsumerState { Image.file( File(_embeddedCoverPreviewPath!), fit: BoxFit.cover, + cacheWidth: backgroundCacheWidth, + cacheHeight: backgroundCacheHeight, errorBuilder: (_, _, _) => Container(color: colorScheme.surface), ) else if (_coverUrl != null) CachedNetworkImage( imageUrl: _coverUrl!, fit: BoxFit.cover, + memCacheWidth: backgroundCacheWidth, + memCacheHeight: backgroundCacheHeight, cacheManager: CoverCacheManager.instance, placeholder: (_, _) => Container(color: colorScheme.surface), errorWidget: (_, _, _) => Container(color: colorScheme.surface), @@ -587,6 +598,8 @@ class _TrackMetadataScreenState extends ConsumerState { Image.file( File(_localCoverPath!), fit: BoxFit.cover, + cacheWidth: backgroundCacheWidth, + cacheHeight: backgroundCacheHeight, errorBuilder: (_, _, _) => Container(color: colorScheme.surface), ) else @@ -648,6 +661,8 @@ class _TrackMetadataScreenState extends ConsumerState { ? Image.file( File(_embeddedCoverPreviewPath!), fit: BoxFit.cover, + cacheWidth: coverCacheSize, + cacheHeight: coverCacheSize, errorBuilder: (_, _, _) => Container( color: colorScheme.surfaceContainerHighest, child: Icon( @@ -673,7 +688,12 @@ class _TrackMetadataScreenState extends ConsumerState { ), ) : _localCoverPath != null && _localCoverPath!.isNotEmpty - ? Image.file(File(_localCoverPath!), fit: BoxFit.cover) + ? Image.file( + File(_localCoverPath!), + fit: BoxFit.cover, + cacheWidth: coverCacheSize, + cacheHeight: coverCacheSize, + ) : Container( color: colorScheme.surfaceContainerHighest, child: Icon( diff --git a/lib/services/downloaded_embedded_cover_resolver.dart b/lib/services/downloaded_embedded_cover_resolver.dart new file mode 100644 index 00000000..0f40bd02 --- /dev/null +++ b/lib/services/downloaded_embedded_cover_resolver.dart @@ -0,0 +1,235 @@ +import 'dart:collection'; +import 'dart:io'; + +import 'package:flutter/widgets.dart'; +import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; + +class _EmbeddedCoverCacheEntry { + final String previewPath; + final int? sourceModTimeMillis; + + const _EmbeddedCoverCacheEntry({ + required this.previewPath, + this.sourceModTimeMillis, + }); +} + +/// Shared resolver for embedded cover previews from downloaded/local files. +/// It keeps a bounded in-memory cache and only refreshes extraction +/// when the source file changed. +class DownloadedEmbeddedCoverResolver { + static const int _maxCacheEntries = 160; + static const int _minModCheckIntervalMs = 1200; + + static final LinkedHashMap _cache = + LinkedHashMap(); + static final Set _pendingExtract = {}; + static final Set _pendingModCheck = {}; + static final Set _failedExtract = {}; + static final Map _lastModCheckMillis = {}; + + static String cleanFilePath(String? filePath) { + if (filePath == null) return ''; + if (filePath.startsWith('EXISTS:')) { + return filePath.substring(7); + } + return filePath; + } + + static Future readFileModTimeMillis(String? filePath) async { + final cleanPath = cleanFilePath(filePath); + if (cleanPath.isEmpty) return null; + + if (isContentUri(cleanPath)) { + try { + final modTimes = await PlatformBridge.getSafFileModTimes([cleanPath]); + return modTimes[cleanPath]; + } catch (_) { + return null; + } + } + + try { + final stat = await File(cleanPath).stat(); + return stat.modified.millisecondsSinceEpoch; + } catch (_) { + return null; + } + } + + static String? resolve(String? filePath, {VoidCallback? onChanged}) { + final cleanPath = cleanFilePath(filePath); + if (cleanPath.isEmpty) return null; + + final cached = _cache[cleanPath]; + if (cached != null) { + if (File(cached.previewPath).existsSync()) { + _touch(cleanPath, cached); + _scheduleModCheck(cleanPath, onChanged: onChanged); + return cached.previewPath; + } + _cache.remove(cleanPath); + _cleanupTempCoverPathSync(cached.previewPath); + } + + _ensureCover(cleanPath, onChanged: onChanged); + return null; + } + + static Future scheduleRefreshForPath( + String? filePath, { + int? beforeModTime, + bool force = false, + VoidCallback? onChanged, + }) async { + final cleanPath = cleanFilePath(filePath); + if (cleanPath.isEmpty) return; + + if (!force) { + if (beforeModTime == null) return; + final afterModTime = await readFileModTimeMillis(cleanPath); + if (afterModTime != null && afterModTime == beforeModTime) { + return; + } + } + + _failedExtract.remove(cleanPath); + _ensureCover(cleanPath, forceRefresh: true, onChanged: onChanged); + } + + static void invalidate(String? filePath) { + final cleanPath = cleanFilePath(filePath); + if (cleanPath.isEmpty) return; + + final cached = _cache.remove(cleanPath); + _pendingExtract.remove(cleanPath); + _pendingModCheck.remove(cleanPath); + _failedExtract.remove(cleanPath); + _lastModCheckMillis.remove(cleanPath); + if (cached != null) { + _cleanupTempCoverPathSync(cached.previewPath); + } + } + + static void _touch(String cleanPath, _EmbeddedCoverCacheEntry entry) { + _cache + ..remove(cleanPath) + ..[cleanPath] = entry; + } + + static void _trimCacheIfNeeded() { + while (_cache.length > _maxCacheEntries) { + final oldestKey = _cache.keys.first; + final removed = _cache.remove(oldestKey); + if (removed != null) { + _cleanupTempCoverPathSync(removed.previewPath); + } + _pendingExtract.remove(oldestKey); + _pendingModCheck.remove(oldestKey); + _failedExtract.remove(oldestKey); + _lastModCheckMillis.remove(oldestKey); + } + } + + static void _scheduleModCheck(String cleanPath, {VoidCallback? onChanged}) { + if (_pendingModCheck.contains(cleanPath)) return; + + final now = DateTime.now().millisecondsSinceEpoch; + final lastCheck = _lastModCheckMillis[cleanPath] ?? 0; + if (now - lastCheck < _minModCheckIntervalMs) return; + _lastModCheckMillis[cleanPath] = now; + + _pendingModCheck.add(cleanPath); + Future.microtask(() async { + try { + final cached = _cache[cleanPath]; + if (cached == null) return; + + final currentModTime = await readFileModTimeMillis(cleanPath); + if (currentModTime != null && + cached.sourceModTimeMillis != null && + currentModTime != cached.sourceModTimeMillis) { + _ensureCover( + cleanPath, + forceRefresh: true, + knownModTime: currentModTime, + onChanged: onChanged, + ); + } + } finally { + _pendingModCheck.remove(cleanPath); + } + }); + } + + static void _ensureCover( + String cleanPath, { + bool forceRefresh = false, + int? knownModTime, + VoidCallback? onChanged, + }) { + if (cleanPath.isEmpty) return; + if (_pendingExtract.contains(cleanPath)) return; + if (!forceRefresh && _cache.containsKey(cleanPath)) return; + if (!forceRefresh && _failedExtract.contains(cleanPath)) return; + + _pendingExtract.add(cleanPath); + Future.microtask(() async { + String? outputPath; + try { + final modTime = knownModTime ?? await readFileModTimeMillis(cleanPath); + final tempDir = await Directory.systemTemp.createTemp( + 'download_cover_preview_', + ); + outputPath = + '${tempDir.path}${Platform.pathSeparator}cover_preview.jpg'; + final result = await PlatformBridge.extractCoverToFile( + cleanPath, + outputPath, + ); + + final hasCover = + result['error'] == null && await File(outputPath).exists(); + if (!hasCover) { + _failedExtract.add(cleanPath); + _cleanupTempCoverPathSync(outputPath); + return; + } + + final previous = _cache[cleanPath]; + final next = _EmbeddedCoverCacheEntry( + previewPath: outputPath, + sourceModTimeMillis: modTime, + ); + _touch(cleanPath, next); + _failedExtract.remove(cleanPath); + _trimCacheIfNeeded(); + + if (previous != null && previous.previewPath != outputPath) { + _cleanupTempCoverPathSync(previous.previewPath); + } + onChanged?.call(); + } catch (_) { + _failedExtract.add(cleanPath); + _cleanupTempCoverPathSync(outputPath); + } finally { + _pendingExtract.remove(cleanPath); + } + }); + } + + static void _cleanupTempCoverPathSync(String? coverPath) { + if (coverPath == null || coverPath.isEmpty) return; + try { + final file = File(coverPath); + if (file.existsSync()) { + file.deleteSync(); + } + final parent = file.parent; + if (parent.existsSync()) { + parent.deleteSync(recursive: true); + } + } catch (_) {} + } +} diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index 9c8ef00d..be1caa1e 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -119,10 +119,15 @@ class LogBuffer extends ChangeNotifier { final result = await PlatformBridge.getGoLogsSince(_lastGoLogIndex); final logs = result['logs'] as List? ?? []; final nextIndex = result['next_index'] as int? ?? _lastGoLogIndex; + final keepNonErrorLogs = _loggingEnabled; for (final log in logs) { - final timestamp = log['timestamp'] as String? ?? ''; final level = log['level'] as String? ?? 'INFO'; + if (!keepNonErrorLogs && level != 'ERROR' && level != 'FATAL') { + continue; + } + + final timestamp = log['timestamp'] as String? ?? ''; final tag = log['tag'] as String? ?? 'Go'; final message = log['message'] as String? ?? ''; @@ -372,6 +377,10 @@ class AppLogger { } void _addToBuffer(String level, String message, {String? error}) { + if (!LogBuffer.loggingEnabled && level != 'ERROR' && level != 'FATAL') { + return; + } + LogBuffer().add( LogEntry( timestamp: DateTime.now(), From 44ac593ddcc2de6234b5fbe312b086169177de5a Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 02:02:03 +0700 Subject: [PATCH 016/145] perf+security: polling guards, sensitive data redaction, SAF path sanitization Go backend: - Add sensitive data redaction in log buffer (tokens, keys, passwords) - Validate extension auth URLs (HTTPS only, no private IPs, no embedded creds) - Block embedded credentials in extension HTTP requests - Tighten extension storage file permissions (0644 -> 0600) - Sanitize extension ID in store download path - Summarize auth URLs in logs to prevent token leakage Android (Kotlin): - Add sanitizeRelativeDir to prevent path traversal in SAF operations - Apply sanitizeFilename to all user-provided file names in SAF Flutter: - Add sensitive data redaction in Dart logger (mirrors Go patterns) - Mask device ID in log exports - Add in-flight guard to progress polling (download queue + local library) - Remove redundant _downloadedSpotifyIds Set, use _bySpotifyId map - Remove redundant _isrcSet, use _byIsrc map - Expand DownloadQueueLookup with byItemId and itemIds - Lazy search index building in queue tab - Bound embedded cover cache in queue tab (max 180) - Coalesce embedded cover refresh callbacks via postFrameCallback - Cache album track filtering in downloaded album screen - Cache thumbnail sizes by extension ID in home tab - Simplify recent access aggregation (single-pass) - Remove unused _isTyping state in home tab - Cap pre-warm track batch size to 80 - Skip setShowingRecentAccess if value unchanged - Use downloadQueueLookupProvider for granular queue selectors - Move grouped album filtering before content data computation --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 41 +- go_backend/exports.go | 16 +- go_backend/extension_runtime_auth.go | 62 ++- go_backend/extension_runtime_http.go | 3 + go_backend/extension_runtime_storage.go | 2 +- go_backend/logbuffer.go | 16 + go_backend/security_hardening_test.go | 80 ++++ lib/providers/download_queue_provider.dart | 60 +-- lib/providers/local_library_provider.dart | 19 +- lib/providers/track_provider.dart | 353 ++++++++++++------ lib/screens/downloaded_album_screen.dart | 78 ++-- lib/screens/home_tab.dart | 154 ++++---- lib/screens/queue_tab.dart | 151 ++++---- lib/utils/logger.dart | 80 +++- 14 files changed, 785 insertions(+), 330 deletions(-) create mode 100644 go_backend/security_hardening_test.go diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index fc937882..931891e9 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -300,8 +300,18 @@ class MainActivity: FlutterFragmentActivity() { return name.replace(Regex("[\\\\/:*?\"<>|]"), "_").trim() } + private fun sanitizeRelativeDir(relativeDir: String): String { + if (relativeDir.isBlank()) return "" + return relativeDir + .split("/") + .map { sanitizeFilename(it) } + .filter { it.isNotBlank() && it != "." && it != ".." } + .joinToString("/") + } + private fun ensureDocumentDir(treeUri: Uri, relativeDir: String): DocumentFile? { - if (relativeDir.isBlank()) { + val safeRelativeDir = sanitizeRelativeDir(relativeDir) + if (safeRelativeDir.isBlank()) { return DocumentFile.fromTreeUri(this, treeUri) } @@ -310,7 +320,7 @@ class MainActivity: FlutterFragmentActivity() { synchronized(safDirLock) { var current = DocumentFile.fromTreeUri(this, treeUri) ?: return null - val parts = relativeDir.split("/").filter { it.isNotBlank() } + val parts = safeRelativeDir.split("/").filter { it.isNotBlank() } for (part in parts) { val existing = current.findFile(part) current = if (existing != null && existing.isDirectory) { @@ -335,9 +345,10 @@ class MainActivity: FlutterFragmentActivity() { private fun findDocumentDir(treeUri: Uri, relativeDir: String): DocumentFile? { var current = DocumentFile.fromTreeUri(this, treeUri) ?: return null - if (relativeDir.isBlank()) return current + val safeRelativeDir = sanitizeRelativeDir(relativeDir) + if (safeRelativeDir.isBlank()) return current - val parts = relativeDir.split("/").filter { it.isNotBlank() } + val parts = safeRelativeDir.split("/").filter { it.isNotBlank() } for (part in parts) { val existing = current.findFile(part) if (existing == null || !existing.isDirectory) return null @@ -377,14 +388,21 @@ class MainActivity: FlutterFragmentActivity() { obj.put("relative_dir", "") return obj.toString() } + val safeRelativeDir = sanitizeRelativeDir(relativeDir) + val safeFileName = sanitizeFilename(fileName) + if (safeFileName.isBlank()) { + obj.put("uri", "") + obj.put("relative_dir", "") + return obj.toString() + } val treeUri = Uri.parse(treeUriStr) - val targetDir = findDocumentDir(treeUri, relativeDir) + val targetDir = findDocumentDir(treeUri, safeRelativeDir) if (targetDir != null) { - val direct = targetDir.findFile(fileName) + val direct = targetDir.findFile(safeFileName) if (direct != null && direct.isFile) { obj.put("uri", direct.uri.toString()) - obj.put("relative_dir", relativeDir) + obj.put("relative_dir", safeRelativeDir) return obj.toString() } } @@ -410,7 +428,7 @@ class MainActivity: FlutterFragmentActivity() { val childPath = if (path.isBlank()) childName else "$path/$childName" queue.add(child to childPath) } else if (child.isFile) { - if (child.name == fileName) { + if (child.name == safeFileName) { obj.put("uri", child.uri.toString()) obj.put("relative_dir", path) return obj.toString() @@ -426,7 +444,7 @@ class MainActivity: FlutterFragmentActivity() { private fun buildSafFileName(req: JSONObject, outputExt: String): String { val provided = req.optString("saf_file_name", "") - if (provided.isNotBlank()) return provided + if (provided.isNotBlank()) return sanitizeFilename(provided) val trackName = req.optString("track_name", "track") val artistName = req.optString("artist_name", "") @@ -617,7 +635,7 @@ class MainActivity: FlutterFragmentActivity() { } val treeUri = Uri.parse(treeUriStr) - val relativeDir = req.optString("saf_relative_dir", "") + val relativeDir = sanitizeRelativeDir(req.optString("saf_relative_dir", "")) val outputExt = normalizeExt(req.optString("saf_output_ext", "")) val mimeType = mimeTypeForExt(outputExt) val fileName = buildSafFileName(req, outputExt) @@ -1474,11 +1492,12 @@ class MainActivity: FlutterFragmentActivity() { "safCreateFromPath" -> { val treeUriStr = call.argument("tree_uri") ?: "" val relativeDir = call.argument("relative_dir") ?: "" - val fileName = call.argument("file_name") ?: "" + val fileName = sanitizeFilename(call.argument("file_name") ?: "") val mimeType = call.argument("mime_type") ?: "application/octet-stream" val srcPath = call.argument("src_path") ?: "" val createdUri = withContext(Dispatchers.IO) { if (treeUriStr.isBlank()) return@withContext null + if (fileName.isBlank()) return@withContext null val dir = ensureDocumentDir(Uri.parse(treeUriStr), relativeDir) ?: return@withContext null val existing = dir.findFile(fileName) val createdNew = existing == null diff --git a/go_backend/exports.go b/go_backend/exports.go index 0e2c1b5a..91f68711 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -2906,14 +2906,26 @@ func GetStoreCategoriesJSON() (string, error) { return string(jsonBytes), nil } +func buildStoreExtensionDestPath(destDir, extensionID string) (string, error) { + if strings.TrimSpace(extensionID) == "" { + return "", fmt.Errorf("invalid extension id") + } + + safeExtensionID := sanitizeFilename(extensionID) + return filepath.Join(destDir, safeExtensionID+".spotiflac-ext"), nil +} + func DownloadStoreExtensionJSON(extensionID, destDir string) (string, error) { store := GetExtensionStore() if store == nil { return "", fmt.Errorf("extension store not initialized") } - destPath := fmt.Sprintf("%s/%s.spotiflac-ext", destDir, extensionID) - err := store.DownloadExtension(extensionID, destPath) + destPath, err := buildStoreExtensionDestPath(destDir, extensionID) + if err != nil { + return "", err + } + err = store.DownloadExtension(extensionID, destPath) if err != nil { return "", err } diff --git a/go_backend/extension_runtime_auth.go b/go_backend/extension_runtime_auth.go index eb17eb37..de4ed06c 100644 --- a/go_backend/extension_runtime_auth.go +++ b/go_backend/extension_runtime_auth.go @@ -18,6 +18,43 @@ import ( // ==================== Auth API (OAuth Support) ==================== +func validateExtensionAuthURL(urlStr string) error { + parsed, err := url.Parse(urlStr) + if err != nil { + return fmt.Errorf("invalid auth URL: %w", err) + } + + if parsed.Scheme != "https" { + return fmt.Errorf("invalid auth URL: only https is allowed") + } + + host := parsed.Hostname() + if host == "" { + return fmt.Errorf("invalid auth URL: hostname is required") + } + + if parsed.User != nil { + return fmt.Errorf("invalid auth URL: embedded credentials are not allowed") + } + + if isPrivateIP(host) { + return fmt.Errorf("invalid auth URL: private/local network is not allowed") + } + + return nil +} + +func summarizeURLForLog(urlStr string) string { + parsed, err := url.Parse(urlStr) + if err != nil { + return urlStr + } + if parsed.Host == "" { + return parsed.Scheme + "://" + } + return fmt.Sprintf("%s://%s%s", parsed.Scheme, parsed.Host, parsed.Path) +} + func (r *ExtensionRuntime) authOpenUrl(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue(map[string]interface{}{ @@ -32,6 +69,13 @@ func (r *ExtensionRuntime) authOpenUrl(call goja.FunctionCall) goja.Value { callbackURL = call.Arguments[1].String() } + if err := validateExtensionAuthURL(authURL); err != nil { + return r.vm.ToValue(map[string]interface{}{ + "success": false, + "error": err.Error(), + }) + } + pendingAuthRequestsMu.Lock() pendingAuthRequests[r.extensionID] = &PendingAuthRequest{ ExtensionID: r.extensionID, @@ -50,7 +94,7 @@ func (r *ExtensionRuntime) authOpenUrl(call goja.FunctionCall) goja.Value { state.AuthCode = "" extensionAuthStateMu.Unlock() - GoLog("[Extension:%s] Auth URL requested: %s\n", r.extensionID, authURL) + GoLog("[Extension:%s] Auth URL requested: %s\n", r.extensionID, summarizeURLForLog(authURL)) return r.vm.ToValue(map[string]interface{}{ "success": true, @@ -273,6 +317,12 @@ func (r *ExtensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V "error": "authUrl, clientId, and redirectUri are required", }) } + if err := validateExtensionAuthURL(authURL); err != nil { + return r.vm.ToValue(map[string]interface{}{ + "success": false, + "error": err.Error(), + }) + } scope, _ := config["scope"].(string) extraParams, _ := config["extraParams"].(map[string]interface{}) @@ -331,7 +381,7 @@ func (r *ExtensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V } pendingAuthRequestsMu.Unlock() - GoLog("[Extension:%s] PKCE OAuth started: %s\n", r.extensionID, fullAuthURL) + GoLog("[Extension:%s] PKCE OAuth started: %s\n", r.extensionID, summarizeURLForLog(fullAuthURL)) return r.vm.ToValue(map[string]interface{}{ "success": true, @@ -441,13 +491,17 @@ func (r *ExtensionRuntime) authExchangeCodeWithPKCE(call goja.FunctionCall) goja "error": err.Error(), }) } + bodyPreview := sanitizeSensitiveLogText(string(body)) + if len(bodyPreview) > 1000 { + bodyPreview = bodyPreview[:1000] + "...[truncated]" + } var tokenResp map[string]interface{} if err := json.Unmarshal(body, &tokenResp); err != nil { return r.vm.ToValue(map[string]interface{}{ "success": false, "error": fmt.Sprintf("failed to parse token response: %v", err), - "body": string(body), + "body": bodyPreview, }) } @@ -468,7 +522,7 @@ func (r *ExtensionRuntime) authExchangeCodeWithPKCE(call goja.FunctionCall) goja return r.vm.ToValue(map[string]interface{}{ "success": false, "error": "no access_token in response", - "body": string(body), + "body": bodyPreview, }) } diff --git a/go_backend/extension_runtime_http.go b/go_backend/extension_runtime_http.go index a5f44617..dcdd32f4 100644 --- a/go_backend/extension_runtime_http.go +++ b/go_backend/extension_runtime_http.go @@ -32,6 +32,9 @@ func (r *ExtensionRuntime) validateDomain(urlStr string) error { if parsed.Scheme != "https" { return fmt.Errorf("network access denied: only https is allowed") } + if parsed.User != nil { + return fmt.Errorf("invalid URL: embedded credentials are not allowed") + } domain := parsed.Hostname() if domain == "" { diff --git a/go_backend/extension_runtime_storage.go b/go_backend/extension_runtime_storage.go index 5e9770ff..2e85033f 100644 --- a/go_backend/extension_runtime_storage.go +++ b/go_backend/extension_runtime_storage.go @@ -46,7 +46,7 @@ func (r *ExtensionRuntime) saveStorage(storage map[string]interface{}) error { return err } - return os.WriteFile(storagePath, data, 0644) + return os.WriteFile(storagePath, data, 0600) } func (r *ExtensionRuntime) storageGet(call goja.FunctionCall) goja.Value { diff --git a/go_backend/logbuffer.go b/go_backend/logbuffer.go index 5b11797e..95012593 100644 --- a/go_backend/logbuffer.go +++ b/go_backend/logbuffer.go @@ -3,6 +3,7 @@ package gobackend import ( "encoding/json" "fmt" + "regexp" "strings" "sync" "time" @@ -30,8 +31,22 @@ const ( var ( globalLogBuffer *LogBuffer logBufferOnce sync.Once + + authorizationBearerPattern = regexp.MustCompile(`(?i)\bAuthorization\b\s*[:=]\s*Bearer\s+[A-Za-z0-9._~+/\-]+=*`) + genericKeyValuePattern = regexp.MustCompile(`(?i)\b(access[_\s-]?token|refresh[_\s-]?token|id[_\s-]?token|client[_\s-]?secret|authorization|password|api[_\s-]?key)\b(\s*[:=]\s*)([^\s,;]+)`) + queryTokenPattern = regexp.MustCompile(`(?i)([?&](?:access_token|refresh_token|id_token|token|client_secret|api_key|apikey|password)=)[^&\s]+`) + bearerTokenPattern = regexp.MustCompile(`(?i)\bBearer\s+[A-Za-z0-9._~+/\-]+=*`) ) +func sanitizeSensitiveLogText(message string) string { + redacted := message + redacted = authorizationBearerPattern.ReplaceAllString(redacted, "Authorization: Bearer [REDACTED]") + redacted = genericKeyValuePattern.ReplaceAllString(redacted, `${1}${2}[REDACTED]`) + redacted = queryTokenPattern.ReplaceAllString(redacted, `${1}[REDACTED]`) + redacted = bearerTokenPattern.ReplaceAllString(redacted, "Bearer [REDACTED]") + return redacted +} + func GetLogBuffer() *LogBuffer { logBufferOnce.Do(func() { globalLogBuffer = &LogBuffer{ @@ -71,6 +86,7 @@ func (lb *LogBuffer) Add(level, tag, message string) { return } + message = sanitizeSensitiveLogText(message) message = truncateLogMessage(message) entry := LogEntry{ diff --git a/go_backend/security_hardening_test.go b/go_backend/security_hardening_test.go new file mode 100644 index 00000000..559ccbd8 --- /dev/null +++ b/go_backend/security_hardening_test.go @@ -0,0 +1,80 @@ +package gobackend + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestSanitizeSensitiveLogText(t *testing.T) { + input := "access_token=abc123 Authorization:Bearer xyz456 https://api.example.com/cb?refresh_token=zzz" + redacted := sanitizeSensitiveLogText(input) + + if strings.Contains(redacted, "abc123") || strings.Contains(redacted, "xyz456") || strings.Contains(redacted, "zzz") { + t.Fatalf("expected sensitive values to be redacted, got: %s", redacted) + } + if !strings.Contains(redacted, "[REDACTED]") { + t.Fatalf("expected redaction marker in output, got: %s", redacted) + } +} + +func TestValidateExtensionAuthURL(t *testing.T) { + if err := validateExtensionAuthURL("https://accounts.example.com/oauth/authorize"); err != nil { + t.Fatalf("expected valid auth URL, got error: %v", err) + } + + blocked := []string{ + "http://accounts.example.com/oauth/authorize", + "https://user:pass@accounts.example.com/oauth/authorize", + "https://localhost/oauth/authorize", + } + + for _, rawURL := range blocked { + if err := validateExtensionAuthURL(rawURL); err == nil { + t.Fatalf("expected URL to be blocked: %s", rawURL) + } + } +} + +func TestValidateDomainRejectsEmbeddedCredentials(t *testing.T) { + ext := &LoadedExtension{ + ID: "test-ext", + Manifest: &ExtensionManifest{ + Name: "test-ext", + Permissions: ExtensionPermissions{ + Network: []string{"api.example.com"}, + }, + }, + DataDir: t.TempDir(), + } + + runtime := NewExtensionRuntime(ext) + if err := runtime.validateDomain("https://user:pass@api.example.com/resource"); err == nil { + t.Fatal("expected embedded URL credentials to be rejected") + } +} + +func TestBuildStoreExtensionDestPath(t *testing.T) { + baseDir := t.TempDir() + + destPath, err := buildStoreExtensionDestPath(baseDir, "../evil/name") + if err != nil { + t.Fatalf("expected sanitized path to be generated, got error: %v", err) + } + + if !isPathWithinBase(baseDir, destPath) { + t.Fatalf("expected destination path to remain under base dir: %s", destPath) + } + + baseName := filepath.Base(destPath) + if strings.Contains(baseName, "/") || strings.Contains(baseName, `\`) { + t.Fatalf("expected filename to be sanitized, got: %s", baseName) + } + if !strings.HasSuffix(baseName, ".spotiflac-ext") { + t.Fatalf("expected .spotiflac-ext suffix, got: %s", baseName) + } + + if _, err := buildStoreExtensionDestPath(baseDir, " "); err == nil { + t.Fatal("expected empty extension id to be rejected") + } +} diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 9a74729c..606bff6b 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -208,16 +208,11 @@ class DownloadHistoryItem { class DownloadHistoryState { final List items; - final Set _downloadedSpotifyIds; final Map _bySpotifyId; final Map _byIsrc; DownloadHistoryState({this.items = const []}) - : _downloadedSpotifyIds = items - .where((item) => item.spotifyId != null && item.spotifyId!.isNotEmpty) - .map((item) => item.spotifyId!) - .toSet(), - _bySpotifyId = Map.fromEntries( + : _bySpotifyId = Map.fromEntries( items .where( (item) => item.spotifyId != null && item.spotifyId!.isNotEmpty, @@ -230,8 +225,7 @@ class DownloadHistoryState { .map((item) => MapEntry(item.isrc!, item)), ); - bool isDownloaded(String spotifyId) => - _downloadedSpotifyIds.contains(spotifyId); + bool isDownloaded(String spotifyId) => _bySpotifyId.containsKey(spotifyId); DownloadHistoryItem? getBySpotifyId(String spotifyId) => _bySpotifyId[spotifyId]; @@ -682,6 +676,7 @@ class DownloadQueueNotifier extends Notifier { bool _isLoaded = false; final Set _ensuredDirs = {}; int _progressPollingErrorCount = 0; + bool _isProgressPollingInFlight = false; String? _lastServiceTrackName; String? _lastServiceArtistName; int _lastServicePercent = -1; @@ -832,6 +827,8 @@ class DownloadQueueNotifier extends Notifier { void _startMultiProgressPolling() { _progressTimer?.cancel(); _progressTimer = Timer.periodic(_progressPollingInterval, (timer) async { + if (_isProgressPollingInFlight) return; + _isProgressPollingInFlight = true; try { final allProgress = await PlatformBridge.getAllDownloadProgress(); final items = allProgress['items'] as Map? ?? {}; @@ -915,16 +912,18 @@ class DownloadQueueNotifier extends Notifier { bytesReceived: normalizedBytes, ); - final mbReceived = bytesReceived / (1024 * 1024); - final mbTotal = bytesTotal / (1024 * 1024); - if (bytesTotal > 0) { - _log.d( - 'Progress [$itemId]: ${(percentage * 100).toStringAsFixed(1)}% (${mbReceived.toStringAsFixed(2)}/${mbTotal.toStringAsFixed(2)} MB) @ ${speedMBps.toStringAsFixed(2)} MB/s', - ); - } else { - _log.d( - 'Progress [$itemId]: ${(percentage * 100).toStringAsFixed(1)}% (DASH segments/unknown size) @ ${speedMBps.toStringAsFixed(2)} MB/s', - ); + if (LogBuffer.loggingEnabled) { + final mbReceived = bytesReceived / (1024 * 1024); + final mbTotal = bytesTotal / (1024 * 1024); + if (bytesTotal > 0) { + _log.d( + 'Progress [$itemId]: ${(percentage * 100).toStringAsFixed(1)}% (${mbReceived.toStringAsFixed(2)}/${mbTotal.toStringAsFixed(2)} MB) @ ${speedMBps.toStringAsFixed(2)} MB/s', + ); + } else { + _log.d( + 'Progress [$itemId]: ${(percentage * 100).toStringAsFixed(1)}% (DASH segments/unknown size) @ ${speedMBps.toStringAsFixed(2)} MB/s', + ); + } } } } @@ -1039,6 +1038,8 @@ class DownloadQueueNotifier extends Notifier { if (_progressPollingErrorCount <= 3) { _log.w('Progress polling failed: $e'); } + } finally { + _isProgressPollingInFlight = false; } }); } @@ -1088,6 +1089,7 @@ class DownloadQueueNotifier extends Notifier { _progressTimer?.cancel(); _progressTimer = null; _progressPollingErrorCount = 0; + _isProgressPollingInFlight = false; _lastServiceTrackName = null; _lastServiceArtistName = null; _lastServicePercent = -1; @@ -4011,15 +4013,29 @@ final downloadQueueProvider = class DownloadQueueLookup { final Map byTrackId; + final Map byItemId; + final List itemIds; - DownloadQueueLookup._(this.byTrackId); + DownloadQueueLookup._({ + required this.byTrackId, + required this.byItemId, + required this.itemIds, + }); factory DownloadQueueLookup.fromItems(List items) { - final map = {}; + final byTrackId = {}; + final byItemId = {}; + final itemIds = []; for (final item in items) { - map.putIfAbsent(item.track.id, () => item); + byTrackId.putIfAbsent(item.track.id, () => item); + byItemId[item.id] = item; + itemIds.add(item.id); } - return DownloadQueueLookup._(map); + return DownloadQueueLookup._( + byTrackId: byTrackId, + byItemId: byItemId, + itemIds: itemIds, + ); } } diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index 08764056..e02e0d32 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -24,7 +24,6 @@ class LocalLibraryState { final bool scanWasCancelled; final DateTime? lastScannedAt; final int excludedDownloadedCount; - final Set _isrcSet; final Set _trackKeySet; final Map _byIsrc; @@ -39,16 +38,9 @@ class LocalLibraryState { this.scanWasCancelled = false, this.lastScannedAt, this.excludedDownloadedCount = 0, - Set? isrcSet, Set? trackKeySet, Map? byIsrc, - }) : _isrcSet = - isrcSet ?? - items - .where((item) => item.isrc != null && item.isrc!.isNotEmpty) - .map((item) => item.isrc!) - .toSet(), - _trackKeySet = trackKeySet ?? items.map((item) => item.matchKey).toSet(), + }) : _trackKeySet = trackKeySet ?? items.map((item) => item.matchKey).toSet(), _byIsrc = byIsrc ?? Map.fromEntries( @@ -57,7 +49,7 @@ class LocalLibraryState { .map((item) => MapEntry(item.isrc!, item)), ); - bool hasIsrc(String isrc) => _isrcSet.contains(isrc); + bool hasIsrc(String isrc) => _byIsrc.containsKey(isrc); bool hasTrack(String trackName, String artistName) { final key = '${trackName.toLowerCase()}|${artistName.toLowerCase()}'; @@ -108,7 +100,6 @@ class LocalLibraryState { lastScannedAt: lastScannedAt ?? this.lastScannedAt, excludedDownloadedCount: excludedDownloadedCount ?? this.excludedDownloadedCount, - isrcSet: keepDerivedIndex ? _isrcSet : null, trackKeySet: keepDerivedIndex ? _trackKeySet : null, byIsrc: keepDerivedIndex ? _byIsrc : null, ); @@ -123,6 +114,7 @@ class LocalLibraryNotifier extends Notifier { bool _isLoaded = false; bool _scanCancelRequested = false; int _progressPollingErrorCount = 0; + bool _isProgressPollingInFlight = false; @override LocalLibraryState build() { @@ -408,6 +400,8 @@ class LocalLibraryNotifier extends Notifier { void _startProgressPolling() { _progressTimer?.cancel(); _progressTimer = Timer.periodic(_progressPollingInterval, (_) async { + if (_isProgressPollingInFlight) return; + _isProgressPollingInFlight = true; try { final progress = await PlatformBridge.getLibraryScanProgress(); final nextProgress = @@ -447,6 +441,8 @@ class LocalLibraryNotifier extends Notifier { if (_progressPollingErrorCount <= 3) { _log.w('Library scan progress polling failed: $e'); } + } finally { + _isProgressPollingInFlight = false; } }); } @@ -455,6 +451,7 @@ class LocalLibraryNotifier extends Notifier { _progressTimer?.cancel(); _progressTimer = null; _progressPollingErrorCount = 0; + _isProgressPollingInFlight = false; } Future cancelScan() async { diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 7101c124..3ecc340e 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -26,8 +26,10 @@ class TrackState { final List? searchPlaylists; // For search results (playlists) final bool hasSearchText; // For back button handling final bool isShowingRecentAccess; // For recent access mode - final String? searchExtensionId; // Extension ID used for current search results - final String? selectedSearchFilter; // Currently selected search filter (e.g., "track", "album", "artist", "playlist") + final String? + searchExtensionId; // Extension ID used for current search results + final String? + selectedSearchFilter; // Currently selected search filter (e.g., "track", "album", "artist", "playlist") const TrackState({ this.tracks = const [], @@ -52,7 +54,12 @@ class TrackState { this.selectedSearchFilter, }); - bool get hasContent => tracks.isNotEmpty || artistAlbums != null || (searchArtists != null && searchArtists!.isNotEmpty) || (searchAlbums != null && searchAlbums!.isNotEmpty) || (searchPlaylists != null && searchPlaylists!.isNotEmpty); + bool get hasContent => + tracks.isNotEmpty || + artistAlbums != null || + (searchArtists != null && searchArtists!.isNotEmpty) || + (searchAlbums != null && searchAlbums!.isNotEmpty) || + (searchPlaylists != null && searchPlaylists!.isNotEmpty); TrackState copyWith({ List? tracks, @@ -95,9 +102,12 @@ class TrackState { searchAlbums: searchAlbums ?? this.searchAlbums, searchPlaylists: searchPlaylists ?? this.searchPlaylists, hasSearchText: hasSearchText ?? this.hasSearchText, - isShowingRecentAccess: isShowingRecentAccess ?? this.isShowingRecentAccess, + isShowingRecentAccess: + isShowingRecentAccess ?? this.isShowingRecentAccess, searchExtensionId: searchExtensionId, - selectedSearchFilter: clearSelectedSearchFilter ? null : (selectedSearchFilter ?? this.selectedSearchFilter), + selectedSearchFilter: clearSelectedSearchFilter + ? null + : (selectedSearchFilter ?? this.selectedSearchFilter), ); } } @@ -178,6 +188,7 @@ class SearchPlaylist { class TrackNotifier extends Notifier { int _currentRequestId = 0; + static const int _maxPreWarmTracksPerRequest = 80; @override TrackState build() { @@ -197,39 +208,42 @@ class TrackNotifier extends Notifier { final extensionHandler = await PlatformBridge.findURLHandler(url); if (extensionHandler != null) { _log.i('Found extension URL handler: $extensionHandler for URL: $url'); - + // Retry logic for extension URL handlers (up to 3 attempts) Map? result; for (int attempt = 1; attempt <= 3; attempt++) { result = await PlatformBridge.handleURLWithExtension(url); if (!_isRequestValid(requestId)) return; - + // Check if we got valid data - if (result != null && result['type'] == 'track' && result['track'] != null) { + if (result != null && + result['type'] == 'track' && + result['track'] != null) { final trackData = result['track'] as Map; final name = trackData['name']?.toString() ?? ''; if (name.isNotEmpty) { break; } - } else if (result != null && (result['type'] == 'album' || result['type'] == 'playlist')) { + } else if (result != null && + (result['type'] == 'album' || result['type'] == 'playlist')) { break; } else if (result != null && result['type'] == 'artist') { break; } - + if (attempt < 3) { await Future.delayed(const Duration(milliseconds: 500)); } } - + if (result != null) { final type = result['type'] as String?; final extensionId = result['extension_id'] as String?; - + if (type == 'track' && result['track'] != null) { final trackData = result['track'] as Map; final track = _parseSearchTrack(trackData, source: extensionId); - + if (track.name.isEmpty) { state = TrackState( isLoading: false, @@ -237,7 +251,7 @@ class TrackNotifier extends Notifier { ); return; } - + state = TrackState( tracks: [track], isLoading: false, @@ -245,15 +259,27 @@ class TrackNotifier extends Notifier { searchExtensionId: extensionId, ); return; - } else if ((type == 'album' || type == 'playlist') && result['tracks'] != null) { + } else if ((type == 'album' || type == 'playlist') && + result['tracks'] != null) { final trackList = result['tracks'] as List; - final tracks = trackList.map((t) => _parseSearchTrack(t as Map, source: extensionId)).toList(); + final tracks = trackList + .map( + (t) => _parseSearchTrack( + t as Map, + source: extensionId, + ), + ) + .toList(); state = TrackState( tracks: tracks, isLoading: false, albumId: result['album']?['id'] as String?, - albumName: result['name'] as String? ?? result['album']?['name'] as String?, - playlistName: type == 'playlist' ? result['name'] as String? : null, + albumName: + result['name'] as String? ?? + result['album']?['name'] as String?, + playlistName: type == 'playlist' + ? result['name'] as String? + : null, coverUrl: result['cover_url'] as String?, searchExtensionId: extensionId, ); @@ -261,17 +287,29 @@ class TrackNotifier extends Notifier { } else if (type == 'artist' && result['artist'] != null) { final artistData = result['artist'] as Map; final albumsList = artistData['albums'] as List? ?? []; - final albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); - - final topTracksList = artistData['top_tracks'] as List? ?? []; - final topTracks = topTracksList.map((t) => _parseSearchTrack(t as Map, source: extensionId)).toList(); - + final albums = albumsList + .map((a) => _parseArtistAlbum(a as Map)) + .toList(); + + final topTracksList = + artistData['top_tracks'] as List? ?? []; + final topTracks = topTracksList + .map( + (t) => _parseSearchTrack( + t as Map, + source: extensionId, + ), + ) + .toList(); + state = TrackState( tracks: [], isLoading: false, artistId: artistData['id'] as String?, artistName: artistData['name'] as String?, - coverUrl: artistData['image_url'] as String? ?? artistData['images'] as String?, + coverUrl: + artistData['image_url'] as String? ?? + artistData['images'] as String?, headerImageUrl: artistData['header_image'] as String?, monthlyListeners: artistData['listeners'] as int?, artistAlbums: albums, @@ -282,19 +320,19 @@ class TrackNotifier extends Notifier { } } } - + // Step 2: Try Deezer URL parsing if (url.contains('deezer.com') || url.contains('deezer.page.link')) { _log.i('Detected Deezer URL, parsing...'); final parsed = await PlatformBridge.parseDeezerUrl(url); if (!_isRequestValid(requestId)) return; - + final type = parsed['type'] as String; final id = parsed['id'] as String; - + final metadata = await PlatformBridge.getDeezerMetadata(type, id); if (!_isRequestValid(requestId)) return; - + if (type == 'track') { final trackData = metadata['track'] as Map; final track = _parseTrack(trackData); @@ -306,7 +344,9 @@ class TrackNotifier extends Notifier { } else if (type == 'album') { final albumInfo = metadata['album_info'] as Map; final trackList = metadata['track_list'] as List; - final tracks = trackList.map((t) => _parseTrack(t as Map)).toList(); + final tracks = trackList + .map((t) => _parseTrack(t as Map)) + .toList(); state = TrackState( tracks: tracks, isLoading: false, @@ -316,9 +356,12 @@ class TrackNotifier extends Notifier { ); _preWarmCacheForTracks(tracks); } else if (type == 'playlist') { - final playlistInfo = metadata['playlist_info'] as Map; + final playlistInfo = + metadata['playlist_info'] as Map; final trackList = metadata['track_list'] as List; - final tracks = trackList.map((t) => _parseTrack(t as Map)).toList(); + final tracks = trackList + .map((t) => _parseTrack(t as Map)) + .toList(); state = TrackState( tracks: tracks, isLoading: false, @@ -329,7 +372,9 @@ class TrackNotifier extends Notifier { } else if (type == 'artist') { final artistInfo = metadata['artist_info'] as Map; final albumsList = metadata['albums'] as List; - final albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); + final albums = albumsList + .map((a) => _parseArtistAlbum(a as Map)) + .toList(); state = TrackState( tracks: [], isLoading: false, @@ -341,33 +386,38 @@ class TrackNotifier extends Notifier { } return; } - + // Step 3: Try Tidal URL parsing if (url.contains('tidal.com')) { _log.i('Detected Tidal URL, parsing...'); final parsed = await PlatformBridge.parseTidalUrl(url); if (!_isRequestValid(requestId)) return; - + final type = parsed['type'] as String; final id = parsed['id'] as String; - + _log.i('Tidal URL parsed: type=$type, id=$id'); - + // For track URLs, convert to Spotify/Deezer and fetch metadata from there if (type == 'track') { try { _log.i('Converting Tidal track to Spotify/Deezer via SongLink...'); - final conversion = await PlatformBridge.convertTidalToSpotifyDeezer(url); + final conversion = await PlatformBridge.convertTidalToSpotifyDeezer( + url, + ); if (!_isRequestValid(requestId)) return; - + final spotifyUrl = conversion['spotify_url'] as String?; final deezerUrl = conversion['deezer_url'] as String?; - + if (spotifyUrl != null && spotifyUrl.isNotEmpty) { _log.i('Found Spotify URL: $spotifyUrl, fetching metadata...'); - final metadata = await PlatformBridge.getSpotifyMetadataWithFallback(spotifyUrl); + final metadata = + await PlatformBridge.getSpotifyMetadataWithFallback( + spotifyUrl, + ); if (!_isRequestValid(requestId)) return; - + final trackData = metadata['track'] as Map; final track = _parseTrack(trackData); state = TrackState( @@ -378,10 +428,15 @@ class TrackNotifier extends Notifier { return; } else if (deezerUrl != null && deezerUrl.isNotEmpty) { _log.i('Found Deezer URL: $deezerUrl, fetching metadata...'); - final deezerParsed = await PlatformBridge.parseDeezerUrl(deezerUrl); - final metadata = await PlatformBridge.getDeezerMetadata('track', deezerParsed['id'] as String); + final deezerParsed = await PlatformBridge.parseDeezerUrl( + deezerUrl, + ); + final metadata = await PlatformBridge.getDeezerMetadata( + 'track', + deezerParsed['id'] as String, + ); if (!_isRequestValid(requestId)) return; - + final trackData = metadata['track'] as Map; final track = _parseTrack(trackData); state = TrackState( @@ -395,30 +450,31 @@ class TrackNotifier extends Notifier { _log.w('Failed to convert Tidal URL via SongLink: $e'); } } - + // For album/artist/playlist, not yet supported state = TrackState( isLoading: false, - error: 'Tidal $type links are not fully supported yet. Only track links work via SongLink conversion.', + error: + 'Tidal $type links are not fully supported yet. Only track links work via SongLink conversion.', hasSearchText: state.hasSearchText, ); return; } - + // Step 4: Fall back to Spotify parsing final parsed = await PlatformBridge.parseSpotifyUrl(url); if (!_isRequestValid(requestId)) return; - + final type = parsed['type'] as String; Map metadata; - + try { metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url); } catch (e) { rethrow; } - + if (!_isRequestValid(requestId)) return; if (type == 'track') { @@ -432,7 +488,9 @@ class TrackNotifier extends Notifier { } else if (type == 'album') { final albumInfo = metadata['album_info'] as Map; final trackList = metadata['track_list'] as List; - final tracks = trackList.map((t) => _parseTrack(t as Map)).toList(); + final tracks = trackList + .map((t) => _parseTrack(t as Map)) + .toList(); state = TrackState( tracks: tracks, isLoading: false, @@ -444,7 +502,9 @@ class TrackNotifier extends Notifier { } else if (type == 'playlist') { final playlistInfo = metadata['playlist_info'] as Map; final trackList = metadata['track_list'] as List; - final tracks = trackList.map((t) => _parseTrack(t as Map)).toList(); + final tracks = trackList + .map((t) => _parseTrack(t as Map)) + .toList(); final owner = playlistInfo['owner'] as Map?; state = TrackState( tracks: tracks, @@ -456,7 +516,9 @@ class TrackNotifier extends Notifier { } else if (type == 'artist') { final artistInfo = metadata['artist_info'] as Map; final albumsList = metadata['albums'] as List; - final albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); + final albums = albumsList + .map((a) => _parseArtistAlbum(a as Map)) + .toList(); state = TrackState( tracks: [], isLoading: false, @@ -468,17 +530,29 @@ class TrackNotifier extends Notifier { } } catch (e) { if (!_isRequestValid(requestId)) return; - state = TrackState(isLoading: false, error: e.toString(), hasSearchText: state.hasSearchText); + state = TrackState( + isLoading: false, + error: e.toString(), + hasSearchText: state.hasSearchText, + ); } } - Future search(String query, {String? metadataSource, String? filterOverride}) async { + Future search( + String query, { + String? metadataSource, + String? filterOverride, + }) async { final requestId = ++_currentRequestId; - + // Preserve selected filter during loading final currentFilter = filterOverride ?? state.selectedSearchFilter; - state = TrackState(isLoading: true, hasSearchText: state.hasSearchText, selectedSearchFilter: currentFilter); + state = TrackState( + isLoading: true, + hasSearchText: state.hasSearchText, + selectedSearchFilter: currentFilter, + ); try { final settings = ref.read(settingsProvider); @@ -494,20 +568,23 @@ class TrackNotifier extends Notifier { searchProvider.isNotEmpty; final source = metadataSource ?? 'deezer'; - + _log.i( 'Search started: source=$source, query="$query", useExtensions=$useExtensions, filter=$currentFilter', ); - + Map results; List extensionTracks = []; - + if (useExtensions) { try { _log.d('Calling extension search API...'); - final extResults = await PlatformBridge.searchTracksWithExtensions(query, limit: 20); + final extResults = await PlatformBridge.searchTracksWithExtensions( + query, + limit: 20, + ); _log.i('Extensions returned ${extResults.length} tracks'); - + for (final t in extResults) { try { extensionTracks.add(_parseSearchTrack(t)); @@ -519,37 +596,52 @@ class TrackNotifier extends Notifier { _log.w('Extension search failed, falling back to built-in: $e'); } } - + if (source == 'deezer') { _log.d('Calling Deezer search API...'); - results = await PlatformBridge.searchDeezerAll(query, trackLimit: 20, artistLimit: 2, filter: currentFilter); - _log.i('Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums'); + results = await PlatformBridge.searchDeezerAll( + query, + trackLimit: 20, + artistLimit: 2, + filter: currentFilter, + ); + _log.i( + 'Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums', + ); } else { _log.d('Calling Spotify search API...'); - results = await PlatformBridge.searchSpotifyAll(query, trackLimit: 20, artistLimit: 2); - _log.i('Spotify returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists'); + results = await PlatformBridge.searchSpotifyAll( + query, + trackLimit: 20, + artistLimit: 2, + ); + _log.i( + 'Spotify returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists', + ); } - + if (!_isRequestValid(requestId)) { _log.w('Search request cancelled (requestId=$requestId)'); return; } - + final trackList = results['tracks'] as List? ?? []; final artistList = results['artists'] as List? ?? []; final albumList = results['albums'] as List? ?? []; - - _log.d('Raw results: ${trackList.length} tracks, ${artistList.length} artists, ${albumList.length} albums'); - + + _log.d( + 'Raw results: ${trackList.length} tracks, ${artistList.length} artists, ${albumList.length} albums', + ); + final tracks = []; - + tracks.addAll(extensionTracks); - + final existingIsrcs = extensionTracks .where((t) => t.isrc != null && t.isrc!.isNotEmpty) .map((t) => t.isrc!) .toSet(); - + for (int i = 0; i < trackList.length; i++) { final t = trackList[i]; try { @@ -566,7 +658,7 @@ class TrackNotifier extends Notifier { _log.e('Failed to parse track[$i]: $e', e); } } - + final artists = []; for (int i = 0; i < artistList.length; i++) { final a = artistList[i]; @@ -580,7 +672,7 @@ class TrackNotifier extends Notifier { _log.e('Failed to parse artist[$i]: $e', e); } } - + final albums = []; for (int i = 0; i < albumList.length; i++) { final a = albumList[i]; @@ -594,7 +686,7 @@ class TrackNotifier extends Notifier { _log.e('Failed to parse album[$i]: $e', e); } } - + final playlistList = results['playlists'] as List? ?? []; final playlists = []; for (int i = 0; i < playlistList.length; i++) { @@ -609,9 +701,11 @@ class TrackNotifier extends Notifier { _log.e('Failed to parse playlist[$i]: $e', e); } } - - _log.i('Search complete: ${tracks.length} tracks (${extensionTracks.length} from extensions), ${artists.length} artists, ${albums.length} albums, ${playlists.length} playlists parsed successfully'); - + + _log.i( + 'Search complete: ${tracks.length} tracks (${extensionTracks.length} from extensions), ${artists.length} artists, ${albums.length} albums, ${playlists.length} playlists parsed successfully', + ); + state = TrackState( tracks: tracks, searchArtists: artists, @@ -624,31 +718,45 @@ class TrackNotifier extends Notifier { } catch (e, stackTrace) { if (!_isRequestValid(requestId)) return; _log.e('Search failed: $e', e, stackTrace); - state = TrackState(isLoading: false, error: e.toString(), hasSearchText: state.hasSearchText, selectedSearchFilter: currentFilter); + state = TrackState( + isLoading: false, + error: e.toString(), + hasSearchText: state.hasSearchText, + selectedSearchFilter: currentFilter, + ); } } - Future customSearch(String extensionId, String query, {Map? options}) async { + Future customSearch( + String extensionId, + String query, { + Map? options, + }) async { final requestId = ++_currentRequestId; state = TrackState( isLoading: true, hasSearchText: state.hasSearchText, - selectedSearchFilter: state.selectedSearchFilter, // Preserve filter during loading + selectedSearchFilter: + state.selectedSearchFilter, // Preserve filter during loading ); try { _log.i('Custom search started: extension=$extensionId, query="$query"'); - - final results = await PlatformBridge.customSearchWithExtension(extensionId, query, options: options); - + + final results = await PlatformBridge.customSearchWithExtension( + extensionId, + query, + options: options, + ); + if (!_isRequestValid(requestId)) { _log.w('Custom search request cancelled (requestId=$requestId)'); return; } - + _log.i('Custom search returned ${results.length} tracks'); - + final tracks = []; for (int i = 0; i < results.length; i++) { final t = results[i]; @@ -658,21 +766,28 @@ class TrackNotifier extends Notifier { _log.e('Failed to parse custom search track[$i]: $e', e); } } - - _log.i('Custom search complete: ${tracks.length} tracks parsed (source=$extensionId)'); - + + _log.i( + 'Custom search complete: ${tracks.length} tracks parsed (source=$extensionId)', + ); + state = TrackState( tracks: tracks, searchArtists: [], isLoading: false, hasSearchText: state.hasSearchText, searchExtensionId: extensionId, // Store which extension was used - selectedSearchFilter: state.selectedSearchFilter, // Preserve selected filter + selectedSearchFilter: + state.selectedSearchFilter, // Preserve selected filter ); } catch (e, stackTrace) { if (!_isRequestValid(requestId)) return; _log.e('Custom search failed: $e', e, stackTrace); - state = TrackState(isLoading: false, error: e.toString(), hasSearchText: state.hasSearchText); + state = TrackState( + isLoading: false, + error: e.toString(), + hasSearchText: state.hasSearchText, + ); } } @@ -683,7 +798,10 @@ class TrackNotifier extends Notifier { if (track.isrc == null || track.isrc!.isEmpty) return; try { - final availability = await PlatformBridge.checkAvailability(track.id, track.isrc!); + final availability = await PlatformBridge.checkAvailability( + track.id, + track.isrc!, + ); final updatedTrack = Track( id: track.id, name: track.name, @@ -736,11 +854,14 @@ class TrackNotifier extends Notifier { } state = state.copyWith(hasSearchText: hasText); } - + void setShowingRecentAccess(bool showing) { + if (state.isShowingRecentAccess == showing) { + return; + } state = state.copyWith(isShowingRecentAccess: showing); } - + /// Set tracks from a collection (album/playlist) opened from search results void setTracksFromCollection({ required List tracks, @@ -782,9 +903,9 @@ class TrackNotifier extends Notifier { } else if (durationValue is double) { durationMs = durationValue.toInt(); } - + final itemType = data['item_type']?.toString(); - + return Track( id: (data['spotify_id'] ?? data['id'] ?? '').toString(), name: (data['name'] ?? '').toString(), @@ -797,7 +918,10 @@ class TrackNotifier extends Notifier { trackNumber: data['track_number'] as int?, discNumber: data['disc_number'] as int?, releaseDate: data['release_date']?.toString(), - source: source ?? data['source']?.toString() ?? data['provider_id']?.toString(), + source: + source ?? + data['source']?.toString() ?? + data['provider_id']?.toString(), albumType: data['album_type']?.toString(), itemType: itemType, ); @@ -849,16 +973,25 @@ class TrackNotifier extends Notifier { } void _preWarmCacheForTracks(List tracks) { - final tracksWithIsrc = tracks.where((t) => t.isrc != null && t.isrc!.isNotEmpty).toList(); - if (tracksWithIsrc.isEmpty) return; - - final cacheRequests = tracksWithIsrc.map((t) => { - 'isrc': t.isrc!, - 'track_name': t.name, - 'artist_name': t.artistName, - 'spotify_id': t.id, // Include Spotify ID for Amazon lookup - 'service': 'tidal', - }).toList(); + if (tracks.isEmpty) return; + final cacheRequests = >[]; + for (final track in tracks) { + final isrc = track.isrc; + if (isrc == null || isrc.isEmpty) { + continue; + } + cacheRequests.add({ + 'isrc': isrc, + 'track_name': track.name, + 'artist_name': track.artistName, + 'spotify_id': track.id, // Include Spotify ID for Amazon lookup + 'service': 'tidal', + }); + if (cacheRequests.length >= _maxPreWarmTracksPerRequest) { + break; + } + } + if (cacheRequests.isEmpty) return; PlatformBridge.preWarmTrackCache(cacheRequests).catchError((_) {}); } diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index d6950a9e..a36d85ca 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -34,6 +34,12 @@ class _DownloadedAlbumScreenState extends ConsumerState { final Set _selectedIds = {}; bool _showTitleInAppBar = false; final ScrollController _scrollController = ScrollController(); + bool _embeddedCoverRefreshScheduled = false; + List? _albumTracksSourceCache; + List? _albumTracksCache; + + String get _albumLookupKey => + '${widget.albumName.toLowerCase()}|${widget.artistName.toLowerCase()}'; @override void initState() { @@ -48,6 +54,16 @@ class _DownloadedAlbumScreenState extends ConsumerState { super.dispose(); } + @override + void didUpdateWidget(covariant DownloadedAlbumScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.albumName != widget.albumName || + oldWidget.artistName != widget.artistName) { + _albumTracksSourceCache = null; + _albumTracksCache = null; + } + } + void _onScroll() { final shouldShow = _scrollController.offset > 280; if (shouldShow != _showTitleInAppBar) { @@ -59,28 +75,36 @@ class _DownloadedAlbumScreenState extends ConsumerState { List _getAlbumTracks( List allItems, ) { - return allItems.where((item) { - // Use albumArtist if available and not empty, otherwise artistName - final itemArtist = - (item.albumArtist != null && item.albumArtist!.isNotEmpty) - ? item.albumArtist! - : item.artistName; - // Use lowercase for case-insensitive matching - final itemKey = - '${item.albumName.toLowerCase()}|${itemArtist.toLowerCase()}'; - final albumKey = - '${widget.albumName.toLowerCase()}|${widget.artistName.toLowerCase()}'; - return itemKey == albumKey; - }).toList()..sort((a, b) { - // Sort by disc number first, then by track number - final aDisc = a.discNumber ?? 1; - final bDisc = b.discNumber ?? 1; - if (aDisc != bDisc) return aDisc.compareTo(bDisc); - final aNum = a.trackNumber ?? 999; - final bNum = b.trackNumber ?? 999; - if (aNum != bNum) return aNum.compareTo(bNum); - return a.trackName.compareTo(b.trackName); - }); + final cached = _albumTracksCache; + if (cached != null && identical(allItems, _albumTracksSourceCache)) { + return cached; + } + + final tracks = + allItems.where((item) { + // Use albumArtist if available and not empty, otherwise artistName + final itemArtist = + (item.albumArtist != null && item.albumArtist!.isNotEmpty) + ? item.albumArtist! + : item.artistName; + // Use lowercase for case-insensitive matching + final itemKey = + '${item.albumName.toLowerCase()}|${itemArtist.toLowerCase()}'; + return itemKey == _albumLookupKey; + }).toList()..sort((a, b) { + // Sort by disc number first, then by track number + final aDisc = a.discNumber ?? 1; + final bDisc = b.discNumber ?? 1; + if (aDisc != bDisc) return aDisc.compareTo(bDisc); + final aNum = a.trackNumber ?? 999; + final bNum = b.trackNumber ?? 999; + if (aNum != bNum) return aNum.compareTo(bNum); + return a.trackName.compareTo(b.trackName); + }); + + _albumTracksSourceCache = allItems; + _albumTracksCache = tracks; + return tracks; } Map> _groupTracksByDisc( @@ -194,8 +218,14 @@ class _DownloadedAlbumScreenState extends ConsumerState { } void _onEmbeddedCoverChanged() { - if (!mounted) return; - setState(() {}); + if (!mounted || _embeddedCoverRefreshScheduled) return; + _embeddedCoverRefreshScheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _embeddedCoverRefreshScheduled = false; + if (mounted) { + setState(() {}); + } + }); } Future _navigateToMetadataScreen(DownloadHistoryItem item) async { diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index aa5c8d79..f60aa69b 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -35,18 +35,25 @@ class HomeTab extends ConsumerStatefulWidget { class _RecentAccessView { final List uniqueItems; - final List downloadItems; + final List downloadIds; final Map downloadFilePathByRecentKey; final bool hasHiddenDownloads; const _RecentAccessView({ required this.uniqueItems, - required this.downloadItems, + required this.downloadIds, required this.downloadFilePathByRecentKey, required this.hasHiddenDownloads, }); } +class _RecentAlbumAggregate { + int count; + DownloadHistoryItem mostRecent; + + _RecentAlbumAggregate({required this.count, required this.mostRecent}); +} + class _CsvImportOptions { final bool confirmed; final bool skipDownloaded; @@ -60,7 +67,6 @@ class _CsvImportOptions { class _HomeTabState extends ConsumerState with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { final _urlController = TextEditingController(); - bool _isTyping = false; final FocusNode _searchFocusNode = FocusNode(); String? _lastSearchQuery; late final ProviderSubscription _trackStateSub; @@ -77,6 +83,9 @@ class _HomeTabState extends ConsumerState List? _recentAccessItemsCache; Set? _recentAccessHiddenIdsCache; _RecentAccessView? _recentAccessViewCache; + bool _embeddedCoverRefreshScheduled = false; + List? _thumbnailSizesExtensionsCache; + Map? _thumbnailSizesCache; double _responsiveScale({ required BuildContext context, @@ -200,6 +209,27 @@ class _HomeTabState extends ConsumerState super.dispose(); } + Map _getThumbnailSizesByExtensionId( + List extensions, + ) { + final cached = _thumbnailSizesCache; + if (cached != null && + identical(extensions, _thumbnailSizesExtensionsCache)) { + return cached; + } + + final map = { + for (final extension in extensions) + if (extension.searchBehavior != null) + extension.id: extension.searchBehavior!.getThumbnailSize( + defaultSize: 56, + ), + }; + _thumbnailSizesExtensionsCache = extensions; + _thumbnailSizesCache = map; + return map; + } + void _onSearchFocusChanged() { if (mounted) { setState(() {}); @@ -217,7 +247,6 @@ class _HomeTabState extends ConsumerState _urlController.text.isNotEmpty && !_searchFocusNode.hasFocus) { _urlController.clear(); - setState(() => _isTyping = false); } } @@ -240,10 +269,7 @@ class _HomeTabState extends ConsumerState ref.read(trackProvider.notifier).setSearchText(text.isNotEmpty); - if (text.isNotEmpty && !_isTyping) { - setState(() => _isTyping = true); - } else if (text.isEmpty && _isTyping) { - setState(() => _isTyping = false); + if (text.isEmpty) { _liveSearchDebounce?.cancel(); return; } @@ -350,7 +376,6 @@ class _HomeTabState extends ConsumerState _urlController.clear(); _searchFocusNode.unfocus(); _lastSearchQuery = null; - setState(() => _isTyping = false); ref.read(trackProvider.notifier).clear(); } @@ -390,7 +415,6 @@ class _HomeTabState extends ConsumerState ); ref.read(trackProvider.notifier).clear(); _urlController.clear(); - setState(() => _isTyping = false); return; } @@ -416,7 +440,6 @@ class _HomeTabState extends ConsumerState ); ref.read(trackProvider.notifier).clear(); _urlController.clear(); - setState(() => _isTyping = false); return; } @@ -438,7 +461,6 @@ class _HomeTabState extends ConsumerState ); ref.read(trackProvider.notifier).clear(); _urlController.clear(); - setState(() => _isTyping = false); return; } } @@ -781,13 +803,9 @@ class _HomeTabState extends ConsumerState ); final showLocalLibraryIndicator = localLibrarySettings.$1 && localLibrarySettings.$2; - final thumbnailSizesByExtensionId = { - for (final extension in extensions) - if (extension.searchBehavior != null) - extension.id: extension.searchBehavior!.getThumbnailSize( - defaultSize: 56, - ), - }; + final thumbnailSizesByExtensionId = _getThumbnailSizesByExtensionId( + extensions, + ); Extension? currentSearchExtension; List searchFilters = []; @@ -1028,8 +1046,14 @@ class _HomeTabState extends ConsumerState } void _onEmbeddedCoverChanged() { - if (!mounted) return; - setState(() {}); + if (!mounted || _embeddedCoverRefreshScheduled) return; + _embeddedCoverRefreshScheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _embeddedCoverRefreshScheduled = false; + if (mounted) { + setState(() {}); + } + }); } Widget _buildRecentDownloads( @@ -1148,66 +1172,58 @@ class _HomeTabState extends ConsumerState return cached; } - final albumGroups = >{}; + final albumGroups = {}; for (final h in historyItems) { final artistForKey = (h.albumArtist != null && h.albumArtist!.isNotEmpty) ? h.albumArtist! : h.artistName; final albumKey = '${h.albumName}|$artistForKey'; - albumGroups.putIfAbsent(albumKey, () => []).add(h); + final existing = albumGroups[albumKey]; + if (existing == null) { + albumGroups[albumKey] = _RecentAlbumAggregate(count: 1, mostRecent: h); + } else { + existing.count++; + if (h.downloadedAt.isAfter(existing.mostRecent.downloadedAt)) { + existing.mostRecent = h; + } + } } - final downloadItems = []; + final downloadIds = []; + final visibleDownloads = []; final downloadFilePathByRecentKey = {}; - for (final entry in albumGroups.entries) { - final tracks = entry.value; - final mostRecent = tracks.reduce( - (a, b) => a.downloadedAt.isAfter(b.downloadedAt) ? a : b, - ); + for (final aggregate in albumGroups.values) { + final mostRecent = aggregate.mostRecent; final artistForKey = (mostRecent.albumArtist != null && mostRecent.albumArtist!.isNotEmpty) ? mostRecent.albumArtist! : mostRecent.artistName; - if (tracks.length == 1) { - final recent = RecentAccessItem( - id: mostRecent.spotifyId ?? mostRecent.id, - name: mostRecent.trackName, - subtitle: mostRecent.artistName, - imageUrl: mostRecent.coverUrl, - type: RecentAccessType.track, - accessedAt: mostRecent.downloadedAt, - providerId: 'download', - ); - downloadItems.add(recent); - downloadFilePathByRecentKey['${recent.type.name}:${recent.id}'] = - mostRecent.filePath; - } else { - final recent = RecentAccessItem( - id: '${mostRecent.albumName}|$artistForKey', - name: mostRecent.albumName, - subtitle: artistForKey, - imageUrl: mostRecent.coverUrl, - type: RecentAccessType.album, - accessedAt: mostRecent.downloadedAt, - providerId: 'download', - ); - downloadItems.add(recent); - downloadFilePathByRecentKey['${recent.type.name}:${recent.id}'] = - mostRecent.filePath; + final isSingleTrack = aggregate.count == 1; + final recentId = isSingleTrack + ? (mostRecent.spotifyId ?? mostRecent.id) + : '${mostRecent.albumName}|$artistForKey'; + final recent = RecentAccessItem( + id: recentId, + name: isSingleTrack ? mostRecent.trackName : mostRecent.albumName, + subtitle: isSingleTrack ? mostRecent.artistName : artistForKey, + imageUrl: mostRecent.coverUrl, + type: isSingleTrack ? RecentAccessType.track : RecentAccessType.album, + accessedAt: mostRecent.downloadedAt, + providerId: 'download', + ); + + downloadIds.add(recentId); + downloadFilePathByRecentKey['${recent.type.name}:${recent.id}'] = + mostRecent.filePath; + if (!hiddenIds.contains(recentId)) { + visibleDownloads.add(recent); } } - downloadItems.sort((a, b) => b.accessedAt.compareTo(a.accessedAt)); - - final visibleDownloads = []; - for (final item in downloadItems) { - if (!hiddenIds.contains(item.id)) { - visibleDownloads.add(item); - if (visibleDownloads.length >= 10) { - break; - } - } + visibleDownloads.sort((a, b) => b.accessedAt.compareTo(a.accessedAt)); + if (visibleDownloads.length > 10) { + visibleDownloads.removeRange(10, visibleDownloads.length); } final allItems = [...items, ...visibleDownloads]; @@ -1227,7 +1243,7 @@ class _HomeTabState extends ConsumerState final view = _RecentAccessView( uniqueItems: uniqueItems, - downloadItems: downloadItems, + downloadIds: downloadIds, downloadFilePathByRecentKey: downloadFilePathByRecentKey, hasHiddenDownloads: hiddenIds.isNotEmpty, ); @@ -1641,7 +1657,7 @@ class _HomeTabState extends ConsumerState Widget _buildRecentAccess(_RecentAccessView view, ColorScheme colorScheme) { final uniqueItems = view.uniqueItems; - final downloadItems = view.downloadItems; + final downloadIds = view.downloadIds; final hasHiddenDownloads = view.hasHiddenDownloads; return Padding( @@ -1661,10 +1677,10 @@ class _HomeTabState extends ConsumerState if (uniqueItems.isNotEmpty) TextButton( onPressed: () { - for (final item in downloadItems) { + for (final id in downloadIds) { ref .read(recentAccessProvider.notifier) - .hideDownloadFromRecents(item.id); + .hideDownloadFromRecents(id); } ref.read(recentAccessProvider.notifier).clearHistory(); }, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 25f4e9a9..665873b2 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -228,6 +228,7 @@ Map> _filterHistoryInIsolate(Map payload) { final entries = (payload['entries'] as List).cast(); final albumCounts = (payload['albumCounts'] as Map).cast(); final query = (payload['query'] as String?) ?? ''; + final hasQuery = query.isNotEmpty; final allIds = []; final albumIds = []; @@ -236,10 +237,11 @@ Map> _filterHistoryInIsolate(Map payload) { for (final entry in entries) { final id = entry[0] as String; final albumKey = entry[1] as String; - final searchKey = entry[2] as String; - - if (query.isNotEmpty && !searchKey.contains(query)) { - continue; + if (hasQuery) { + final searchKey = entry[2] as String; + if (!searchKey.contains(query)) { + continue; + } } allIds.add(id); @@ -276,6 +278,8 @@ class _QueueTabState extends ConsumerState { final ValueNotifier _alwaysMissingFileNotifier = ValueNotifier(false); final Set _pendingChecks = {}; static const int _maxCacheSize = 500; + static const int _maxSearchIndexCacheSize = 4000; + static const int _maxDownloadedEmbeddedCoverCacheSize = 180; bool _isSelectionMode = false; final Set _selectedIds = {}; @@ -311,8 +315,6 @@ class _QueueTabState extends ConsumerState { final Set _pendingDownloadedCoverExtract = {}; final Set _pendingDownloadedCoverRefresh = {}; final Set _failedDownloadedCoverExtract = {}; - Map _historyItemsById = {}; - List> _historyFilterEntries = const []; Map> _filteredHistoryCache = const {}; List? _filterItemsCache; String _filterQueryCache = ''; @@ -407,32 +409,16 @@ class _QueueTabState extends ConsumerState { _historyItemsCache = items; _localLibraryItemsCache = localItems; _historyStatsCache = _buildHistoryStats(items, localItems); - _searchIndexCache - ..clear() - ..addEntries( - items.map((item) => MapEntry(item.id, _buildSearchKey(item))), - ); + if (historyChanged) { + _searchIndexCache.clear(); + } if (localChanged) { - _localSearchIndexCache - ..clear() - ..addEntries( - localItems.map( - (item) => MapEntry(item.id, _buildLocalSearchKey(item)), - ), - ); + _localSearchIndexCache.clear(); _localFilterItemsCache = null; _localFilterQueryCache = ''; _filteredLocalItemsCache = const []; } _unifiedItemsCache.clear(); - _historyItemsById = {for (final item in items) item.id: item}; - _historyFilterEntries = List>.generate(items.length, (index) { - final item = items[index]; - final searchKey = _searchIndexCache[item.id] ?? _buildSearchKey(item); - final albumKey = - '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; - return [item.id, albumKey, searchKey]; - }, growable: false); if (historyChanged) { final validPaths = items @@ -459,6 +445,30 @@ class _QueueTabState extends ConsumerState { .toLowerCase(); } + String _historySearchKeyForItem(DownloadHistoryItem item) { + final cached = _searchIndexCache[item.id]; + if (cached != null) return cached; + + final searchKey = _buildSearchKey(item); + _searchIndexCache[item.id] = searchKey; + while (_searchIndexCache.length > _maxSearchIndexCacheSize) { + _searchIndexCache.remove(_searchIndexCache.keys.first); + } + return searchKey; + } + + String _localSearchKeyForItem(LocalLibraryItem item) { + final cached = _localSearchIndexCache[item.id]; + if (cached != null) return cached; + + final searchKey = _buildLocalSearchKey(item); + _localSearchIndexCache[item.id] = searchKey; + while (_localSearchIndexCache.length > _maxSearchIndexCacheSize) { + _localSearchIndexCache.remove(_localSearchIndexCache.keys.first); + } + return searchKey; + } + List _filterLocalItems( List items, String query, @@ -471,11 +481,7 @@ class _QueueTabState extends ConsumerState { final filtered = items .where((item) { - final searchKey = - _localSearchIndexCache[item.id] ?? _buildLocalSearchKey(item); - if (!_localSearchIndexCache.containsKey(item.id)) { - _localSearchIndexCache[item.id] = searchKey; - } + final searchKey = _localSearchKeyForItem(item); return searchKey.contains(query); }) .toList(growable: false); @@ -548,15 +554,26 @@ class _QueueTabState extends ConsumerState { } final requestId = ++_filterRequestId; + final includeSearchKey = query.isNotEmpty; + final entries = List>.generate(items.length, (index) { + final item = items[index]; + final albumKey = + '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; + if (!includeSearchKey) { + return [item.id, albumKey]; + } + final searchKey = _historySearchKeyForItem(item); + return [item.id, albumKey, searchKey]; + }, growable: false); final payload = { - 'entries': _historyFilterEntries, + 'entries': entries, 'albumCounts': albumCounts, 'query': query, }; compute(_filterHistoryInIsolate, payload).then((result) { if (!mounted || requestId != _filterRequestId) return; - final itemsById = _historyItemsById; + final itemsById = {for (final item in items) item.id: item}; final filtered = >{}; for (final entry in result.entries) { filtered[entry.key] = entry.value @@ -604,10 +621,7 @@ class _QueueTabState extends ConsumerState { final query = searchQuery; return items .where((item) { - final searchKey = _searchIndexCache[item.id] ?? _buildSearchKey(item); - if (!_searchIndexCache.containsKey(item.id)) { - _searchIndexCache[item.id] = searchKey; - } + final searchKey = _historySearchKeyForItem(item); return searchKey.contains(query); }) .toList(growable: false); @@ -812,6 +826,18 @@ class _QueueTabState extends ConsumerState { _cleanupTempCoverPathSync(cachedPath); } + void _trimDownloadedEmbeddedCoverCache() { + while (_downloadedEmbeddedCoverCache.length > + _maxDownloadedEmbeddedCoverCacheSize) { + final oldestKey = _downloadedEmbeddedCoverCache.keys.first; + final removedPath = _downloadedEmbeddedCoverCache.remove(oldestKey); + _pendingDownloadedCoverExtract.remove(oldestKey); + _pendingDownloadedCoverRefresh.remove(oldestKey); + _failedDownloadedCoverExtract.remove(oldestKey); + _cleanupTempCoverPathSync(removedPath); + } + } + Future _readFileModTimeMillis(String? filePath) async { final cleanPath = _cleanFilePath(filePath); if (cleanPath.isEmpty) return null; @@ -918,6 +944,7 @@ class _QueueTabState extends ConsumerState { final previous = _downloadedEmbeddedCoverCache[cleanPath]; _downloadedEmbeddedCoverCache[cleanPath] = outputPath; _failedDownloadedCoverExtract.remove(cleanPath); + _trimDownloadedEmbeddedCoverCache(); if (previous != null && previous != outputPath) { _cleanupTempCoverPathSync(previous); } @@ -1607,10 +1634,7 @@ class _QueueTabState extends ConsumerState { if (searchQuery.isNotEmpty) { final query = searchQuery; filteredItems = items.where((item) { - final searchKey = _searchIndexCache[item.id] ?? _buildSearchKey(item); - if (!_searchIndexCache.containsKey(item.id)) { - _searchIndexCache[item.id] = searchKey; - } + final searchKey = _historySearchKeyForItem(item); return searchKey.contains(query); }).toList(); } @@ -1797,7 +1821,7 @@ class _QueueTabState extends ConsumerState { _initializePageController(); final hasQueueItems = ref.watch( - downloadQueueProvider.select((s) => s.items.isNotEmpty), + downloadQueueLookupProvider.select((lookup) => lookup.itemIds.isNotEmpty), ); final allHistoryItems = ref.watch( downloadHistoryProvider.select((s) => s.items), @@ -1825,6 +1849,14 @@ class _QueueTabState extends ConsumerState { _buildHistoryStats(allHistoryItems, localLibraryItems); final groupedAlbums = historyStats.groupedAlbums; final groupedLocalAlbums = historyStats.groupedLocalAlbums; + final filteredGroupedAlbums = _filterGroupedAlbums( + groupedAlbums, + _searchQuery, + ); + final filteredGroupedLocalAlbums = _filterGroupedLocalAlbums( + groupedLocalAlbums, + _searchQuery, + ); final albumCount = historyStats.totalAlbumCount; final singleCount = historyStats.totalSingleTracks; final filterDataCache = {}; @@ -1835,8 +1867,8 @@ class _QueueTabState extends ConsumerState { () => _computeFilterContentData( filterMode: filterMode, allHistoryItems: allHistoryItems, - groupedAlbums: groupedAlbums, - groupedLocalAlbums: groupedLocalAlbums, + filteredGroupedAlbums: filteredGroupedAlbums, + filteredGroupedLocalAlbums: filteredGroupedLocalAlbums, albumCounts: historyStats.albumCounts, localAlbumCounts: historyStats.localAlbumCounts, localLibraryItems: localLibraryItems, @@ -2227,8 +2259,8 @@ class _QueueTabState extends ConsumerState { _FilterContentData _computeFilterContentData({ required String filterMode, required List allHistoryItems, - required List<_GroupedAlbum> groupedAlbums, - required List<_GroupedLocalAlbum> groupedLocalAlbums, + required List<_GroupedAlbum> filteredGroupedAlbums, + required List<_GroupedLocalAlbum> filteredGroupedLocalAlbums, required Map albumCounts, required Map localAlbumCounts, required List localLibraryItems, @@ -2243,16 +2275,6 @@ class _QueueTabState extends ConsumerState { filterMode: filterMode, ); - final searchQuery = _searchQuery; - final filteredGroupedAlbums = _filterGroupedAlbums( - groupedAlbums, - searchQuery, - ); - final filteredGroupedLocalAlbums = _filterGroupedLocalAlbums( - groupedLocalAlbums, - searchQuery, - ); - final unifiedItems = _getUnifiedItems( filterMode: filterMode, historyItems: historyItems, @@ -2278,7 +2300,7 @@ class _QueueTabState extends ConsumerState { return Consumer( builder: (context, ref, child) { final queueCount = ref.watch( - downloadQueueProvider.select((s) => s.items.length), + downloadQueueLookupProvider.select((lookup) => lookup.itemIds.length), ); if (queueCount == 0) { return const SliverToBoxAdapter(child: SizedBox.shrink()); @@ -2310,10 +2332,8 @@ class _QueueTabState extends ConsumerState { return Consumer( builder: (context, ref, child) { final queueIdsSnapshot = ref.watch( - downloadQueueProvider.select( - (s) => _QueueItemIdsSnapshot( - s.items.map((item) => item.id).toList(growable: false), - ), + downloadQueueLookupProvider.select( + (lookup) => _QueueItemIdsSnapshot(lookup.itemIds), ), ); if (queueIdsSnapshot.ids.isEmpty) { @@ -4011,14 +4031,7 @@ class _QueueItemSliverRow extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final item = ref.watch( - downloadQueueProvider.select((state) { - for (final current in state.items) { - if (current.id == itemId) { - return current; - } - } - return null; - }), + downloadQueueLookupProvider.select((lookup) => lookup.byItemId[itemId]), ); if (item == null) { return const SizedBox.shrink(); diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index be1caa1e..bc006b1a 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -8,6 +8,27 @@ import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; const int _maxLogMessageLength = 500; +const String _redactedValue = '[REDACTED]'; + +final RegExp _authorizationBearerPattern = RegExp( + r'\bAuthorization\b\s*[:=]\s*Bearer\s+[A-Za-z0-9._~+/\-]+=*', + caseSensitive: false, +); + +final RegExp _genericSensitiveKeyValuePattern = RegExp( + r'\b(access[_\s-]?token|refresh[_\s-]?token|id[_\s-]?token|client[_\s-]?secret|authorization|password|api[_\s-]?key)\b(\s*[:=]\s*)([^\s,;]+)', + caseSensitive: false, +); + +final RegExp _sensitiveQueryPattern = RegExp( + r'([?&](?:access_token|refresh_token|id_token|token|client_secret|api_key|apikey|password)=)[^&\s]+', + caseSensitive: false, +); + +final RegExp _bearerTokenPattern = RegExp( + r'\bBearer\s+[A-Za-z0-9._~+/\-]+=*', + caseSensitive: false, +); String _truncateLogText(String value, {int maxLength = _maxLogMessageLength}) { if (value.length <= maxLength) { @@ -16,6 +37,39 @@ String _truncateLogText(String value, {int maxLength = _maxLogMessageLength}) { return '${value.substring(0, maxLength)}...[truncated]'; } +String _redactSensitiveText(String value) { + var redacted = value; + + redacted = redacted.replaceAllMapped(_authorizationBearerPattern, (_) { + return 'Authorization: Bearer $_redactedValue'; + }); + + redacted = redacted.replaceAllMapped(_genericSensitiveKeyValuePattern, ( + match, + ) { + final key = match.group(1) ?? ''; + final delimiter = match.group(2) ?? '='; + return '$key$delimiter$_redactedValue'; + }); + + redacted = redacted.replaceAllMapped(_sensitiveQueryPattern, (match) { + final prefix = match.group(1) ?? ''; + return '$prefix$_redactedValue'; + }); + + redacted = redacted.replaceAllMapped(_bearerTokenPattern, (_) { + return 'Bearer $_redactedValue'; + }); + + return redacted; +} + +String _maskIdentifier(String value) { + if (value.isEmpty) return value; + if (value.length <= 4) return '***'; + return '${value.substring(0, 2)}***${value.substring(value.length - 2)}'; +} + class LogEntry { final DateTime timestamp; final String level; @@ -59,6 +113,7 @@ class LogBuffer extends ChangeNotifier { final Queue _entries = Queue(); Timer? _goLogTimer; int _lastGoLogIndex = 0; + bool _isFetchingGoLogs = false; static bool _loggingEnabled = false; static bool get loggingEnabled => _loggingEnabled; @@ -79,9 +134,11 @@ class LogBuffer extends ChangeNotifier { return; } - final sanitizedMessage = _truncateLogText(entry.message); + final sanitizedMessage = _truncateLogText( + _redactSensitiveText(entry.message), + ); final sanitizedError = entry.error != null - ? _truncateLogText(entry.error!) + ? _truncateLogText(_redactSensitiveText(entry.error!)) : null; final sanitizedEntry = (sanitizedMessage == entry.message && sanitizedError == entry.error) @@ -105,13 +162,20 @@ class LogBuffer extends ChangeNotifier { void startGoLogPolling() { _goLogTimer?.cancel(); _goLogTimer = Timer.periodic(_goLogPollingInterval, (_) async { - await _fetchGoLogs(); + if (_isFetchingGoLogs) return; + _isFetchingGoLogs = true; + try { + await _fetchGoLogs(); + } finally { + _isFetchingGoLogs = false; + } }); } void stopGoLogPolling() { _goLogTimer?.cancel(); _goLogTimer = null; + _isFetchingGoLogs = false; } Future _fetchGoLogs() async { @@ -216,7 +280,7 @@ class LogBuffer extends ChangeNotifier { buffer.writeln( 'Android Version: ${android.version.release} (SDK ${android.version.sdkInt})', ); - buffer.writeln('Device ID: ${android.id}'); + buffer.writeln('Device ID: ${_maskIdentifier(android.id)}'); buffer.writeln('Hardware: ${android.hardware}'); buffer.writeln('Product: ${android.product}'); buffer.writeln('Supported ABIs: ${android.supportedAbis.join(', ')}'); @@ -313,12 +377,14 @@ class BufferedOutput extends LogOutput { void output(OutputEvent event) { if (kDebugMode) { for (final line in event.lines) { - debugPrint(_truncateLogText(line)); + debugPrint(_truncateLogText(_redactSensitiveText(line))); } } final level = _levelToString(event.level); - final message = _truncateLogText(event.lines.join('\n')); + final message = _truncateLogText( + _redactSensitiveText(event.lines.join('\n')), + ); LogBuffer().add( LogEntry( @@ -421,7 +487,7 @@ class AppLogger { _addToBuffer('ERROR', message, error: error.toString()); if (kDebugMode) { debugPrint( - '[$_tag] ERROR: ${_truncateLogText(message)} | ${_truncateLogText(error.toString())}', + '[$_tag] ERROR: ${_truncateLogText(_redactSensitiveText(message))} | ${_truncateLogText(_redactSensitiveText(error.toString()))}', ); if (stackTrace != null) { debugPrint(stackTrace.toString()); From b6a055a01a24c23a86a7352be92af1f49e36c31e Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 02:03:59 +0700 Subject: [PATCH 017/145] docs: add performance, security, and UI sections to v3.6.5 changelog --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ lib/utils/logger.dart | 12 +++++------- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61557dc4..a02c8a8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - Amazon provider re-enabled in download service picker and download settings (alongside Tidal, Qobuz, and YouTube picker flow) - Track Metadata cover UI now refreshes from the embedded file after Edit Metadata/Re-enrich, so the displayed art matches actual file tags - Edit Metadata cover section moved to the top of the form and now previews current embedded cover before replacement (plus selected replacement preview) +- Edit Metadata cover preview enlarged (120px to 160px) with shadow, side-by-side layout for current vs selected cover, and label repositioned below image ### Fixed @@ -82,12 +83,43 @@ - Non-Library screens that read download history (Home/album/history views) now reflect updated title/artist/album/tags without manual rescan - Track Metadata back-navigation now returns an explicit update result after successful edits/re-enrich, enabling History-tab cover refresh fallback when SAF timestamps are unreliable +### Performance + +- Configured Flutter image cache limits (240 entries / 60 MiB) and added `ResizeImage` wrappers for cover art precaching across all screens, reducing peak memory usage on cover-heavy pages +- Added LRU eviction to Deezer cache with configurable max entries per cache type (search/album/artist/ISRC) and periodic expired-entry cleanup to prevent unbounded memory growth in long sessions +- Download progress notifications are now normalized (2-decimal progress, 1-decimal speed, 0.1 MiB byte steps) and deduplicated by track/artist/percent/queue-count, reducing notification overhead during batch downloads +- Each queue item now uses a dedicated `ConsumerWidget` with per-item `.select()` instead of rebuilding the entire list on any item change; items are wrapped in `RepaintBoundary` for paint isolation +- Queue/Library search indexes are now built on-demand per item instead of upfront for all items, with bounded LRU caches (max 4000 entries) +- `copyWith` now preserves derived lookup indexes (ISRC map, track key set) when items list is unchanged, avoiding O(n) rebuild on every scan progress update +- Scan progress polling now compares values before calling `setState`, skipping unnecessary widget rebuilds when nothing changed +- Added in-flight flag to download progress and library scan polling to prevent concurrent timer callbacks from overlapping +- New `DownloadedEmbeddedCoverResolver` service replaces per-screen cover extraction logic with a shared bounded cache (160 entries), mod-time validation, and throttled refresh checks +- Multiple embedded cover change callbacks are now coalesced into a single frame via `addPostFrameCallback`, preventing redundant rebuilds +- Downloaded album screen now caches filtered/sorted track lists and reuses them when the source data reference is unchanged +- Home tab recent downloads now use single-pass aggregation instead of building full per-album lists, and store only IDs instead of full item objects for the clear-all action +- Removed duplicate `_downloadedSpotifyIds` Set and `_isrcSet` (both now use existing map lookups), removed unused `_isTyping` state in home tab +- Track cache pre-warming is now capped at 80 tracks per request to avoid excessive backend calls on large playlists +- About page contributor avatars now use `memCacheWidth`/`memCacheHeight` to decode at display size instead of full resolution + +### Security + +- All logs (Go and Dart) now automatically redact Bearer tokens, access/refresh tokens, client secrets, API keys, and passwords using regex-based sanitization before storage +- Extension auth URLs are now validated for HTTPS-only, no embedded credentials, and no private/local network targets before opening +- Auth URLs in logs are summarized to scheme+host+path only (query params stripped) to prevent token leakage; token exchange error bodies are truncated and sanitized +- Extension HTTP requests now block URLs with embedded credentials (`user:pass@host`) +- Extension storage files changed from `0644` to `0600` (owner-only read/write) +- All SAF relative directory paths are now sanitized per-segment with `.`/`..` filtering; all user-provided file names pass through `sanitizeFilename()` before use +- Extension ID is sanitized before building download destination path +- Device ID in exported log reports is now masked (first 2 + last 2 chars only) + ### Technical - Centralized request serialization in `PlatformBridge` via shared invoke helper and unified payload model - Go strategy router normalizes incoming service casing before dispatch - Extension runtime: `customSearch` now passes query/options via VM globals instead of string interpolation, preventing parser edge cases on certain devices - Extension runtime: JS panic handler now logs full stack trace for easier debugging +- `DownloadQueueLookup` expanded with `byItemId` map and `itemIds` list for O(1) queue item access from UI +- Non-error/non-fatal log entries are now skipped entirely (not just hidden) when detailed logging is disabled, reducing buffer growth and Go log polling overhead ### Removed diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index bc006b1a..3a32375c 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -64,12 +64,6 @@ String _redactSensitiveText(String value) { return redacted; } -String _maskIdentifier(String value) { - if (value.isEmpty) return value; - if (value.length <= 4) return '***'; - return '${value.substring(0, 2)}***${value.substring(value.length - 2)}'; -} - class LogEntry { final DateTime timestamp; final String level; @@ -280,7 +274,11 @@ class LogBuffer extends ChangeNotifier { buffer.writeln( 'Android Version: ${android.version.release} (SDK ${android.version.sdkInt})', ); - buffer.writeln('Device ID: ${_maskIdentifier(android.id)}'); + buffer.writeln('Build ID: ${android.id}'); + if (android.version.securityPatch != null && + android.version.securityPatch!.isNotEmpty) { + buffer.writeln('Security Patch: ${android.version.securityPatch}'); + } buffer.writeln('Hardware: ${android.hardware}'); buffer.writeln('Product: ${android.product}'); buffer.writeln('Supported ABIs: ${android.supportedAbis.join(', ')}'); From 6f20620c9763a945bd696bd33ff63434cf936204 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 02:40:09 +0700 Subject: [PATCH 018/145] perf: parallel I/O, caching, and chunked DB operations (batch 3) - Orphan cleanup: parallel file existence checks (chunk 16) - LocalLibraryState: O(1) findByTrackAndArtist via _byTrackKey map - Local library load: parallel DB + SharedPreferences fetch - Legacy mod-time backfill: chunked parallel File.stat (chunk 24) - Downloaded album screen: cache disc groups, quality, cover path - Local album screen: cache common quality, map-based batch delete - Cache management: parallel async init, chunked directory cleanup - Cover resolver: throttled preview exists check (2.2s interval) - History/Library DB: chunked SQL DELETE (500 per batch) - Batch delete screens: O(1) item lookup via tracksById map --- lib/providers/download_queue_provider.dart | 49 ++++----- lib/providers/local_library_provider.dart | 54 +++++++--- lib/screens/downloaded_album_screen.dart | 99 ++++++++++++++++--- lib/screens/local_album_screen.dart | 19 ++-- .../settings/cache_management_page.dart | 80 ++++++++++----- .../downloaded_embedded_cover_resolver.dart | 18 +++- lib/services/history_database.dart | 20 ++-- lib/services/library_database.dart | 71 ++++++++++--- 8 files changed, 304 insertions(+), 106 deletions(-) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 606bff6b..e63c276d 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -519,31 +519,36 @@ class DownloadHistoryNotifier extends Notifier { final entries = await _db.getAllEntriesWithPaths(); final orphanedIds = []; + final pathById = {}; + const checkChunkSize = 16; - for (final entry in entries) { - final id = entry['id'] as String; - final filePath = entry['file_path'] as String?; + for (var i = 0; i < entries.length; i += checkChunkSize) { + final end = (i + checkChunkSize < entries.length) + ? i + checkChunkSize + : entries.length; + final chunk = entries.sublist(i, end); - if (filePath == null || filePath.isEmpty) continue; + final checks = await Future.wait?>( + chunk.map((entry) async { + final id = entry['id'] as String; + final filePath = entry['file_path'] as String?; + if (filePath == null || filePath.isEmpty) return null; + pathById[id] = filePath; + try { + return MapEntry(id, await fileExists(filePath)); + } catch (e) { + _historyLog.w('Error checking file existence for $id: $e'); + return MapEntry(id, false); + } + }), + ); - bool exists = false; - - if (filePath.startsWith('content://')) { - // SAF path - check via platform bridge - try { - exists = await PlatformBridge.safExists(filePath); - } catch (e) { - _historyLog.w('Error checking SAF file existence: $e'); - exists = false; - } - } else { - // Regular file path - exists = File(filePath).existsSync(); - } - - if (!exists) { - orphanedIds.add(id); - _historyLog.d('Found orphaned entry: $id ($filePath)'); + for (final check in checks) { + if (check == null || check.value) continue; + orphanedIds.add(check.key); + _historyLog.d( + 'Found orphaned entry: ${check.key} (${pathById[check.key] ?? ''})', + ); } } diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index e02e0d32..fcaa0f1c 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -12,6 +12,7 @@ final _log = AppLogger('LocalLibrary'); const _lastScannedAtKey = 'local_library_last_scanned_at'; const _excludedDownloadedCountKey = 'local_library_excluded_downloaded_count'; +final _prefs = SharedPreferences.getInstance(); class LocalLibraryState { final List items; @@ -26,6 +27,7 @@ class LocalLibraryState { final int excludedDownloadedCount; final Set _trackKeySet; final Map _byIsrc; + final Map _byTrackKey; LocalLibraryState({ this.items = const [], @@ -40,6 +42,7 @@ class LocalLibraryState { this.excludedDownloadedCount = 0, Set? trackKeySet, Map? byIsrc, + Map? byTrackKey, }) : _trackKeySet = trackKeySet ?? items.map((item) => item.matchKey).toSet(), _byIsrc = byIsrc ?? @@ -47,7 +50,10 @@ class LocalLibraryState { items .where((item) => item.isrc != null && item.isrc!.isNotEmpty) .map((item) => MapEntry(item.isrc!, item)), - ); + ), + _byTrackKey = + byTrackKey ?? + Map.fromEntries(items.map((item) => MapEntry(item.matchKey, item))); bool hasIsrc(String isrc) => _byIsrc.containsKey(isrc); @@ -60,7 +66,7 @@ class LocalLibraryState { LocalLibraryItem? findByTrackAndArtist(String trackName, String artistName) { final key = '${trackName.toLowerCase()}|${artistName.toLowerCase()}'; - return items.where((item) => item.matchKey == key).firstOrNull; + return _byTrackKey[key]; } bool existsInLibrary({String? isrc, String? trackName, String? artistName}) { @@ -102,6 +108,7 @@ class LocalLibraryState { excludedDownloadedCount ?? this.excludedDownloadedCount, trackKeySet: keepDerivedIndex ? _trackKeySet : null, byIsrc: keepDerivedIndex ? _byIsrc : null, + byTrackKey: keepDerivedIndex ? _byTrackKey : null, ); } } @@ -133,13 +140,17 @@ class LocalLibraryNotifier extends Notifier { _isLoaded = true; try { - final jsonList = await _db.getAll(); - final items = jsonList.map((e) => LocalLibraryItem.fromJson(e)).toList(); + final dbItemsFuture = _db.getAll(); + final prefsFuture = _prefs; + final jsonList = await dbItemsFuture; + final items = jsonList + .map((e) => LocalLibraryItem.fromJson(e)) + .toList(growable: false); DateTime? lastScannedAt; var excludedDownloadedCount = 0; try { - final prefs = await SharedPreferences.getInstance(); + final prefs = await prefsFuture; final lastScannedAtStr = prefs.getString(_lastScannedAtKey); if (lastScannedAtStr != null && lastScannedAtStr.isNotEmpty) { lastScannedAt = DateTime.tryParse(lastScannedAtStr); @@ -589,17 +600,34 @@ class LocalLibraryNotifier extends Notifier { } } + final paths = legacyPaths + .where((path) => !path.startsWith('content://')) + .toList(growable: false); + const chunkSize = 24; final backfilled = {}; - for (final path in legacyPaths) { - if (_scanCancelRequested || path.startsWith('content://')) { - continue; + + for (var i = 0; i < paths.length; i += chunkSize) { + if (_scanCancelRequested) { + break; } - try { - final stat = await File(path).stat(); - if (stat.type == FileSystemEntityType.file) { - backfilled[path] = stat.modified.millisecondsSinceEpoch; + final end = (i + chunkSize < paths.length) ? i + chunkSize : paths.length; + final chunk = paths.sublist(i, end); + final chunkEntries = await Future.wait?>( + chunk.map((path) async { + try { + final stat = await File(path).stat(); + if (stat.type == FileSystemEntityType.file) { + return MapEntry(path, stat.modified.millisecondsSinceEpoch); + } + } catch (_) {} + return null; + }), + ); + for (final entry in chunkEntries) { + if (entry != null) { + backfilled[entry.key] = entry.value; } - } catch (_) {} + } } return backfilled; } diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index a36d85ca..4f1270f3 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -37,6 +37,14 @@ class _DownloadedAlbumScreenState extends ConsumerState { bool _embeddedCoverRefreshScheduled = false; List? _albumTracksSourceCache; List? _albumTracksCache; + List? _discGroupingSourceCache; + Map>? _discGroupingCache; + List? _sortedDiscNumbersCache; + List? _commonQualitySourceCache; + String? _commonQualityCache; + List? _embeddedCoverSourceCache; + String? _embeddedCoverPathCache; + bool _embeddedCoverPathResolved = false; String get _albumLookupKey => '${widget.albumName.toLowerCase()}|${widget.artistName.toLowerCase()}'; @@ -61,6 +69,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { oldWidget.artistName != widget.artistName) { _albumTracksSourceCache = null; _albumTracksCache = null; + _invalidateDerivedTrackCaches(); } } @@ -104,20 +113,45 @@ class _DownloadedAlbumScreenState extends ConsumerState { _albumTracksSourceCache = allItems; _albumTracksCache = tracks; + _invalidateDerivedTrackCaches(); return tracks; } - Map> _groupTracksByDisc( + void _invalidateDerivedTrackCaches() { + _discGroupingSourceCache = null; + _discGroupingCache = null; + _sortedDiscNumbersCache = null; + _commonQualitySourceCache = null; + _commonQualityCache = null; + _embeddedCoverSourceCache = null; + _embeddedCoverPathCache = null; + _embeddedCoverPathResolved = false; + } + + Map> _getDiscGroups( List tracks, ) { + final cached = _discGroupingCache; + if (cached != null && identical(tracks, _discGroupingSourceCache)) { + return cached; + } + final discMap = >{}; for (final track in tracks) { final discNumber = track.discNumber ?? 1; discMap.putIfAbsent(discNumber, () => []).add(track); } + _discGroupingSourceCache = tracks; + _discGroupingCache = discMap; + _sortedDiscNumbersCache = discMap.keys.toList()..sort(); return discMap; } + List _getSortedDiscNumbers(List tracks) { + _getDiscGroups(tracks); + return _sortedDiscNumbersCache ?? const []; + } + void _enterSelectionMode(String itemId) { HapticFeedback.mediumImpact(); setState(() { @@ -178,10 +212,11 @@ class _DownloadedAlbumScreenState extends ConsumerState { if (confirmed == true && mounted) { final historyNotifier = ref.read(downloadHistoryProvider.notifier); final idsToDelete = _selectedIds.toList(); + final tracksById = {for (final track in currentTracks) track.id: track}; int deletedCount = 0; for (final id in idsToDelete) { - final item = currentTracks.where((e) => e.id == id).firstOrNull; + final item = tracksById[id]; if (item != null) { try { await deleteFile(item.filePath); @@ -220,6 +255,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { void _onEmbeddedCoverChanged() { if (!mounted || _embeddedCoverRefreshScheduled) return; _embeddedCoverRefreshScheduled = true; + _embeddedCoverPathResolved = false; WidgetsBinding.instance.addPostFrameCallback((_) { _embeddedCoverRefreshScheduled = false; if (mounted) { @@ -346,11 +382,24 @@ class _DownloadedAlbumScreenState extends ConsumerState { } String? _resolveAlbumEmbeddedCoverPath(List tracks) { - if (tracks.isEmpty) return null; - return DownloadedEmbeddedCoverResolver.resolve( + if (_embeddedCoverPathResolved && + identical(tracks, _embeddedCoverSourceCache)) { + return _embeddedCoverPathCache; + } + + _embeddedCoverSourceCache = tracks; + _embeddedCoverPathResolved = true; + + if (tracks.isEmpty) { + _embeddedCoverPathCache = null; + return null; + } + + _embeddedCoverPathCache = DownloadedEmbeddedCoverResolver.resolve( tracks.first.filePath, onChanged: _onEmbeddedCoverChanged, ); + return _embeddedCoverPathCache; } Widget _buildAppBar( @@ -541,6 +590,8 @@ class _DownloadedAlbumScreenState extends ConsumerState { ColorScheme colorScheme, List tracks, ) { + final commonQuality = _getCommonQuality(tracks); + return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -604,22 +655,22 @@ class _DownloadedAlbumScreenState extends ConsumerState { ), ), const SizedBox(width: 8), - if (_getCommonQuality(tracks) != null) + if (commonQuality != null) Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( - color: _getCommonQuality(tracks)!.startsWith('24') + color: commonQuality.startsWith('24') ? colorScheme.tertiaryContainer : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(20), ), child: Text( - _getCommonQuality(tracks)!, + commonQuality, style: TextStyle( - color: _getCommonQuality(tracks)!.startsWith('24') + color: commonQuality.startsWith('24') ? colorScheme.onTertiaryContainer : colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, @@ -638,12 +689,30 @@ class _DownloadedAlbumScreenState extends ConsumerState { } String? _getCommonQuality(List tracks) { - if (tracks.isEmpty) return null; - final firstQuality = tracks.first.quality; - if (firstQuality == null) return null; - for (final track in tracks) { - if (track.quality != firstQuality) return null; + if (identical(tracks, _commonQualitySourceCache)) { + return _commonQualityCache; } + + if (tracks.isEmpty) { + _commonQualitySourceCache = tracks; + _commonQualityCache = null; + return null; + } + final firstQuality = tracks.first.quality; + if (firstQuality == null) { + _commonQualitySourceCache = tracks; + _commonQualityCache = null; + return null; + } + for (final track in tracks) { + if (track.quality != firstQuality) { + _commonQualitySourceCache = tracks; + _commonQualityCache = null; + return null; + } + } + _commonQualitySourceCache = tracks; + _commonQualityCache = firstQuality; return firstQuality; } @@ -689,7 +758,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { ColorScheme colorScheme, List tracks, ) { - final discMap = _groupTracksByDisc(tracks); + final discMap = _getDiscGroups(tracks); if (discMap.length <= 1) { return SliverList( @@ -703,7 +772,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { ); } - final discNumbers = discMap.keys.toList()..sort(); + final discNumbers = _getSortedDiscNumbers(tracks); final List children = []; for (final discNumber in discNumbers) { diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index e9cbcdaf..529ad159 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -36,6 +36,7 @@ class _LocalAlbumScreenState extends ConsumerState { late Map> _discGroupsCache; late List _sortedDiscNumbersCache; late bool _hasMultipleDiscsCache; + String? _commonQualityCache; @override void initState() { @@ -87,6 +88,7 @@ class _LocalAlbumScreenState extends ConsumerState { _discGroupsCache = _groupTracksByDisc(_sortedTracksCache); _sortedDiscNumbersCache = _discGroupsCache.keys.toList()..sort(); _hasMultipleDiscsCache = _discGroupsCache.length > 1; + _commonQualityCache = _computeCommonQuality(_sortedTracksCache); } Map> _groupTracksByDisc( @@ -160,15 +162,16 @@ class _LocalAlbumScreenState extends ConsumerState { if (confirmed == true && mounted) { final libraryNotifier = ref.read(localLibraryProvider.notifier); final idsToDelete = _selectedIds.toList(); + final tracksById = {for (final track in currentTracks) track.id: track}; int deletedCount = 0; for (final id in idsToDelete) { - final item = currentTracks.where((e) => e.id == id).firstOrNull; + final item = tracksById[id]; if (item != null) { try { await deleteFile(item.filePath); } catch (_) {} - libraryNotifier.removeItem(id); + await libraryNotifier.removeItem(id); deletedCount++; } } @@ -425,6 +428,8 @@ class _LocalAlbumScreenState extends ConsumerState { ColorScheme colorScheme, List tracks, ) { + final commonQuality = _commonQualityCache; + return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -519,22 +524,22 @@ class _LocalAlbumScreenState extends ConsumerState { ), const SizedBox(width: 8), // Quality badge if all tracks have the same quality - if (_getCommonQuality(tracks) != null) + if (commonQuality != null) Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( - color: _getCommonQuality(tracks)!.contains('24') + color: commonQuality.contains('24') ? colorScheme.primaryContainer : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(20), ), child: Text( - _getCommonQuality(tracks)!, + commonQuality, style: TextStyle( - color: _getCommonQuality(tracks)!.contains('24') + color: commonQuality.contains('24') ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, @@ -552,7 +557,7 @@ class _LocalAlbumScreenState extends ConsumerState { ); } - String? _getCommonQuality(List tracks) { + String? _computeCommonQuality(List tracks) { if (tracks.isEmpty) return null; final first = tracks.first; if (first.bitDepth == null || first.sampleRate == null) return null; diff --git a/lib/screens/settings/cache_management_page.dart b/lib/screens/settings/cache_management_page.dart index b39bdf9b..c01c685f 100644 --- a/lib/screens/settings/cache_management_page.dart +++ b/lib/screens/settings/cache_management_page.dart @@ -60,19 +60,30 @@ class _CacheManagementPageState extends ConsumerState { } Future<_CacheOverview> _buildOverview() async { - final appCacheDir = await getApplicationCacheDirectory(); - final tempDir = await getTemporaryDirectory(); + final appCacheDirFuture = getApplicationCacheDirectory(); + final tempDirFuture = getTemporaryDirectory(); + final appSupportDirFuture = getApplicationSupportDirectory(); + final coverStatsFuture = CoverCacheManager.getStats(); + final prefsFuture = SharedPreferences.getInstance(); + final trackCacheEntriesFuture = _getTrackCacheSizeSafe(); + + final appCacheDir = await appCacheDirFuture; + final tempDir = await tempDirFuture; final appCachePath = p.normalize(appCacheDir.path); final tempPath = p.normalize(tempDir.path); final tempIsSameAsAppCache = appCachePath == tempPath; - final appCacheStats = await _scanDirectory(Directory(appCachePath)); - final tempStats = tempIsSameAsAppCache - ? null - : await _scanDirectory(Directory(tempPath)); - final coverStats = await CoverCacheManager.getStats(); + final appCacheStatsFuture = _scanDirectory(Directory(appCachePath)); + final tempStatsFuture = tempIsSameAsAppCache + ? Future<_DirectoryStats?>.value(null) + : _scanDirectory(Directory(tempPath)); - final prefs = await SharedPreferences.getInstance(); + final appSupportDir = await appSupportDirFuture; + final libraryCoverStatsFuture = _scanDirectory( + Directory('${appSupportDir.path}/library_covers'), + ); + + final prefs = await prefsFuture; final explorePayload = prefs.getString(_exploreCacheKey); final exploreTs = prefs.getInt(_exploreCacheTsKey); var exploreBytes = 0; @@ -84,16 +95,11 @@ class _CacheManagementPageState extends ConsumerState { } final hasExploreCache = exploreBytes > 0; - int trackCacheEntries; - try { - trackCacheEntries = await PlatformBridge.getTrackCacheSize(); - } catch (_) { - trackCacheEntries = 0; - } - - final appSupportDir = await getApplicationSupportDirectory(); - final libraryCoverDir = Directory('${appSupportDir.path}/library_covers'); - final libraryCoverStats = await _scanDirectory(libraryCoverDir); + final appCacheStats = await appCacheStatsFuture; + final tempStats = await tempStatsFuture; + final coverStats = await coverStatsFuture; + final libraryCoverStats = await libraryCoverStatsFuture; + final trackCacheEntries = await trackCacheEntriesFuture; return _CacheOverview( appCachePath: appCachePath, @@ -132,16 +138,37 @@ class _CacheManagementPageState extends ConsumerState { return _DirectoryStats(fileCount: fileCount, totalSizeBytes: totalSize); } + Future _getTrackCacheSizeSafe() async { + try { + return await PlatformBridge.getTrackCacheSize(); + } catch (_) { + return 0; + } + } + Future _clearDirectoryContents(String path) async { final directory = Directory(path); if (!await directory.exists()) return; try { - final entities = directory.listSync(followLinks: false); - for (final entity in entities) { - try { - await entity.delete(recursive: true); - } catch (_) {} + final entities = []; + await for (final entity in directory.list(followLinks: false)) { + entities.add(entity); + } + + const deleteChunkSize = 24; + for (var i = 0; i < entities.length; i += deleteChunkSize) { + final end = (i + deleteChunkSize < entities.length) + ? i + deleteChunkSize + : entities.length; + final chunk = entities.sublist(i, end); + await Future.wait( + chunk.map((entity) async { + try { + await entity.delete(recursive: true); + } catch (_) {} + }), + ); } } catch (_) {} @@ -583,7 +610,9 @@ class _CacheManagementPageState extends ConsumerState { subtitle: _buildSubtitle( context.l10n.cacheTrackLookupDesc, overview.trackCacheEntries > 0 - ? context.l10n.cacheEntries(overview.trackCacheEntries) + ? context.l10n.cacheEntries( + overview.trackCacheEntries, + ) : context.l10n.cacheNoData, ), trailing: _buildClearTrailing( @@ -611,7 +640,8 @@ class _CacheManagementPageState extends ConsumerState { SettingsItem( icon: Icons.cleaning_services_outlined, title: context.l10n.cacheCleanupUnused, - subtitle: '${context.l10n.cacheCleanupUnusedDesc}\n${context.l10n.cacheCleanupUnusedSubtitle}', + subtitle: + '${context.l10n.cacheCleanupUnusedDesc}\n${context.l10n.cacheCleanupUnusedSubtitle}', trailing: _buildClearTrailing( 'cleanup_unused', _cleanupUnusedData, diff --git a/lib/services/downloaded_embedded_cover_resolver.dart b/lib/services/downloaded_embedded_cover_resolver.dart index 0f40bd02..3f598c2f 100644 --- a/lib/services/downloaded_embedded_cover_resolver.dart +++ b/lib/services/downloaded_embedded_cover_resolver.dart @@ -21,6 +21,7 @@ class _EmbeddedCoverCacheEntry { class DownloadedEmbeddedCoverResolver { static const int _maxCacheEntries = 160; static const int _minModCheckIntervalMs = 1200; + static const int _minPreviewExistsCheckIntervalMs = 2200; static final LinkedHashMap _cache = LinkedHashMap(); @@ -28,6 +29,8 @@ class DownloadedEmbeddedCoverResolver { static final Set _pendingModCheck = {}; static final Set _failedExtract = {}; static final Map _lastModCheckMillis = {}; + static final Map _lastPreviewExistsCheckMillis = + {}; static String cleanFilePath(String? filePath) { if (filePath == null) return ''; @@ -64,12 +67,21 @@ class DownloadedEmbeddedCoverResolver { final cached = _cache[cleanPath]; if (cached != null) { - if (File(cached.previewPath).existsSync()) { + final now = DateTime.now().millisecondsSinceEpoch; + final lastPreviewCheck = _lastPreviewExistsCheckMillis[cleanPath] ?? 0; + final shouldVerifyExists = + now - lastPreviewCheck >= _minPreviewExistsCheckIntervalMs; + + if (!shouldVerifyExists || File(cached.previewPath).existsSync()) { + if (shouldVerifyExists) { + _lastPreviewExistsCheckMillis[cleanPath] = now; + } _touch(cleanPath, cached); _scheduleModCheck(cleanPath, onChanged: onChanged); return cached.previewPath; } _cache.remove(cleanPath); + _lastPreviewExistsCheckMillis.remove(cleanPath); _cleanupTempCoverPathSync(cached.previewPath); } @@ -107,6 +119,7 @@ class DownloadedEmbeddedCoverResolver { _pendingModCheck.remove(cleanPath); _failedExtract.remove(cleanPath); _lastModCheckMillis.remove(cleanPath); + _lastPreviewExistsCheckMillis.remove(cleanPath); if (cached != null) { _cleanupTempCoverPathSync(cached.previewPath); } @@ -129,6 +142,7 @@ class DownloadedEmbeddedCoverResolver { _pendingModCheck.remove(oldestKey); _failedExtract.remove(oldestKey); _lastModCheckMillis.remove(oldestKey); + _lastPreviewExistsCheckMillis.remove(oldestKey); } } @@ -204,6 +218,8 @@ class DownloadedEmbeddedCoverResolver { ); _touch(cleanPath, next); _failedExtract.remove(cleanPath); + _lastPreviewExistsCheckMillis[cleanPath] = + DateTime.now().millisecondsSinceEpoch; _trimCacheIfNeeded(); if (previous != null && previous.previewPath != outputPath) { diff --git a/lib/services/history_database.dart b/lib/services/history_database.dart index 1e9f91d7..b20dd7bc 100644 --- a/lib/services/history_database.dart +++ b/lib/services/history_database.dart @@ -525,12 +525,18 @@ class HistoryDatabase { if (ids.isEmpty) return 0; final db = await database; - final placeholders = List.filled(ids.length, '?').join(','); - final count = await db.rawDelete( - 'DELETE FROM history WHERE id IN ($placeholders)', - ids, - ); - _log.i('Deleted $count orphaned entries'); - return count; + var totalDeleted = 0; + const chunkSize = 500; + for (var i = 0; i < ids.length; i += chunkSize) { + final end = (i + chunkSize < ids.length) ? i + chunkSize : ids.length; + final chunk = ids.sublist(i, end); + final placeholders = List.filled(chunk.length, '?').join(','); + totalDeleted += await db.rawDelete( + 'DELETE FROM history WHERE id IN ($placeholders)', + chunk, + ); + } + _log.i('Deleted $totalDeleted orphaned entries'); + return totalDeleted; } } diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index a45c4769..c15b0c99 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -229,6 +229,7 @@ class LibraryDatabase { } Future upsertBatch(List> items) async { + if (items.isEmpty) return; final db = await database; final batch = db.batch(); @@ -350,16 +351,46 @@ class LibraryDatabase { Future cleanupMissingFiles() async { final db = await database; final rows = await db.query('library', columns: ['id', 'file_path']); - - int removed = 0; - for (final row in rows) { - final filePath = row['file_path'] as String; - if (!await fileExists(filePath)) { - await db.delete('library', where: 'id = ?', whereArgs: [row['id']]); - removed++; + + final missingIds = []; + const checkChunkSize = 16; + for (var i = 0; i < rows.length; i += checkChunkSize) { + final end = (i + checkChunkSize < rows.length) + ? i + checkChunkSize + : rows.length; + final chunk = rows.sublist(i, end); + final checks = await Future.wait>( + chunk.map((row) async { + final id = row['id'] as String; + final filePath = row['file_path'] as String; + return MapEntry(id, await fileExists(filePath)); + }), + ); + for (final check in checks) { + if (!check.value) { + missingIds.add(check.key); + } } } - + + if (missingIds.isEmpty) { + return 0; + } + + var removed = 0; + const deleteChunkSize = 500; + for (var i = 0; i < missingIds.length; i += deleteChunkSize) { + final end = (i + deleteChunkSize < missingIds.length) + ? i + deleteChunkSize + : missingIds.length; + final idChunk = missingIds.sublist(i, end); + final placeholders = List.filled(idChunk.length, '?').join(','); + removed += await db.rawDelete( + 'DELETE FROM library WHERE id IN ($placeholders)', + idChunk, + ); + } + if (removed > 0) { _log.i('Cleaned up $removed missing files from library'); } @@ -440,14 +471,22 @@ class LibraryDatabase { Future deleteByPaths(List filePaths) async { if (filePaths.isEmpty) return 0; final db = await database; - final placeholders = List.filled(filePaths.length, '?').join(','); - final result = await db.rawDelete( - 'DELETE FROM library WHERE file_path IN ($placeholders)', - filePaths, - ); - if (result > 0) { - _log.i('Deleted $result items from library'); + var totalDeleted = 0; + const chunkSize = 500; + for (var i = 0; i < filePaths.length; i += chunkSize) { + final end = (i + chunkSize < filePaths.length) + ? i + chunkSize + : filePaths.length; + final chunk = filePaths.sublist(i, end); + final placeholders = List.filled(chunk.length, '?').join(','); + totalDeleted += await db.rawDelete( + 'DELETE FROM library WHERE file_path IN ($placeholders)', + chunk, + ); } - return result; + if (totalDeleted > 0) { + _log.i('Deleted $totalDeleted items from library'); + } + return totalDeleted; } } From a3b49d2642c00aef303f75fbe06f8ff8bddc0486 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 02:41:32 +0700 Subject: [PATCH 019/145] docs: add batch 3 performance entries to changelog, fix device ID entry --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a02c8a8d..a358ba4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,17 @@ - Removed duplicate `_downloadedSpotifyIds` Set and `_isrcSet` (both now use existing map lookups), removed unused `_isTyping` state in home tab - Track cache pre-warming is now capped at 80 tracks per request to avoid excessive backend calls on large playlists - About page contributor avatars now use `memCacheWidth`/`memCacheHeight` to decode at display size instead of full resolution +- Orphaned download cleanup now checks file existence in parallel (chunk 16) instead of sequentially +- Local library `findByTrackAndArtist` now uses O(1) map lookup (`_byTrackKey`) instead of O(n) linear scan +- Local library database load and SharedPreferences fetch now run in parallel +- Legacy mod-time backfill now uses chunked parallel `File.stat` (chunk 24) with per-chunk cancel check +- Downloaded album screen now caches disc grouping, sorted disc numbers, common quality, and embedded cover path with reference-identity invalidation +- Local album screen common quality is now computed once during cache rebuild instead of per-build +- Batch delete in album screens now uses O(1) map lookup (`tracksById`) instead of `.where().firstOrNull` +- Cache management page now fires all async init calls in parallel and uses chunked async directory deletion (chunk 24) +- Cover resolver preview file existence check is now throttled (2.2s interval) to reduce synchronous I/O in build path +- History and library database DELETE operations are now chunked (500 per batch) to stay within SQLite variable limits +- Library database `cleanupMissingFiles` now checks file existence in parallel (chunk 16) and deletes in batched SQL ### Security @@ -110,7 +121,7 @@ - Extension storage files changed from `0644` to `0600` (owner-only read/write) - All SAF relative directory paths are now sanitized per-segment with `.`/`..` filtering; all user-provided file names pass through `sanitizeFilename()` before use - Extension ID is sanitized before building download destination path -- Device ID in exported log reports is now masked (first 2 + last 2 chars only) +- Log export device info now shows Build ID and Security Patch level instead of masked Device ID ### Technical From b317f7cd763bb67e7a23f0e41a92695109018b1f Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 08:01:11 +0700 Subject: [PATCH 020/145] fix: show library filter buttons while downloads are active Previously filter/sort headers in All, Albums, and Singles tabs were hidden when queue items existed, preventing users from filtering their library (e.g. find MP3 tracks to re-download as FLAC) during active downloads. --- lib/screens/queue_tab.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 665873b2..2eba8858 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -2374,7 +2374,7 @@ class _QueueTabState extends ConsumerState { return CustomScrollView( slivers: [ - if (totalTrackCount > 0 && !hasQueueItems && filterMode == 'all') + if (totalTrackCount > 0 && filterMode == 'all') SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), @@ -2424,7 +2424,6 @@ class _QueueTabState extends ConsumerState { if ((filteredGroupedAlbums.isNotEmpty || filteredGroupedLocalAlbums.isNotEmpty) && - !hasQueueItems && filterMode == 'albums') SliverToBoxAdapter( child: Padding( @@ -2461,7 +2460,6 @@ class _QueueTabState extends ConsumerState { // Albums empty state with filter button if (filteredGroupedAlbums.isEmpty && filteredGroupedLocalAlbums.isEmpty && - !hasQueueItems && filterMode == 'albums' && (historyItems.isNotEmpty || localLibraryItems.isNotEmpty)) SliverToBoxAdapter( @@ -2612,7 +2610,7 @@ class _QueueTabState extends ConsumerState { ), // Singles filter - show unified items (downloaded + local singles) - if (filterMode == 'singles' && !hasQueueItems) + if (filterMode == 'singles') SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), From 564dd8bf95102f583c599308f056a8e6bfc25ed6 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 12:28:50 +0700 Subject: [PATCH 021/145] refactor: migrate queue_tab cover resolver to shared service, add supporter --- lib/screens/queue_tab.dart | 190 +++--------------- lib/screens/settings/donate_page.dart | 3 +- .../downloaded_embedded_cover_resolver.dart | 87 +++----- 3 files changed, 54 insertions(+), 226 deletions(-) diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 2eba8858..6b03573f 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -14,7 +14,7 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/services/library_database.dart'; -import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/screens/downloaded_album_screen.dart'; import 'package:spotiflac_android/screens/local_album_screen.dart'; @@ -279,7 +279,7 @@ class _QueueTabState extends ConsumerState { final Set _pendingChecks = {}; static const int _maxCacheSize = 500; static const int _maxSearchIndexCacheSize = 4000; - static const int _maxDownloadedEmbeddedCoverCacheSize = 180; + bool _embeddedCoverRefreshScheduled = false; bool _isSelectionMode = false; final Set _selectedIds = {}; @@ -311,10 +311,6 @@ class _QueueTabState extends ConsumerState { _HistoryStats? _historyStatsCache; final Map _searchIndexCache = {}; final Map _localSearchIndexCache = {}; - final Map _downloadedEmbeddedCoverCache = {}; - final Set _pendingDownloadedCoverExtract = {}; - final Set _pendingDownloadedCoverRefresh = {}; - final Set _failedDownloadedCoverExtract = {}; Map> _filteredHistoryCache = const {}; List? _filterItemsCache; String _filterQueryCache = ''; @@ -361,13 +357,6 @@ class _QueueTabState extends ConsumerState { @override void dispose() { - for (final coverPath in _downloadedEmbeddedCoverCache.values) { - _cleanupTempCoverPathSync(coverPath); - } - _downloadedEmbeddedCoverCache.clear(); - _pendingDownloadedCoverExtract.clear(); - _pendingDownloadedCoverRefresh.clear(); - _failedDownloadedCoverExtract.clear(); for (final notifier in _fileExistsNotifiers.values) { notifier.dispose(); } @@ -425,12 +414,7 @@ class _QueueTabState extends ConsumerState { .map((item) => _cleanFilePath(item.filePath)) .where((path) => path.isNotEmpty) .toSet(); - final staleKeys = _downloadedEmbeddedCoverCache.keys - .where((path) => !validPaths.contains(path)) - .toList(growable: false); - for (final key in staleKeys) { - _invalidateDownloadedEmbeddedCover(key); - } + DownloadedEmbeddedCoverResolver.invalidatePathsNotIn(validPaths); } _requestFilterRefresh(); } @@ -794,69 +778,22 @@ class _QueueTabState extends ConsumerState { /// Strip EXISTS: prefix from file path (legacy history items) String _cleanFilePath(String? filePath) { - if (filePath == null) return ''; - if (filePath.startsWith('EXISTS:')) { - return filePath.substring(7); - } - return filePath; - } - - void _cleanupTempCoverPathSync(String? coverPath) { - if (coverPath == null || coverPath.isEmpty) return; - try { - final file = File(coverPath); - if (file.existsSync()) { - file.deleteSync(); - } - final parent = file.parent; - if (parent.existsSync()) { - parent.deleteSync(recursive: true); - } - } catch (_) {} - } - - void _invalidateDownloadedEmbeddedCover(String? filePath) { - final cleanPath = _cleanFilePath(filePath); - if (cleanPath.isEmpty) return; - - final cachedPath = _downloadedEmbeddedCoverCache.remove(cleanPath); - _pendingDownloadedCoverExtract.remove(cleanPath); - _pendingDownloadedCoverRefresh.remove(cleanPath); - _failedDownloadedCoverExtract.remove(cleanPath); - _cleanupTempCoverPathSync(cachedPath); - } - - void _trimDownloadedEmbeddedCoverCache() { - while (_downloadedEmbeddedCoverCache.length > - _maxDownloadedEmbeddedCoverCacheSize) { - final oldestKey = _downloadedEmbeddedCoverCache.keys.first; - final removedPath = _downloadedEmbeddedCoverCache.remove(oldestKey); - _pendingDownloadedCoverExtract.remove(oldestKey); - _pendingDownloadedCoverRefresh.remove(oldestKey); - _failedDownloadedCoverExtract.remove(oldestKey); - _cleanupTempCoverPathSync(removedPath); - } + return DownloadedEmbeddedCoverResolver.cleanFilePath(filePath); } Future _readFileModTimeMillis(String? filePath) async { - final cleanPath = _cleanFilePath(filePath); - if (cleanPath.isEmpty) return null; + return DownloadedEmbeddedCoverResolver.readFileModTimeMillis(filePath); + } - if (cleanPath.startsWith('content://')) { - try { - final modTimes = await PlatformBridge.getSafFileModTimes([cleanPath]); - return modTimes[cleanPath]; - } catch (_) { - return null; + void _onEmbeddedCoverChanged() { + if (!mounted || _embeddedCoverRefreshScheduled) return; + _embeddedCoverRefreshScheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _embeddedCoverRefreshScheduled = false; + if (mounted) { + setState(() {}); } - } - - try { - final stat = await File(cleanPath).stat(); - return stat.modified.millisecondsSinceEpoch; - } catch (_) { - return null; - } + }); } Future _scheduleDownloadedEmbeddedCoverRefreshForPath( @@ -864,98 +801,19 @@ class _QueueTabState extends ConsumerState { int? beforeModTime, bool force = false, }) async { - final cleanPath = _cleanFilePath(filePath); - if (cleanPath.isEmpty) return; - - if (!force) { - if (beforeModTime == null) { - return; - } - final afterModTime = await _readFileModTimeMillis(cleanPath); - if (afterModTime != null && afterModTime == beforeModTime) { - return; - } - } - - _pendingDownloadedCoverRefresh.add(cleanPath); - _failedDownloadedCoverExtract.remove(cleanPath); - if (mounted) { - setState(() {}); - } + await DownloadedEmbeddedCoverResolver.scheduleRefreshForPath( + filePath, + beforeModTime: beforeModTime, + force: force, + onChanged: _onEmbeddedCoverChanged, + ); } String? _resolveDownloadedEmbeddedCoverPath(String? filePath) { - final cleanPath = _cleanFilePath(filePath); - if (cleanPath.isEmpty) return null; - - if (_pendingDownloadedCoverRefresh.remove(cleanPath)) { - _ensureDownloadedEmbeddedCover(cleanPath, forceRefresh: true); - } - - final cachedPath = _downloadedEmbeddedCoverCache[cleanPath]; - if (cachedPath != null) { - if (File(cachedPath).existsSync()) { - return cachedPath; - } - _downloadedEmbeddedCoverCache.remove(cleanPath); - _cleanupTempCoverPathSync(cachedPath); - } - - return null; - } - - void _ensureDownloadedEmbeddedCover( - String cleanPath, { - bool forceRefresh = false, - }) { - if (cleanPath.isEmpty) return; - if (_pendingDownloadedCoverExtract.contains(cleanPath)) return; - if (!forceRefresh && _downloadedEmbeddedCoverCache.containsKey(cleanPath)) { - return; - } - if (!forceRefresh && _failedDownloadedCoverExtract.contains(cleanPath)) { - return; - } - - _pendingDownloadedCoverExtract.add(cleanPath); - Future.microtask(() async { - String? outputPath; - try { - final tempDir = await Directory.systemTemp.createTemp('library_cover_'); - outputPath = '${tempDir.path}${Platform.pathSeparator}cover.jpg'; - final result = await PlatformBridge.extractCoverToFile( - cleanPath, - outputPath, - ); - - final hasCover = - result['error'] == null && await File(outputPath).exists(); - if (!hasCover) { - _failedDownloadedCoverExtract.add(cleanPath); - _cleanupTempCoverPathSync(outputPath); - return; - } - - if (!mounted) { - _cleanupTempCoverPathSync(outputPath); - return; - } - - final previous = _downloadedEmbeddedCoverCache[cleanPath]; - _downloadedEmbeddedCoverCache[cleanPath] = outputPath; - _failedDownloadedCoverExtract.remove(cleanPath); - _trimDownloadedEmbeddedCoverCache(); - if (previous != null && previous != outputPath) { - _cleanupTempCoverPathSync(previous); - } - setState(() {}); - } catch (_) { - _failedDownloadedCoverExtract.add(cleanPath); - _cleanupTempCoverPathSync(outputPath); - } finally { - _pendingDownloadedCoverExtract.remove(cleanPath); - } - }); + return DownloadedEmbeddedCoverResolver.resolve( + filePath, + onChanged: _onEmbeddedCoverChanged, + ); } ValueListenable _fileExistsListenable(String? filePath) { diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index 1ec3e784..56a7aefe 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -204,8 +204,9 @@ class _RecentDonorsCard extends StatelessWidget { _DonorTile(name: 'Julian', colorScheme: colorScheme), _DonorTile(name: 'matt_3050', colorScheme: colorScheme), _DonorTile(name: 'Daniel', colorScheme: colorScheme), + _DonorTile(name: '283Fabio', colorScheme: colorScheme), _DonorTile( - name: '283Fabio', + name: 'Elias el Autentico', colorScheme: colorScheme, showDivider: false, ), diff --git a/lib/services/downloaded_embedded_cover_resolver.dart b/lib/services/downloaded_embedded_cover_resolver.dart index 3f598c2f..9b613510 100644 --- a/lib/services/downloaded_embedded_cover_resolver.dart +++ b/lib/services/downloaded_embedded_cover_resolver.dart @@ -19,18 +19,13 @@ class _EmbeddedCoverCacheEntry { /// It keeps a bounded in-memory cache and only refreshes extraction /// when the source file changed. class DownloadedEmbeddedCoverResolver { - static const int _maxCacheEntries = 160; - static const int _minModCheckIntervalMs = 1200; - static const int _minPreviewExistsCheckIntervalMs = 2200; + static const int _maxCacheEntries = 180; static final LinkedHashMap _cache = LinkedHashMap(); static final Set _pendingExtract = {}; - static final Set _pendingModCheck = {}; + static final Set _pendingRefresh = {}; static final Set _failedExtract = {}; - static final Map _lastModCheckMillis = {}; - static final Map _lastPreviewExistsCheckMillis = - {}; static String cleanFilePath(String? filePath) { if (filePath == null) return ''; @@ -65,27 +60,20 @@ class DownloadedEmbeddedCoverResolver { final cleanPath = cleanFilePath(filePath); if (cleanPath.isEmpty) return null; + if (_pendingRefresh.remove(cleanPath)) { + _ensureCover(cleanPath, forceRefresh: true, onChanged: onChanged); + } + final cached = _cache[cleanPath]; if (cached != null) { - final now = DateTime.now().millisecondsSinceEpoch; - final lastPreviewCheck = _lastPreviewExistsCheckMillis[cleanPath] ?? 0; - final shouldVerifyExists = - now - lastPreviewCheck >= _minPreviewExistsCheckIntervalMs; - - if (!shouldVerifyExists || File(cached.previewPath).existsSync()) { - if (shouldVerifyExists) { - _lastPreviewExistsCheckMillis[cleanPath] = now; - } + if (File(cached.previewPath).existsSync()) { _touch(cleanPath, cached); - _scheduleModCheck(cleanPath, onChanged: onChanged); return cached.previewPath; } _cache.remove(cleanPath); - _lastPreviewExistsCheckMillis.remove(cleanPath); _cleanupTempCoverPathSync(cached.previewPath); } - _ensureCover(cleanPath, onChanged: onChanged); return null; } @@ -106,8 +94,9 @@ class DownloadedEmbeddedCoverResolver { } } + _pendingRefresh.add(cleanPath); _failedExtract.remove(cleanPath); - _ensureCover(cleanPath, forceRefresh: true, onChanged: onChanged); + onChanged?.call(); } static void invalidate(String? filePath) { @@ -116,15 +105,30 @@ class DownloadedEmbeddedCoverResolver { final cached = _cache.remove(cleanPath); _pendingExtract.remove(cleanPath); - _pendingModCheck.remove(cleanPath); + _pendingRefresh.remove(cleanPath); _failedExtract.remove(cleanPath); - _lastModCheckMillis.remove(cleanPath); - _lastPreviewExistsCheckMillis.remove(cleanPath); if (cached != null) { _cleanupTempCoverPathSync(cached.previewPath); } } + static void invalidatePathsNotIn(Set validCleanPaths) { + if (validCleanPaths.isEmpty) { + final keys = _cache.keys.toList(growable: false); + for (final key in keys) { + invalidate(key); + } + return; + } + + final staleKeys = _cache.keys + .where((path) => !validCleanPaths.contains(path)) + .toList(growable: false); + for (final key in staleKeys) { + invalidate(key); + } + } + static void _touch(String cleanPath, _EmbeddedCoverCacheEntry entry) { _cache ..remove(cleanPath) @@ -139,44 +143,11 @@ class DownloadedEmbeddedCoverResolver { _cleanupTempCoverPathSync(removed.previewPath); } _pendingExtract.remove(oldestKey); - _pendingModCheck.remove(oldestKey); + _pendingRefresh.remove(oldestKey); _failedExtract.remove(oldestKey); - _lastModCheckMillis.remove(oldestKey); - _lastPreviewExistsCheckMillis.remove(oldestKey); } } - static void _scheduleModCheck(String cleanPath, {VoidCallback? onChanged}) { - if (_pendingModCheck.contains(cleanPath)) return; - - final now = DateTime.now().millisecondsSinceEpoch; - final lastCheck = _lastModCheckMillis[cleanPath] ?? 0; - if (now - lastCheck < _minModCheckIntervalMs) return; - _lastModCheckMillis[cleanPath] = now; - - _pendingModCheck.add(cleanPath); - Future.microtask(() async { - try { - final cached = _cache[cleanPath]; - if (cached == null) return; - - final currentModTime = await readFileModTimeMillis(cleanPath); - if (currentModTime != null && - cached.sourceModTimeMillis != null && - currentModTime != cached.sourceModTimeMillis) { - _ensureCover( - cleanPath, - forceRefresh: true, - knownModTime: currentModTime, - onChanged: onChanged, - ); - } - } finally { - _pendingModCheck.remove(cleanPath); - } - }); - } - static void _ensureCover( String cleanPath, { bool forceRefresh = false, @@ -218,8 +189,6 @@ class DownloadedEmbeddedCoverResolver { ); _touch(cleanPath, next); _failedExtract.remove(cleanPath); - _lastPreviewExistsCheckMillis[cleanPath] = - DateTime.now().millisecondsSinceEpoch; _trimCacheIfNeeded(); if (previous != null && previous.previewPath != outputPath) { From 2092f078ecd79075c17c02c91db77728c337f6dd Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:06:33 +0700 Subject: [PATCH 022/145] Update badge URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b7b9da7..22fc0c81 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![GitHub All Releases](https://img.shields.io/github/downloads/zarzet/SpotiFLAC-Mobile/total?style=for-the-badge&refresh=1)](https://github.com/zarzet/SpotiFLAC-Mobile/releases) +[![GitHub All Releases](https://img.shields.io/github/downloads/zarzet/SpotiFLAC-Mobile/total?style=for-the-badge)](https://github.com/zarzet/SpotiFLAC-Mobile/releases) [![VirusTotal](https://img.shields.io/badge/VirusTotal-Safe-brightgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/40f8f1914287dea317122a837f98b0ddf7af3205adc2f84a350d767e0a6a345c) [![Crowdin](https://img.shields.io/badge/HELP%20TRANSLATE%20ON-CROWDIN-%2321252b?style=for-the-badge&logo=crowdin)](https://crowdin.com/project/spotiflac-mobile) From a4899144c51bf8818ff59a82961d397545ed87d1 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 8 Mar 2026 23:12:48 +0700 Subject: [PATCH 023/145] fix: show error feedback for unrecognized URLs and fix l10n warnings --- lib/l10n/app_localizations.dart | 18 + lib/l10n/app_localizations_de.dart | 714 +++---- lib/l10n/app_localizations_en.dart | 11 + lib/l10n/app_localizations_es.dart | 11 + lib/l10n/app_localizations_fr.dart | 15 +- lib/l10n/app_localizations_hi.dart | 15 +- lib/l10n/app_localizations_id.dart | 166 +- lib/l10n/app_localizations_ja.dart | 174 +- lib/l10n/app_localizations_ko.dart | 471 +++-- lib/l10n/app_localizations_nl.dart | 15 +- lib/l10n/app_localizations_pt.dart | 11 + lib/l10n/app_localizations_ru.dart | 239 ++- lib/l10n/app_localizations_tr.dart | 11 + lib/l10n/app_localizations_zh.dart | 477 ++++- lib/l10n/arb/app_de.arb | 18 +- lib/l10n/arb/app_en.arb | 12 + lib/l10n/arb/app_es-ES.arb | 3105 ---------------------------- lib/l10n/arb/app_id.arb | 12 + lib/l10n/arb/app_pt-PT.arb | 3105 ---------------------------- lib/l10n/arb/app_ru.arb | 22 +- lib/l10n/arb/app_tr-TR.arb | 3105 ---------------------------- lib/l10n/arb/app_zh_CN.arb | 2 +- lib/l10n/arb/app_zh_TW.arb | 2 +- lib/providers/track_provider.dart | 13 + lib/screens/home_tab.dart | 74 +- lib/screens/main_shell.dart | 18 +- 26 files changed, 1630 insertions(+), 10206 deletions(-) delete mode 100644 lib/l10n/arb/app_es-ES.arb delete mode 100644 lib/l10n/arb/app_pt-PT.arb delete mode 100644 lib/l10n/arb/app_tr-TR.arb diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 822982eb..c059621b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1306,6 +1306,24 @@ abstract class AppLocalizations { /// **'No tracks found'** String get errorNoTracksFound; + /// Error title - URL not handled by any extension or service + /// + /// In en, this message translates to: + /// **'Link not recognized'** + String get errorUrlNotRecognized; + + /// Error message - URL not recognized explanation + /// + /// In en, this message translates to: + /// **'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'** + String get errorUrlNotRecognizedMessage; + + /// Error message - generic URL fetch failure + /// + /// In en, this message translates to: + /// **'Failed to load content from this link. Please try again.'** + String get errorUrlFetchFailed; + /// Error - extension source not available /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index af8f7b73..246f5314 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -15,7 +15,7 @@ class AppLocalizationsDe extends AppLocalizations { String get navHome => 'Startseite'; @override - String get navLibrary => 'Archiv'; + String get navLibrary => 'Bibliothek'; @override String get navSettings => 'Einstellungen'; @@ -365,7 +365,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get aboutAppDescription => - 'Lade Spotify-Titel in verlustfreier Qualität von Tidal und Qobuz herunter.'; + 'Lade Spotify-Titel in verlustfreier Qualität von Tidal, Qobuz und Amazon Music herunter.'; @override String get artistAlbums => 'Alben'; @@ -500,7 +500,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get setupNotificationBackgroundDescription => - 'Werde benachrichtigt über Download-Fortschritt und -Fertigstellung. Dies hilft Ihnen, Downloads zu verfolgen, wenn die App im Hintergrund ist.'; + 'Erhalte Benachrichtigungen über den Fortschritt und die Fertigstellung deiner Downloads, selbst wenn die App im Hintergrund läuft.'; @override String get setupSkipForNow => 'Vorerst überspringen'; @@ -592,11 +592,11 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get dialogImportPlaylistTitle => 'Wiedergabeliste importieren'; + String get dialogImportPlaylistTitle => 'Playlist importieren'; @override String dialogImportPlaylistMessage(int count) { - return '$count Titel in CSV gefunden. Zur Warteschlange hinzufügen?'; + return '$count Titel gefunden hinzufügen?'; } @override @@ -606,12 +606,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String snackbarAddedToQueue(String trackName) { - return '\"$trackName\" zur Warteschlange hinzugefügt'; + return '\"$trackName\" hinzugefügt'; } @override String snackbarAddedTracksToQueue(int count) { - return '$count Titel zur Warteschlange hinzugefügt'; + return '$count Titel hinzugefügt'; } @override @@ -701,6 +701,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String get errorNoTracksFound => 'Keine Titel gefunden'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return 'Kann $item nicht lade wegen fehlender Erweiterungsquelle'; @@ -765,11 +776,11 @@ class AppLocalizationsDe extends AppLocalizations { String get filenameFormat => 'Dateinamenformat'; @override - String get filenameShowAdvancedTags => 'Show advanced tags'; + String get filenameShowAdvancedTags => 'Erweiterte Tags anzeigen'; @override String get filenameShowAdvancedTagsDescription => - 'Enable formatted tags for track padding and date patterns'; + 'Formatierte Tags für Track-Padding und Datumsmuster aktivieren'; @override String get folderOrganizationNone => 'Keine Organisation'; @@ -918,11 +929,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String logEntries(int count) { - return 'Entries ($count)'; + return '$count Einträge'; } @override - String get credentialsTitle => 'Spotify Credentials'; + String get credentialsTitle => 'Spotify-Anmeldedaten'; @override String get credentialsDescription => @@ -984,7 +995,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Wähle wie Songtexte mit deinen Downloads gespeichert werden'; @override - String get lyricsModeEmbed => 'In Datei einbinden'; + String get lyricsModeEmbed => 'In Datei einbetten'; @override String get lyricsModeEmbedSubtitle => 'Lyrics in FLAC Metadaten gespeichert'; @@ -1001,7 +1012,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get lyricsModeBothSubtitle => - 'Lyrics einbinden und als .lrc speichern'; + 'Lyrics einbetten und als .lrc speichern'; @override String get sectionColor => 'Farbe'; @@ -1019,29 +1030,30 @@ class AppLocalizationsDe extends AppLocalizations { String get appearanceLanguage => 'App Sprache'; @override - String get settingsAppearanceSubtitle => 'Theme, colors, display'; + String get settingsAppearanceSubtitle => 'Design, Farben, Anzeige'; @override - String get settingsDownloadSubtitle => 'Service, quality, filename format'; + String get settingsDownloadSubtitle => 'Dienst, Qualität, Dateinamen-Format'; @override - String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + String get settingsOptionsSubtitle => 'Fallback, Lyrics, Covers, Updates'; @override - String get settingsExtensionsSubtitle => 'Manage download providers'; + String get settingsExtensionsSubtitle => 'Download-Anbieter verwalten'; @override - String get settingsLogsSubtitle => 'View app logs for debugging'; + String get settingsLogsSubtitle => 'App-Logs zum Debuggen anzeigen'; @override - String get loadingSharedLink => 'Loading shared link...'; + String get loadingSharedLink => 'Link wird geladen...'; @override - String get pressBackAgainToExit => 'Press back again to exit'; + String get pressBackAgainToExit => + 'Drücke wieder \"zurück\" um die App zu beenden'; @override String downloadAllCount(int count) { - return 'Download All ($count)'; + return 'Alle $count Titel herunterladen'; } @override @@ -1049,65 +1061,65 @@ class AppLocalizationsDe extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count tracks', - one: '1 track', + other: '$count Titel', + one: '1 Titel', ); return '$_temp0'; } @override - String get trackCopyFilePath => 'Copy file path'; + String get trackCopyFilePath => 'Dateipfad kopieren'; @override - String get trackRemoveFromDevice => 'Remove from device'; + String get trackRemoveFromDevice => 'Vom Gerät entfernen'; @override - String get trackLoadLyrics => 'Load Lyrics'; + String get trackLoadLyrics => 'Lade Lyrics'; @override - String get trackMetadata => 'Metadata'; + String get trackMetadata => 'Metadaten'; @override - String get trackFileInfo => 'File Info'; + String get trackFileInfo => 'Datei-Info'; @override String get trackLyrics => 'Lyrics'; @override - String get trackFileNotFound => 'File not found'; + String get trackFileNotFound => 'Datei nicht gefunden'; @override - String get trackOpenInDeezer => 'Open in Deezer'; + String get trackOpenInDeezer => 'In Deezer öffnen'; @override - String get trackOpenInSpotify => 'Open in Spotify'; + String get trackOpenInSpotify => 'In Spotify öffnen'; @override - String get trackTrackName => 'Track name'; + String get trackTrackName => 'Name des Titels'; @override - String get trackArtist => 'Artist'; + String get trackArtist => 'Künstler'; @override - String get trackAlbumArtist => 'Album artist'; + String get trackAlbumArtist => 'Album Künstler'; @override String get trackAlbum => 'Album'; @override - String get trackTrackNumber => 'Track number'; + String get trackTrackNumber => 'Titelnummer'; @override - String get trackDiscNumber => 'Disc number'; + String get trackDiscNumber => 'CD-Nummer'; @override - String get trackDuration => 'Duration'; + String get trackDuration => 'Länge'; @override - String get trackAudioQuality => 'Audio quality'; + String get trackAudioQuality => 'Audioqualität'; @override - String get trackReleaseDate => 'Release date'; + String get trackReleaseDate => 'Erscheinungsdatum'; @override String get trackGenre => 'Genre'; @@ -1116,71 +1128,73 @@ class AppLocalizationsDe extends AppLocalizations { String get trackLabel => 'Label'; @override - String get trackCopyright => 'Copyright'; + String get trackCopyright => 'Urheberrecht'; @override - String get trackDownloaded => 'Downloaded'; + String get trackDownloaded => 'Heruntergeladen'; @override - String get trackCopyLyrics => 'Copy lyrics'; + String get trackCopyLyrics => 'Lyrics kopieren'; @override - String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + String get trackLyricsNotAvailable => + 'Lyrics sind für diesen Titel nicht verfügbar'; @override - String get trackLyricsTimeout => 'Request timed out. Try again later.'; + String get trackLyricsTimeout => + 'Anfrage Timeout. Versuche es später erneut.'; @override - String get trackLyricsLoadFailed => 'Failed to load lyrics'; + String get trackLyricsLoadFailed => 'Fehler beim Laden der Lyrics'; @override - String get trackEmbedLyrics => 'Embed Lyrics'; + String get trackEmbedLyrics => 'Lyrics einbetten'; @override - String get trackLyricsEmbedded => 'Lyrics embedded successfully'; + String get trackLyricsEmbedded => 'Lyrics erfolgreich eingebettet'; @override - String get trackInstrumental => 'Instrumental track'; + String get trackInstrumental => 'Instrumentalspur'; @override - String get trackCopiedToClipboard => 'Copied to clipboard'; + String get trackCopiedToClipboard => 'In Zwischenablage kopiert'; @override - String get trackDeleteConfirmTitle => 'Remove from device?'; + String get trackDeleteConfirmTitle => 'Vom Gerät entfernen?'; @override String get trackDeleteConfirmMessage => - 'This will permanently delete the downloaded file and remove it from your history.'; + 'Dies wird die heruntergeladene Datei dauerhaft löschen und sie aus deinem Verlauf entfernen.'; @override - String get dateToday => 'Today'; + String get dateToday => 'Heute'; @override - String get dateYesterday => 'Yesterday'; + String get dateYesterday => 'Gestern'; @override String dateDaysAgo(int count) { - return '$count days ago'; + return 'Vor $count Tagen'; } @override String dateWeeksAgo(int count) { - return '$count weeks ago'; + return 'Vor $count Wochen'; } @override String dateMonthsAgo(int count) { - return '$count months ago'; + return 'Vor $count Monaten'; } @override - String get storeFilterAll => 'All'; + String get storeFilterAll => 'Alle'; @override - String get storeFilterMetadata => 'Metadata'; + String get storeFilterMetadata => 'Metadaten'; @override - String get storeFilterDownload => 'Download'; + String get storeFilterDownload => 'Herunterladen'; @override String get storeFilterUtility => 'Utility'; @@ -1192,142 +1206,146 @@ class AppLocalizationsDe extends AppLocalizations { String get storeFilterIntegration => 'Integration'; @override - String get storeClearFilters => 'Clear filters'; + String get storeClearFilters => 'Filter entfernen'; @override - String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + String get extensionDefaultProvider => 'Standard (Deezer/Spotify)'; @override - String get extensionDefaultProviderSubtitle => 'Use built-in search'; + String get extensionDefaultProviderSubtitle => 'Eingebaute Suche verwenden'; @override - String get extensionAuthor => 'Author'; + String get extensionAuthor => 'Entwickler'; @override String get extensionId => 'ID'; @override - String get extensionError => 'Error'; + String get extensionError => 'Fehler'; @override - String get extensionCapabilities => 'Capabilities'; + String get extensionCapabilities => 'Eigenschaften'; @override - String get extensionMetadataProvider => 'Metadata Provider'; + String get extensionMetadataProvider => 'Metadaten-Anbieter'; @override - String get extensionDownloadProvider => 'Download Provider'; + String get extensionDownloadProvider => 'Download-Anbieter'; @override - String get extensionLyricsProvider => 'Lyrics Provider'; + String get extensionLyricsProvider => 'Lyrics-Anbieter'; @override String get extensionUrlHandler => 'URL Handler'; @override - String get extensionQualityOptions => 'Quality Options'; + String get extensionQualityOptions => 'Qualitätsoptionen'; @override String get extensionPostProcessingHooks => 'Post-Processing Hooks'; @override - String get extensionPermissions => 'Permissions'; + String get extensionPermissions => 'Berechtigungen'; @override - String get extensionSettings => 'Settings'; + String get extensionSettings => 'Einstellungen'; @override - String get extensionRemoveButton => 'Remove Extension'; + String get extensionRemoveButton => 'Erweiterung entfernen'; @override - String get extensionUpdated => 'Updated'; + String get extensionUpdated => 'Aktualisiert'; @override - String get extensionMinAppVersion => 'Min App Version'; + String get extensionMinAppVersion => 'Min App-Version'; @override - String get extensionCustomTrackMatching => 'Custom Track Matching'; + String get extensionCustomTrackMatching => + 'Benutzerdefiniertes Track-Matching'; @override - String get extensionPostProcessing => 'Post-Processing'; + String get extensionPostProcessing => 'Post-processing'; @override String extensionHooksAvailable(int count) { - return '$count hook(s) available'; + return '$count Hook(s) verfügbar'; } @override String extensionPatternsCount(int count) { - return '$count pattern(s)'; + return '$count Muster'; } @override String extensionStrategy(String strategy) { - return 'Strategy: $strategy'; + return 'Strategie: $strategy'; } @override - String get extensionsProviderPrioritySection => 'Provider Priority'; + String get extensionsProviderPrioritySection => 'Provider-Priorität'; @override - String get extensionsInstalledSection => 'Installed Extensions'; + String get extensionsInstalledSection => 'Installierte Erweiterungen'; @override - String get extensionsNoExtensions => 'No extensions installed'; + String get extensionsNoExtensions => 'Keine Erweiterungen installiert'; @override String get extensionsNoExtensionsSubtitle => - 'Install .spotiflac-ext files to add new providers'; + 'Installiere .spotiflac-ext Dateien um neue Anbieter hinzuzufügen'; @override - String get extensionsInstallButton => 'Install Extension'; + String get extensionsInstallButton => 'Erweiterung installieren'; @override String get extensionsInfoTip => - 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + 'Erweiterungen können neue Metadaten und Download-Anbieter hinzufügen. Installiere nur Erweiterungen von vertrauenswürdigen Quellen.'; @override - String get extensionsInstalledSuccess => 'Extension installed successfully'; + String get extensionsInstalledSuccess => + 'Erweiterung erfolgreich installiert'; @override - String get extensionsDownloadPriority => 'Download Priority'; + String get extensionsDownloadPriority => 'Download-Priorität'; @override - String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + String get extensionsDownloadPrioritySubtitle => + 'Download-Service-Reihenfolge festlegen'; @override String get extensionsNoDownloadProvider => - 'No extensions with download provider'; + 'Keine Erweiterungen mit Download-Provider'; @override - String get extensionsMetadataPriority => 'Metadata Priority'; + String get extensionsMetadataPriority => 'Metadaten Priorität'; @override String get extensionsMetadataPrioritySubtitle => - 'Set search & metadata source order'; + 'Reihenfolge der Such- und Metadaten quellen festlegen'; @override String get extensionsNoMetadataProvider => - 'No extensions with metadata provider'; + 'Keine Erweiterungen mit Metadaten-Anbieter'; @override - String get extensionsSearchProvider => 'Search Provider'; + String get extensionsSearchProvider => 'Such-Provider'; @override - String get extensionsNoCustomSearch => 'No extensions with custom search'; + String get extensionsNoCustomSearch => + 'Keine Erweiterungen mit benutzerdefinierter Suche'; @override String get extensionsSearchProviderDescription => - 'Choose which service to use for searching tracks'; + 'Wähle den Dienst für die Suche von Titel'; @override - String get extensionsCustomSearch => 'Custom search'; + String get extensionsCustomSearch => 'Benutzerdefinierte Suche'; @override - String get extensionsErrorLoading => 'Error loading extension'; + String get extensionsErrorLoading => 'Fehler beim Laden der Erweiterung'; @override - String get qualityFlacLossless => 'FLAC Lossless'; + String get qualityFlacLossless => 'FLAC Verlustfrei'; @override String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; @@ -1336,21 +1354,21 @@ class AppLocalizationsDe extends AppLocalizations { String get qualityHiResFlac => 'Hi-Res FLAC'; @override - String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + String get qualityHiResFlacSubtitle => '24-Bit / bis 96kHz'; @override String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; @override - String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + String get qualityHiResFlacMaxSubtitle => '24-Bit / bis 192kHz'; @override String get qualityNote => - 'Actual quality depends on track availability from the service'; + 'Die eigentliche Qualität hängt von der Verfügbarkeit des Dienstes ab'; @override String get youtubeQualityNote => - 'YouTube provides lossy audio only. Not part of lossless fallback.'; + 'YouTube bietet nur verlustbehaftete Audioqualität. Deswegen ist es kein Teil des verlustfreien Fallbacks.'; @override String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate'; @@ -1359,13 +1377,13 @@ class AppLocalizationsDe extends AppLocalizations { String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate'; @override - String get downloadAskBeforeDownload => 'Ask Before Download'; + String get downloadAskBeforeDownload => 'Qualität vor Download fragen'; @override - String get downloadDirectory => 'Download Directory'; + String get downloadDirectory => 'Downloadverzeichnis'; @override - String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + String get downloadSeparateSinglesFolder => 'Singles Ordner trennen'; @override String get downloadAlbumFolderStructure => 'Album Folder Structure'; @@ -1378,53 +1396,53 @@ class AppLocalizationsDe extends AppLocalizations { @override String get downloadUsePrimaryArtistOnlyEnabled => - 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + 'Vorgestellte Künstler aus dem Ordnernamen entfernt (z.B. Justin Bieber, Quavo → Justin Bieber)'; @override String get downloadUsePrimaryArtistOnlyDisabled => 'Full artist string used for folder name'; @override - String get downloadSelectQuality => 'Select Quality'; + String get downloadSelectQuality => 'Qualität wählen'; @override - String get downloadFrom => 'Download From'; + String get downloadFrom => 'Herunterladen von'; @override - String get appearanceAmoledDark => 'AMOLED Dark'; + String get appearanceAmoledDark => 'AMOLED Schwarz'; @override - String get appearanceAmoledDarkSubtitle => 'Pure black background'; + String get appearanceAmoledDarkSubtitle => 'AMOLED Hintergrund'; @override - String get queueClearAll => 'Clear All'; + String get queueClearAll => 'Alles löschen'; @override String get queueClearAllMessage => - 'Are you sure you want to clear all downloads?'; + 'Bist du dir sicher, dass du alle Downloads löschen möchten?'; @override String get settingsAutoExportFailed => 'Auto-export failed downloads'; @override String get settingsAutoExportFailedSubtitle => - 'Save failed downloads to TXT file automatically'; + 'Fehlgeschlagene Downloads automatisch in eine TXT-Datei speichern'; @override - String get settingsDownloadNetwork => 'Download Network'; + String get settingsDownloadNetwork => 'Download Netzwerk'; @override - String get settingsDownloadNetworkAny => 'WiFi + Mobile Data'; + String get settingsDownloadNetworkAny => 'WLAN + Mobile Daten'; @override - String get settingsDownloadNetworkWifiOnly => 'WiFi Only'; + String get settingsDownloadNetworkWifiOnly => 'Nur WLAN'; @override String get settingsDownloadNetworkSubtitle => - 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + 'Wähle aus, welches Netzwerk für Downloads verwendet werden soll. Wenn nur WLAN aktiviert wird, werden Downloads auf mobilen Daten angehalten.'; @override - String get albumFolderArtistAlbum => 'Artist / Album'; + String get albumFolderArtistAlbum => 'Künstler/Album'; @override String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; @@ -1434,13 +1452,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get albumFolderArtistYearAlbumSubtitle => - 'Albums/Artist Name/[2005] Album Name/'; + 'Albums/Künster Name/[2005] Album Name/'; @override - String get albumFolderAlbumOnly => 'Album Only'; + String get albumFolderAlbumOnly => 'Nur Alben'; @override - String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + String get albumFolderAlbumOnlySubtitle => 'Alben/Album Name/'; @override String get albumFolderYearAlbum => '[Year] Album'; @@ -1456,39 +1474,39 @@ class AppLocalizationsDe extends AppLocalizations { 'Artist/Album/ and Artist/Singles/'; @override - String get downloadedAlbumDeleteSelected => 'Delete Selected'; + String get downloadedAlbumDeleteSelected => 'Ausgewählte löschen'; @override String downloadedAlbumDeleteMessage(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'Titel', + one: 'Titel', ); - return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + return '$count $_temp0 aus diesem Album löschen?\n\nDadurch werden auch die Dateien aus dem Speicher gelöscht.'; } @override String downloadedAlbumSelectedCount(int count) { - return '$count selected'; + return '$count ausgewählt'; } @override - String get downloadedAlbumAllSelected => 'All tracks selected'; + String get downloadedAlbumAllSelected => 'Alle Titel sind ausgewählt'; @override - String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + String get downloadedAlbumTapToSelect => 'Tippe auf Titel zum Auswählen'; @override String downloadedAlbumDeleteCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'Titel', + one: 'Titel', ); - return 'Delete $count $_temp0'; + return 'Lösche $count $_temp0'; } @override @@ -1500,22 +1518,22 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get recentTypeArtist => 'Artist'; + String get recentTypeArtist => 'Künstler'; @override String get recentTypeAlbum => 'Album'; @override - String get recentTypeSong => 'Song'; + String get recentTypeSong => 'Titel'; @override String get recentTypePlaylist => 'Playlist'; @override - String get recentEmpty => 'No recent items yet'; + String get recentEmpty => 'Noch keine aktuellen Einträge'; @override - String get recentShowAllDownloads => 'Show All Downloads'; + String get recentShowAllDownloads => 'Alle Downloads anzeigen'; @override String recentPlaylistInfo(String name) { @@ -1523,41 +1541,41 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get discographyDownload => 'Download Discography'; + String get discographyDownload => 'Diskographie herunterladen'; @override - String get discographyDownloadAll => 'Download All'; + String get discographyDownloadAll => 'Alle Herunterladen'; @override String discographyDownloadAllSubtitle(int count, int albumCount) { - return '$count tracks from $albumCount releases'; + return '$count Titel von $albumCount Releases'; } @override - String get discographyAlbumsOnly => 'Albums Only'; + String get discographyAlbumsOnly => 'Nur Alben'; @override String discographyAlbumsOnlySubtitle(int count, int albumCount) { - return '$count tracks from $albumCount albums'; + return '$count Titel von $albumCount Albums'; } @override - String get discographySinglesOnly => 'Singles & EPs Only'; + String get discographySinglesOnly => 'Nur Singles & EPs'; @override String discographySinglesOnlySubtitle(int count, int albumCount) { - return '$count tracks from $albumCount singles'; + return '$count Titel von $albumCount Singles'; } @override - String get discographySelectAlbums => 'Select Albums...'; + String get discographySelectAlbums => 'Alben auswählen...'; @override String get discographySelectAlbumsSubtitle => 'Choose specific albums or singles'; @override - String get discographyFetchingTracks => 'Fetching tracks...'; + String get discographyFetchingTracks => 'Lade Titel...'; @override String discographyFetchingAlbum(int current, int total) { @@ -1566,11 +1584,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String discographySelectedCount(int count) { - return '$count selected'; + return '$count ausgewählt'; } @override - String get discographyDownloadSelected => 'Download Selected'; + String get discographyDownloadSelected => 'Auswahl herunterladen'; @override String discographyAddedToQueue(int count) { @@ -1579,20 +1597,20 @@ class AppLocalizationsDe extends AppLocalizations { @override String discographySkippedDownloaded(int added, int skipped) { - return '$added added, $skipped already downloaded'; + return '$added hinzugefügt, $skipped bereits heruntergeladen'; } @override - String get discographyNoAlbums => 'No albums available'; + String get discographyNoAlbums => 'Es sind keine Alben verfügbar'; @override String get discographyFailedToFetch => 'Failed to fetch some albums'; @override - String get sectionStorageAccess => 'Storage Access'; + String get sectionStorageAccess => 'Speicherzugriff'; @override - String get allFilesAccess => 'All Files Access'; + String get allFilesAccess => 'Zugriff auf alle Dateien'; @override String get allFilesAccessEnabledSubtitle => 'Can write to any folder'; @@ -1602,170 +1620,170 @@ class AppLocalizationsDe extends AppLocalizations { @override String get allFilesAccessDescription => - 'Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.'; + 'Aktiviere die Option, wenn beim Speichern in benutzerdefinierten Ordnern Schreibfehler auftreten. Weil Android 13+ standardmäßig den Zugriff auf bestimmte Verzeichnisse einschränkt.'; @override String get allFilesAccessDeniedMessage => - 'Permission was denied. Please enable \'All files access\' manually in system settings.'; + 'Zugriff verweigert. Bitte aktiviere \"Zugriff auf alle Dateien\" manuell in den Systemeinstellungen.'; @override String get allFilesAccessDisabledMessage => - 'All Files Access disabled. The app will use limited storage access.'; + 'Zugriff auf alle Dateien ist deaktiviert. Die App verwendet nur begrenzten Zugriff auf den Speicher.'; @override - String get settingsLocalLibrary => 'Local Library'; + String get settingsLocalLibrary => 'Lokale Bibliothek'; @override String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates'; @override - String get settingsCache => 'Storage & Cache'; + String get settingsCache => 'Speicher & Cache'; @override String get settingsCacheSubtitle => 'View size and clear cached data'; @override - String get libraryTitle => 'Local Library'; + String get libraryTitle => 'Lokale Bibliothek'; @override - String get libraryScanSettings => 'Scan Settings'; + String get libraryScanSettings => 'Scan Einstellungen'; @override - String get libraryEnableLocalLibrary => 'Enable Local Library'; + String get libraryEnableLocalLibrary => 'Lokale Bibliothek aktivieren'; @override String get libraryEnableLocalLibrarySubtitle => 'Scan and track your existing music'; @override - String get libraryFolder => 'Library Folder'; + String get libraryFolder => 'Bibliotheksordner'; @override - String get libraryFolderHint => 'Tap to select folder'; + String get libraryFolderHint => 'Tippe um Ordner auszuwählen'; @override String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator'; @override String get libraryShowDuplicateIndicatorSubtitle => - 'Show when searching for existing tracks'; + 'Bei der Suche nach vorhandenen Titeln anzeigen'; @override - String get libraryActions => 'Actions'; + String get libraryActions => 'Aktionen'; @override - String get libraryScan => 'Scan Library'; + String get libraryScan => 'Bibliothek scannen'; @override - String get libraryScanSubtitle => 'Scan for audio files'; + String get libraryScanSubtitle => 'Suche nach Audiodateien'; @override - String get libraryScanSelectFolderFirst => 'Select a folder first'; + String get libraryScanSelectFolderFirst => 'Wähle zuerst einen Ordner'; @override - String get libraryCleanupMissingFiles => 'Cleanup Missing Files'; + String get libraryCleanupMissingFiles => 'Fehlende Dateien bereinigen'; @override String get libraryCleanupMissingFilesSubtitle => - 'Remove entries for files that no longer exist'; + 'Verlaufseinträge für Dateien löschen, die nicht mehr existieren'; @override - String get libraryClear => 'Clear Library'; + String get libraryClear => 'Bibliothek löschen'; @override - String get libraryClearSubtitle => 'Remove all scanned tracks'; + String get libraryClearSubtitle => 'Alle gescannten Titel entfernen'; @override - String get libraryClearConfirmTitle => 'Clear Library'; + String get libraryClearConfirmTitle => 'Bibliothek löschen'; @override String get libraryClearConfirmMessage => - 'This will remove all scanned tracks from your library. Your actual music files will not be deleted.'; + 'Dadurch werden alle gescannten Titel aus Ihrer Bibliothek entfernt. Ihre eigentlichen Musikdateien werden nicht gelöscht.'; @override - String get libraryAbout => 'About Local Library'; + String get libraryAbout => 'Über die lokale Bibliothek'; @override String get libraryAboutDescription => - 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.'; + 'Durchsucht deine bestehende Musiksammlung, um Duplikate beim Herunterladen zu erkennen. Unterstützt die Formate FLAC, M4A, MP3, Opus und OGG. Metadaten werden, sofern verfügbar, aus den Dateitags gelesen.'; @override String libraryTracksUnit(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: '$count Titel', + one: '1 Titel', ); return '$_temp0'; } @override String libraryLastScanned(String time) { - return 'Last scanned: $time'; + return 'Zuletzt gescannt: $time'; } @override - String get libraryLastScannedNever => 'Never'; + String get libraryLastScannedNever => 'Nie'; @override - String get libraryScanning => 'Scanning...'; + String get libraryScanning => 'Scannen...'; @override String libraryScanProgress(String progress, int total) { - return '$progress% of $total files'; + return '$progress% von $total Dateien'; } @override - String get libraryInLibrary => 'In Library'; + String get libraryInLibrary => 'In Bibliothek'; @override String libraryRemovedMissingFiles(int count) { - return 'Removed $count missing files from library'; + return 'Entfernte $count fehlende Dateien aus der Bibliothek'; } @override - String get libraryCleared => 'Library cleared'; + String get libraryCleared => 'Bibliothek geleert'; @override - String get libraryStorageAccessRequired => 'Storage Access Required'; + String get libraryStorageAccessRequired => 'Speicherzugriff erforderlich'; @override String get libraryStorageAccessMessage => - 'SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.'; + 'SpotiFLAC benötigt Speicherzugriff, um deine Musikbibliothek zu scannen. Bitte erteile die Berechtigung in den Einstellungen.'; @override - String get libraryFolderNotExist => 'Selected folder does not exist'; + String get libraryFolderNotExist => 'Der ausgewählte Ordner existiert nicht'; @override - String get librarySourceDownloaded => 'Downloaded'; + String get librarySourceDownloaded => 'Heruntergeladen'; @override - String get librarySourceLocal => 'Local'; + String get librarySourceLocal => 'Lokal'; @override - String get libraryFilterAll => 'All'; + String get libraryFilterAll => 'Alle'; @override - String get libraryFilterDownloaded => 'Downloaded'; + String get libraryFilterDownloaded => 'Heruntergeladen'; @override - String get libraryFilterLocal => 'Local'; + String get libraryFilterLocal => 'Lokal'; @override - String get libraryFilterTitle => 'Filters'; + String get libraryFilterTitle => 'Filter'; @override - String get libraryFilterReset => 'Reset'; + String get libraryFilterReset => 'Zurücksetzen'; @override - String get libraryFilterApply => 'Apply'; + String get libraryFilterApply => 'Anwenden'; @override - String get libraryFilterSource => 'Source'; + String get libraryFilterSource => 'Quelle'; @override - String get libraryFilterQuality => 'Quality'; + String get libraryFilterQuality => 'Qualität'; @override String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; @@ -1774,30 +1792,30 @@ class AppLocalizationsDe extends AppLocalizations { String get libraryFilterQualityCD => 'CD (16bit)'; @override - String get libraryFilterQualityLossy => 'Lossy'; + String get libraryFilterQualityLossy => 'Verlustbehaftet'; @override String get libraryFilterFormat => 'Format'; @override - String get libraryFilterSort => 'Sort'; + String get libraryFilterSort => 'Sortieren'; @override - String get libraryFilterSortLatest => 'Latest'; + String get libraryFilterSortLatest => 'Neuste'; @override - String get libraryFilterSortOldest => 'Oldest'; + String get libraryFilterSortOldest => 'Älteste'; @override - String get timeJustNow => 'Just now'; + String get timeJustNow => 'Gerade eben'; @override String timeMinutesAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count minutes ago', - one: '1 minute ago', + other: 'vor $count Minuten', + one: 'vor $count Minute', ); return '$_temp0'; } @@ -1807,199 +1825,201 @@ class AppLocalizationsDe extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hours ago', - one: '1 hour ago', + other: 'vor $count Stunden', + one: 'vor $count Stunde', ); return '$_temp0'; } @override - String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!'; + String get tutorialWelcomeTitle => 'Willkommen bei SpotiFLAC!'; @override String get tutorialWelcomeDesc => - 'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.'; + 'Lass uns lernen, wie du deine Lieblingsmusik in verlustfreier Qualität herunterlädst. Dieses schnelle Tutorial zeigt dir die Grundlagen.'; @override String get tutorialWelcomeTip1 => - 'Download music from Spotify, Deezer, or paste any supported URL'; + 'Lade Musik von Spotify, Deezer herunter oder jeden unterstützten Link einfügen'; @override String get tutorialWelcomeTip2 => - 'FLAC-Qualität von Tidal, Qobuz oder Deezer'; + 'Hole dir FLAC Audio von Tidal, Qobuz oder Amazon Musik'; @override String get tutorialWelcomeTip3 => - 'Automatic metadata, cover art, and lyrics embedding'; + 'Automatische Metadaten, Cover und Lyrics einbetten'; @override - String get tutorialSearchTitle => 'Finding Music'; + String get tutorialSearchTitle => 'Suche Musik'; @override String get tutorialSearchDesc => - 'There are two easy ways to find music you want to download.'; + 'Es gibt zwei einfache Möglichkeiten, Musik zu finden, die du herunterladen möchtest.'; @override - String get tutorialDownloadTitle => 'Downloading Music'; + String get tutorialDownloadTitle => 'Musik wird heruntergeladen'; @override String get tutorialDownloadDesc => - 'Downloading music is simple and fast. Here\'s how it works.'; + 'Das Herunterladen von Musik ist einfach und schnell. So funktioniert es.'; @override - String get tutorialLibraryTitle => 'Your Library'; + String get tutorialLibraryTitle => 'Deine Bibliothek'; @override String get tutorialLibraryDesc => - 'All your downloaded music is organized in the Library tab.'; + 'Die gesamte heruntergeladene Musik ist in der Bibliothek organisiert.'; @override String get tutorialLibraryTip1 => - 'View download progress and queue in the Library tab'; + 'Fortschritt und Warteschlange im Bibliothek‑Tab anzeigen'; @override String get tutorialLibraryTip2 => - 'Tap any track to play it with your music player'; + 'Tippe auf einen Titel, um ihn mit deinem Musikplayer abzuspielen'; @override String get tutorialLibraryTip3 => - 'Switch between list and grid view for better browsing'; + 'Wechsle zwischen Listen- und Gitteransicht für ein besseres Surfen'; @override - String get tutorialExtensionsTitle => 'Extensions'; + String get tutorialExtensionsTitle => 'Erweiterungen'; @override String get tutorialExtensionsDesc => - 'Extend the app\'s capabilities with community extensions.'; + 'Erweitere die Fähigkeiten der App mit Community-Erweiterungen.'; @override String get tutorialExtensionsTip1 => - 'Browse the Store tab to discover useful extensions'; + 'Im Store Tab findest du nützliche Erweiterungen'; @override String get tutorialExtensionsTip2 => - 'Add new download providers or search sources'; + 'Neue Download- oder Suchanbieter hinzufügen'; @override String get tutorialExtensionsTip3 => - 'Get lyrics, enhanced metadata, and more features'; + 'Lyrics, erweiterte Metadaten und mehr Funktionen erhalten'; @override - String get tutorialSettingsTitle => 'Customize Your Experience'; + String get tutorialSettingsTitle => 'Passe deine Benutzererfahrung an'; @override String get tutorialSettingsDesc => - 'Personalize the app in Settings to match your preferences.'; + 'Personalisiere die App in den Einstellungen nach deiner Präferenz.'; @override String get tutorialSettingsTip1 => - 'Change download location and folder organization'; + 'Downloadverzeichnis und Ordnerorganisation ändern'; @override String get tutorialSettingsTip2 => - 'Set default audio quality and format preferences'; + 'Standard Audioqualität und Formateinstellungen festlegen'; @override - String get tutorialSettingsTip3 => 'Customize app theme and appearance'; + String get tutorialSettingsTip3 => 'App-Design und Aussehen anpassen'; @override String get tutorialReadyMessage => - 'You\'re all set! Start downloading your favorite music now.'; + 'Das ist alles! Lade jetzt deine Lieblingsmusik herunter.'; @override - String get libraryForceFullScan => 'Force Full Scan'; + String get libraryForceFullScan => 'Vollen Neu-Scan erzwingen'; @override - String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache'; + String get libraryForceFullScanSubtitle => + 'Alle Dateien erneut scannen und Cache ignorieren'; @override - String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads'; + String get cleanupOrphanedDownloads => 'Verwaiste Downloads bereinigen'; @override String get cleanupOrphanedDownloadsSubtitle => - 'Remove history entries for files that no longer exist'; + 'Verlaufseinträge für Dateien löschen, die nicht mehr existieren'; @override String cleanupOrphanedDownloadsResult(int count) { - return 'Removed $count orphaned entries from history'; + return 'Entfernte $count verwaiste Einträge aus dem Verlauf'; } @override - String get cleanupOrphanedDownloadsNone => 'No orphaned entries found'; + String get cleanupOrphanedDownloadsNone => + 'Keine verwaisten Einträge gefunden'; @override - String get cacheTitle => 'Storage & Cache'; + String get cacheTitle => 'Speicher & Cache'; @override - String get cacheSummaryTitle => 'Cache overview'; + String get cacheSummaryTitle => 'Cache-Übersicht'; @override String get cacheSummarySubtitle => - 'Clearing cache will not remove downloaded music files.'; + 'Das Leeren des Caches entfernt nicht heruntergeladene Musikdateien.'; @override String cacheEstimatedTotal(String size) { - return 'Estimated cache usage: $size'; + return 'Geschätzte Cache-Größe: $size'; } @override - String get cacheSectionStorage => 'Cached Data'; + String get cacheSectionStorage => 'Zwischengespeicherte Daten'; @override - String get cacheSectionMaintenance => 'Maintenance'; + String get cacheSectionMaintenance => 'Wartung'; @override - String get cacheAppDirectory => 'App cache directory'; + String get cacheAppDirectory => 'App-Cache Verzeichnis'; @override String get cacheAppDirectoryDesc => - 'HTTP responses, WebView data, and other temporary app data.'; + 'HTTP-Antworten, WebView Daten und andere temporäre App-Daten.'; @override - String get cacheTempDirectory => 'Temporary directory'; + String get cacheTempDirectory => 'Temporäres Verzeichnis'; @override String get cacheTempDirectoryDesc => - 'Temporary files from downloads and audio conversion.'; + 'Temporäre Dateien von Downloads und Audio-Konvertierung.'; @override - String get cacheCoverImage => 'Cover image cache'; + String get cacheCoverImage => 'Cover-Cache'; @override String get cacheCoverImageDesc => - 'Downloaded album and track cover art. Will re-download when viewed.'; + 'Album- und Titelcover heruntergeladen. Werden erneut heruntergeladen.'; @override - String get cacheLibraryCover => 'Library cover cache'; + String get cacheLibraryCover => 'Bibliotheks-Cover-Cache'; @override String get cacheLibraryCoverDesc => - 'Cover art extracted from local music files. Will re-extract on next scan.'; + 'Cover aus lokalen Musikdateien extrahiert. Wird beim nächsten Scannen neu extrahiert.'; @override - String get cacheExploreFeed => 'Explore feed cache'; + String get cacheExploreFeed => 'Feed-Cache entdecken'; @override String get cacheExploreFeedDesc => - 'Explore tab content (new releases, trending). Will refresh on next visit.'; + 'Startseiten-Inhalt (neue Releases, Trends). Wird bei einem Neustart aktualisiert.'; @override - String get cacheTrackLookup => 'Track lookup cache'; + String get cacheTrackLookup => 'Titel Such-Cache'; @override String get cacheTrackLookupDesc => - 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.'; + 'Spotify/Deezer Track-ID-Lookups. Das Löschen kann die nächsten Suchergebnisse verlangsamen.'; @override String get cacheCleanupUnusedDesc => - 'Remove orphaned download history and library entries for missing files.'; + 'Verwaisten Downloadverlauf und Bibliothekseinträge für fehlende Dateien entfernen.'; @override - String get cacheNoData => 'No cached data'; + String get cacheNoData => 'Keine gecachten Daten'; @override String cacheSizeWithFiles(String size, int count) { - return '$size in $count files'; + return '$size in $count Dateien'; } @override @@ -2009,71 +2029,71 @@ class AppLocalizationsDe extends AppLocalizations { @override String cacheEntries(int count) { - return '$count entries'; + return '$count Einträge'; } @override String cacheClearSuccess(String target) { - return 'Cleared: $target'; + return 'Entfernt: $target'; } @override - String get cacheClearConfirmTitle => 'Clear cache?'; + String get cacheClearConfirmTitle => 'Cache leeren?'; @override String cacheClearConfirmMessage(String target) { - return 'This will clear cached data for $target. Downloaded music files will not be deleted.'; + return 'Dies löscht zwischengespeicherte Daten in $target. Die Musikdateien werden nicht gelöscht.'; } @override - String get cacheClearAllConfirmTitle => 'Clear all cache?'; + String get cacheClearAllConfirmTitle => 'Gesamten Cache leeren?'; @override String get cacheClearAllConfirmMessage => - 'This will clear all cache categories on this page. Downloaded music files will not be deleted.'; + 'Dadurch werden alle Cache-Kategorien auf dieser Seite gelöscht. Heruntergeladene Musikdateien werden nicht gelöscht.'; @override - String get cacheClearAll => 'Clear all cache'; + String get cacheClearAll => 'Gesamten Cache leeren'; @override - String get cacheCleanupUnused => 'Cleanup unused data'; + String get cacheCleanupUnused => 'Unbenutzte Daten bereinigen'; @override String get cacheCleanupUnusedSubtitle => - 'Remove orphaned download history and missing library entries'; + 'Verwaisten Downloadverlauf und fehlende Bibliothekseinträge löschen'; @override String cacheCleanupResult(int downloadCount, int libraryCount) { - return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries'; + return 'Bereinigung: $downloadCount verwaiste Downloads, $libraryCount fehlende Bibliothekseinträge'; } @override - String get cacheRefreshStats => 'Refresh stats'; + String get cacheRefreshStats => 'Statistik aktualisieren'; @override - String get trackSaveCoverArt => 'Save Cover Art'; + String get trackSaveCoverArt => 'Cover speichern'; @override - String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file'; + String get trackSaveCoverArtSubtitle => 'Albumcover als .jpg Datei speichern'; @override - String get trackSaveLyrics => 'Save Lyrics (.lrc)'; + String get trackSaveLyrics => 'Lyrics als .lrc speichern'; @override - String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + String get trackSaveLyricsSubtitle => 'Lade Lyrics als .lrc Datei'; @override - String get trackSaveLyricsProgress => 'Saving lyrics...'; + String get trackSaveLyricsProgress => 'Speichere Lyrics...'; @override - String get trackReEnrich => 'Re-enrich'; + String get trackReEnrich => 'Neu-anreichern'; @override String get trackReEnrichOnlineSubtitle => - 'Search metadata online and embed into file'; + 'Metadaten online suchen und in Datei einbinden'; @override - String get trackEditMetadata => 'Edit Metadata'; + String get trackEditMetadata => 'Metadaten bearbeiten'; @override String trackCoverSaved(String fileName) { @@ -2085,43 +2105,44 @@ class AppLocalizationsDe extends AppLocalizations { @override String trackLyricsSaved(String fileName) { - return 'Lyrics saved to $fileName'; + return 'Lyrics in $fileName gespeichert'; } @override - String get trackReEnrichProgress => 'Re-enriching metadata...'; + String get trackReEnrichProgress => 'Metadaten neu anreichern...'; @override - String get trackReEnrichSearching => 'Searching metadata online...'; + String get trackReEnrichSearching => 'Suche Metadaten online...'; @override - String get trackReEnrichSuccess => 'Metadata re-enriched successfully'; + String get trackReEnrichSuccess => 'Metadaten erfolgreich neu angereichert'; @override - String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + String get trackReEnrichFfmpegFailed => + 'FFmpeg Metadaten-Einbettung fehlgeschlagen'; @override String trackSaveFailed(String error) { - return 'Failed: $error'; + return 'Fehler: $error'; } @override - String get trackConvertFormat => 'Convert Format'; + String get trackConvertFormat => 'Format konvertieren'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => 'In MP3 oder Opus konvertieren'; @override - String get trackConvertTitle => 'Convert Audio'; + String get trackConvertTitle => 'Audio konvertieren'; @override - String get trackConvertTargetFormat => 'Target Format'; + String get trackConvertTargetFormat => 'Zielformat'; @override String get trackConvertBitrate => 'Bitrate'; @override - String get trackConvertConfirmTitle => 'Confirm Conversion'; + String get trackConvertConfirmTitle => 'Konvertierung bestätigen'; @override String trackConvertConfirmMessage( @@ -2129,198 +2150,199 @@ class AppLocalizationsDe extends AppLocalizations { String targetFormat, String bitrate, ) { - return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + return 'Konvertieren von $sourceFormat in $targetFormat bei $bitrate?\n\nDie Originaldatei wird nach der Konvertierung gelöscht.'; } @override - String get trackConvertConverting => 'Converting audio...'; + String get trackConvertConverting => 'Konvertiere Audio...'; @override String trackConvertSuccess(String format) { - return 'Converted to $format successfully'; + return 'Konvertiert in $format erfolgreich'; } @override - String get trackConvertFailed => 'Conversion failed'; + String get trackConvertFailed => 'Konvertierung fehlgeschlagen'; @override - String get actionCreate => 'Create'; + String get actionCreate => 'Erstellen'; @override - String get collectionFoldersTitle => 'My folders'; + String get collectionFoldersTitle => 'Meine Ordner'; @override - String get collectionWishlist => 'Wishlist'; + String get collectionWishlist => 'Wunschliste'; @override - String get collectionLoved => 'Loved'; + String get collectionLoved => 'Lieblingssongs'; @override - String get collectionPlaylists => 'Playlists'; + String get collectionPlaylists => 'Playlisten'; @override String get collectionPlaylist => 'Playlist'; @override - String get collectionAddToPlaylist => 'Add to playlist'; + String get collectionAddToPlaylist => 'Zur Playlist hinzufügen'; @override - String get collectionCreatePlaylist => 'Create playlist'; + String get collectionCreatePlaylist => 'Playlist erstellen'; @override - String get collectionNoPlaylistsYet => 'No playlists yet'; + String get collectionNoPlaylistsYet => 'Noch keine Playlists'; @override String get collectionNoPlaylistsSubtitle => - 'Create a playlist to start categorizing tracks'; + 'Playlist erstellen, um Titel zu kategorisieren'; @override String collectionPlaylistTracks(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count tracks', - one: '1 track', + other: '$count Titel', + one: '1 Titel', ); return '$_temp0'; } @override String collectionAddedToPlaylist(String playlistName) { - return 'Added to \"$playlistName\"'; + return 'Zu \"$playlistName \" hinzugefügt'; } @override String collectionAlreadyInPlaylist(String playlistName) { - return 'Already in \"$playlistName\"'; + return 'Bereits in \"$playlistName\"'; } @override - String get collectionPlaylistCreated => 'Playlist created'; + String get collectionPlaylistCreated => 'Playlist erstellt'; @override - String get collectionPlaylistNameHint => 'Playlist name'; + String get collectionPlaylistNameHint => 'Playlist-Name'; @override - String get collectionPlaylistNameRequired => 'Playlist name is required'; + String get collectionPlaylistNameRequired => 'Playlist-Name ist erforderlich'; @override - String get collectionRenamePlaylist => 'Rename playlist'; + String get collectionRenamePlaylist => 'Playlist umbenennen'; @override - String get collectionDeletePlaylist => 'Delete playlist'; + String get collectionDeletePlaylist => 'Playlist löschen'; @override String collectionDeletePlaylistMessage(String playlistName) { - return 'Delete \"$playlistName\" and all tracks inside it?'; + return 'Willst du \"$playlistName\" und alle darin enthaltenen Titel löschen?'; } @override - String get collectionPlaylistDeleted => 'Playlist deleted'; + String get collectionPlaylistDeleted => 'Playlist gelöscht'; @override - String get collectionPlaylistRenamed => 'Playlist renamed'; + String get collectionPlaylistRenamed => 'Playlist umbenannt'; @override - String get collectionWishlistEmptyTitle => 'Wishlist is empty'; + String get collectionWishlistEmptyTitle => 'Wunschliste ist leer'; @override String get collectionWishlistEmptySubtitle => - 'Tap + on tracks to save what you want to download later'; + 'Tippe auf das + bei den Titeln, um sie zum späteren Herunterladen zu speichern'; @override - String get collectionLovedEmptyTitle => 'Loved folder is empty'; + String get collectionLovedEmptyTitle => 'Lieblingssongs sind leer'; @override String get collectionLovedEmptySubtitle => - 'Tap love on tracks to keep your favorites'; + 'Tippe auf das Herz, um deine Favoriten zu behalten'; @override - String get collectionPlaylistEmptyTitle => 'Playlist is empty'; + String get collectionPlaylistEmptyTitle => 'Die Playlist ist leer'; @override String get collectionPlaylistEmptySubtitle => - 'Long-press + on any track to add it here'; + 'Drücke lange + auf einem beliebigen Titel, um ihn hier hinzuzufügen'; @override - String get collectionRemoveFromPlaylist => 'Remove from playlist'; + String get collectionRemoveFromPlaylist => 'Von Playlist entfernen'; @override - String get collectionRemoveFromFolder => 'Remove from folder'; + String get collectionRemoveFromFolder => 'Aus Ordner entfernen'; @override String collectionRemoved(String trackName) { - return '\"$trackName\" removed'; + return '\"$trackName\" entfernt'; } @override String collectionAddedToLoved(String trackName) { - return '\"$trackName\" added to Loved'; + return '\"$trackName\" zu Lieblingssongs hinzugefügt'; } @override String collectionRemovedFromLoved(String trackName) { - return '\"$trackName\" removed from Loved'; + return '\"$trackName\" aus Lieblingssongs entfernt'; } @override String collectionAddedToWishlist(String trackName) { - return '\"$trackName\" added to Wishlist'; + return '\"$trackName\" zur Wunschliste hinzugefügt'; } @override String collectionRemovedFromWishlist(String trackName) { - return '\"$trackName\" removed from Wishlist'; + return '\"$trackName\" aus der Wunschliste entfernt'; } @override - String get trackOptionAddToLoved => 'Add to Loved'; + String get trackOptionAddToLoved => 'Zu Lieblingssongs hinzufügen'; @override - String get trackOptionRemoveFromLoved => 'Remove from Loved'; + String get trackOptionRemoveFromLoved => 'Aus Lieblingssongs entfernt'; @override - String get trackOptionAddToWishlist => 'Add to Wishlist'; + String get trackOptionAddToWishlist => 'Zur Wunschliste hinzufügen'; @override - String get trackOptionRemoveFromWishlist => 'Remove from Wishlist'; + String get trackOptionRemoveFromWishlist => 'Von der Wunschliste entfernen'; @override - String get collectionPlaylistChangeCover => 'Change cover image'; + String get collectionPlaylistChangeCover => 'Coverbild ändern'; @override - String get collectionPlaylistRemoveCover => 'Remove cover image'; + String get collectionPlaylistRemoveCover => 'Cover entfernen'; @override String selectionShareCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'Titel', + one: 'Titel', ); - return 'Share $count $_temp0'; + return 'Teile $count $_temp0'; } @override - String get selectionShareNoFiles => 'No shareable files found'; + String get selectionShareNoFiles => 'Keine teilbare Dateien gefunden'; @override String selectionConvertCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'Titel', + one: 'Titel', ); - return 'Convert $count $_temp0'; + return 'Konvertiere $count $_temp0'; } @override - String get selectionConvertNoConvertible => 'No convertible tracks selected'; + String get selectionConvertNoConvertible => + 'Keine konvertierbare Titel ausgewählt'; @override - String get selectionBatchConvertConfirmTitle => 'Batch Convert'; + String get selectionBatchConvertConfirmTitle => 'Batch-Konvertierung'; @override String selectionBatchConvertConfirmMessage( @@ -2331,30 +2353,30 @@ class AppLocalizationsDe extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'Titel', + one: 'Titel', ); - return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; + return 'Konvertiere $count $format $_temp0 zu $bitrate?\n\nOriginaldateien werden nach der Konvertierung gelöscht.'; } @override String selectionBatchConvertProgress(int current, int total) { - return 'Converting $current of $total...'; + return 'Konvertiere $current von $total...'; } @override String selectionBatchConvertSuccess(int success, int total, String format) { - return 'Converted $success of $total tracks to $format'; + return '$success von $total Titeln in $format konvertiert'; } @override String downloadedAlbumDownloadedCount(int count) { - return '$count downloaded'; + return '$count heruntergeladen'; } @override String get downloadUseAlbumArtistForFoldersAlbumSubtitle => - 'Artist folders use Album Artist when available'; + 'Künstlerordner verwenden den Album-Interpreten, wenn verfügbar'; @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index c98cf0ee..c583ab11 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -688,6 +688,17 @@ class AppLocalizationsEn extends AppLocalizations { @override String get errorNoTracksFound => 'No tracks found'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return 'Cannot load $item: missing extension source'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index efc7a016..f4758a27 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -688,6 +688,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get errorNoTracksFound => 'No tracks found'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return 'Cannot load $item: missing extension source'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 89d94355..c93b863f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -358,7 +358,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; @override String get artistAlbums => 'Albums'; @@ -690,6 +690,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String get errorNoTracksFound => 'No tracks found'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return 'Cannot load $item: missing extension source'; @@ -1811,7 +1822,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get tutorialWelcomeTip2 => - 'Audio en qualité FLAC depuis Tidal, Qobuz ou Deezer'; + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; @override String get tutorialWelcomeTip3 => diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 5b6ff05f..a12620b5 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -356,7 +356,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; @override String get artistAlbums => 'Albums'; @@ -688,6 +688,17 @@ class AppLocalizationsHi extends AppLocalizations { @override String get errorNoTracksFound => 'No tracks found'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return 'Cannot load $item: missing extension source'; @@ -1809,7 +1820,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get tutorialWelcomeTip2 => - 'Tidal, Qobuz, या Deezer से FLAC गुणवत्ता ऑडियो प्राप्त करें'; + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; @override String get tutorialWelcomeTip3 => diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 4312a41a..e408e8ae 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -15,7 +15,7 @@ class AppLocalizationsId extends AppLocalizations { String get navHome => 'Beranda'; @override - String get navLibrary => 'Library'; + String get navLibrary => 'Pustaka'; @override String get navSettings => 'Pengaturan'; @@ -45,7 +45,7 @@ class AppLocalizationsId extends AppLocalizations { String get historyFilterSingles => 'Single'; @override - String get historySearchHint => 'Search history...'; + String get historySearchHint => 'Cari riwayat...'; @override String get settingsTitle => 'Pengaturan'; @@ -104,7 +104,7 @@ class AppLocalizationsId extends AppLocalizations { String get appearanceHistoryViewList => 'Daftar'; @override - String get appearanceHistoryViewGrid => 'Grid'; + String get appearanceHistoryViewGrid => 'Kisi'; @override String get optionsTitle => 'Opsi'; @@ -126,7 +126,7 @@ class AppLocalizationsId extends AppLocalizations { 'Ketuk Deezer atau Spotify untuk beralih dari ekstensi'; @override - String get optionsAutoFallback => 'Auto Fallback'; + String get optionsAutoFallback => 'Cadangan Otomatis'; @override String get optionsAutoFallbackSubtitle => @@ -217,7 +217,7 @@ class AppLocalizationsId extends AppLocalizations { @override String optionsSpotifyCredentialsConfigured(String clientId) { - return 'Client ID: $clientId...'; + return 'ID Klien: $clientId...'; } @override @@ -230,7 +230,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get optionsSpotifyDeprecationWarning => - 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.'; + 'Pencarian Spotify akan dihentikan pada 3 Maret 2026 karena perubahan API Spotify. Silakan beralih ke Deezer.'; @override String get extensionsTitle => 'Ekstensi'; @@ -283,7 +283,7 @@ class AppLocalizationsId extends AppLocalizations { 'Seniman berbakat yang membuat logo aplikasi kita yang indah!'; @override - String get aboutTranslators => 'Translators'; + String get aboutTranslators => 'Penerjemah'; @override String get aboutSpecialThanks => 'Terima Kasih Khusus'; @@ -311,19 +311,19 @@ class AppLocalizationsId extends AppLocalizations { 'Sarankan fitur baru untuk aplikasi'; @override - String get aboutTelegramChannel => 'Telegram Channel'; + String get aboutTelegramChannel => 'Saluran Telegram'; @override - String get aboutTelegramChannelSubtitle => 'Announcements and updates'; + String get aboutTelegramChannelSubtitle => 'Pengumuman dan pembaruan'; @override - String get aboutTelegramChat => 'Telegram Community'; + String get aboutTelegramChat => 'Komunitas Telegram'; @override - String get aboutTelegramChatSubtitle => 'Chat with other users'; + String get aboutTelegramChatSubtitle => 'Berbincang dengan pengguna lain'; @override - String get aboutSocial => 'Social'; + String get aboutSocial => 'Sosial'; @override String get aboutApp => 'Aplikasi'; @@ -341,7 +341,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get aboutSjdonadoDesc => - 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!'; + 'Pencipta I Don\'t Have Spotify (IDHS). Penyelesai tautan cadangan yang menyelamatkan keadaan!'; @override String get aboutDabMusic => 'DAB Music'; @@ -355,11 +355,11 @@ class AppLocalizationsId extends AppLocalizations { @override String get aboutSpotiSaverDesc => - 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!'; + 'Tidal perangkat streaming FLAC resolusi tinggi. Bagian penting dari teka-teki tanpa kehilangan kualitas!'; @override String get aboutAppDescription => - 'Unduh lagu Spotify dalam kualitas lossless dari Tidal dan Qobuz.'; + 'Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.'; @override String get artistAlbums => 'Album'; @@ -456,7 +456,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get setupIcloudNotSupported => - 'iCloud Drive is not supported. Please use the app Documents folder.'; + 'iCloud Drive tidak didukung. Silakan gunakan folder Dokumen di aplikasi.'; @override String get setupDownloadInFlac => 'Unduh lagu Spotify dalam format FLAC'; @@ -593,7 +593,7 @@ class AppLocalizationsId extends AppLocalizations { @override String csvImportTracks(int count) { - return '$count tracks from CSV'; + return '$count trek dari CSV'; } @override @@ -613,7 +613,7 @@ class AppLocalizationsId extends AppLocalizations { @override String snackbarAlreadyInLibrary(String trackName) { - return '\"$trackName\" already exists in your library'; + return '\"$trackName\" sudah ada di perpustakaan Anda'; } @override @@ -691,6 +691,17 @@ class AppLocalizationsId extends AppLocalizations { @override String get errorNoTracksFound => 'Tidak ada lagu ditemukan'; + @override + String get errorUrlNotRecognized => 'Link tidak dikenali'; + + @override + String get errorUrlNotRecognizedMessage => + 'Link ini tidak didukung. Pastikan URL benar dan ekstensi yang kompatibel sudah terpasang.'; + + @override + String get errorUrlFetchFailed => + 'Gagal memuat konten dari link ini. Silakan coba lagi.'; + @override String errorMissingExtensionSource(String item) { return 'Tidak dapat memuat $item: sumber ekstensi tidak ada'; @@ -755,11 +766,11 @@ class AppLocalizationsId extends AppLocalizations { String get filenameFormat => 'Format Nama File'; @override - String get filenameShowAdvancedTags => 'Tampilkan tag lanjutan'; + String get filenameShowAdvancedTags => 'Show advanced tags'; @override String get filenameShowAdvancedTagsDescription => - 'Aktifkan tag format untuk padding nomor lagu dan pola tanggal'; + 'Enable formatted tags for track padding and date patterns'; @override String get folderOrganizationNone => 'Tidak ada'; @@ -1343,10 +1354,10 @@ class AppLocalizationsId extends AppLocalizations { 'YouTube provides lossy audio only. Not part of lossless fallback.'; @override - String get youtubeOpusBitrateTitle => 'Bitrate Opus YouTube'; + String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate'; @override - String get youtubeMp3BitrateTitle => 'Bitrate MP3 YouTube'; + String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate'; @override String get downloadAskBeforeDownload => 'Tanya Sebelum Unduh'; @@ -1684,8 +1695,8 @@ class AppLocalizationsId extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'trek', - one: 'trek', + other: 'tracks', + one: 'track', ); return '$_temp0'; } @@ -1816,7 +1827,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get tutorialWelcomeTip2 => - 'Dapatkan audio kualitas FLAC dari Tidal, Qobuz, atau Deezer'; + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; @override String get tutorialWelcomeTip3 => @@ -2134,10 +2145,10 @@ class AppLocalizationsId extends AppLocalizations { String get trackConvertFailed => 'Conversion failed'; @override - String get actionCreate => 'Buat'; + String get actionCreate => 'Create'; @override - String get collectionFoldersTitle => 'Folder saya'; + String get collectionFoldersTitle => 'My folders'; @override String get collectionWishlist => 'Wishlist'; @@ -2146,172 +2157,171 @@ class AppLocalizationsId extends AppLocalizations { String get collectionLoved => 'Loved'; @override - String get collectionPlaylists => 'Playlist'; + String get collectionPlaylists => 'Playlists'; @override String get collectionPlaylist => 'Playlist'; @override - String get collectionAddToPlaylist => 'Tambahkan ke playlist'; + String get collectionAddToPlaylist => 'Add to playlist'; @override - String get collectionCreatePlaylist => 'Buat playlist'; + String get collectionCreatePlaylist => 'Create playlist'; @override - String get collectionNoPlaylistsYet => 'Belum ada playlist'; + String get collectionNoPlaylistsYet => 'No playlists yet'; @override String get collectionNoPlaylistsSubtitle => - 'Buat playlist untuk mulai mengategorikan lagu'; + 'Create a playlist to start categorizing tracks'; @override String collectionPlaylistTracks(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count lagu', - one: '1 lagu', + other: '$count tracks', + one: '1 track', ); return '$_temp0'; } @override String collectionAddedToPlaylist(String playlistName) { - return 'Ditambahkan ke \"$playlistName\"'; + return 'Added to \"$playlistName\"'; } @override String collectionAlreadyInPlaylist(String playlistName) { - return 'Sudah ada di \"$playlistName\"'; + return 'Already in \"$playlistName\"'; } @override - String get collectionPlaylistCreated => 'Playlist berhasil dibuat'; + String get collectionPlaylistCreated => 'Playlist created'; @override - String get collectionPlaylistNameHint => 'Nama playlist'; + String get collectionPlaylistNameHint => 'Playlist name'; @override - String get collectionPlaylistNameRequired => 'Nama playlist wajib diisi'; + String get collectionPlaylistNameRequired => 'Playlist name is required'; @override - String get collectionRenamePlaylist => 'Ubah nama playlist'; + String get collectionRenamePlaylist => 'Rename playlist'; @override - String get collectionDeletePlaylist => 'Hapus playlist'; + String get collectionDeletePlaylist => 'Delete playlist'; @override String collectionDeletePlaylistMessage(String playlistName) { - return 'Hapus \"$playlistName\" beserta semua lagunya?'; + return 'Delete \"$playlistName\" and all tracks inside it?'; } @override - String get collectionPlaylistDeleted => 'Playlist dihapus'; + String get collectionPlaylistDeleted => 'Playlist deleted'; @override - String get collectionPlaylistRenamed => 'Nama playlist diperbarui'; + String get collectionPlaylistRenamed => 'Playlist renamed'; @override - String get collectionWishlistEmptyTitle => 'Wishlist masih kosong'; + String get collectionWishlistEmptyTitle => 'Wishlist is empty'; @override String get collectionWishlistEmptySubtitle => - 'Tap + di lagu untuk menyimpan yang ingin diunduh nanti'; + 'Tap + on tracks to save what you want to download later'; @override - String get collectionLovedEmptyTitle => 'Folder Loved masih kosong'; + String get collectionLovedEmptyTitle => 'Loved folder is empty'; @override String get collectionLovedEmptySubtitle => - 'Tap love di lagu untuk menyimpan favoritmu'; + 'Tap love on tracks to keep your favorites'; @override - String get collectionPlaylistEmptyTitle => 'Playlist masih kosong'; + String get collectionPlaylistEmptyTitle => 'Playlist is empty'; @override String get collectionPlaylistEmptySubtitle => - 'Tekan lama tombol + pada lagu untuk menambahkannya ke sini'; + 'Long-press + on any track to add it here'; @override - String get collectionRemoveFromPlaylist => 'Hapus dari playlist'; + String get collectionRemoveFromPlaylist => 'Remove from playlist'; @override - String get collectionRemoveFromFolder => 'Hapus dari folder'; + String get collectionRemoveFromFolder => 'Remove from folder'; @override String collectionRemoved(String trackName) { - return '\"$trackName\" dihapus'; + return '\"$trackName\" removed'; } @override String collectionAddedToLoved(String trackName) { - return '\"$trackName\" ditambahkan ke Loved'; + return '\"$trackName\" added to Loved'; } @override String collectionRemovedFromLoved(String trackName) { - return '\"$trackName\" dihapus dari Loved'; + return '\"$trackName\" removed from Loved'; } @override String collectionAddedToWishlist(String trackName) { - return '\"$trackName\" ditambahkan ke Wishlist'; + return '\"$trackName\" added to Wishlist'; } @override String collectionRemovedFromWishlist(String trackName) { - return '\"$trackName\" dihapus dari Wishlist'; + return '\"$trackName\" removed from Wishlist'; } @override - String get trackOptionAddToLoved => 'Tambahkan ke Loved'; + String get trackOptionAddToLoved => 'Add to Loved'; @override - String get trackOptionRemoveFromLoved => 'Hapus dari Loved'; + String get trackOptionRemoveFromLoved => 'Remove from Loved'; @override - String get trackOptionAddToWishlist => 'Tambahkan ke Wishlist'; + String get trackOptionAddToWishlist => 'Add to Wishlist'; @override - String get trackOptionRemoveFromWishlist => 'Hapus dari Wishlist'; + String get trackOptionRemoveFromWishlist => 'Remove from Wishlist'; @override - String get collectionPlaylistChangeCover => 'Ubah gambar sampul'; + String get collectionPlaylistChangeCover => 'Change cover image'; @override - String get collectionPlaylistRemoveCover => 'Hapus gambar sampul'; + String get collectionPlaylistRemoveCover => 'Remove cover image'; @override String selectionShareCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'trek', - one: 'trek', + other: 'tracks', + one: 'track', ); - return 'Bagikan $count $_temp0'; + return 'Share $count $_temp0'; } @override - String get selectionShareNoFiles => 'Tidak ada file yang dapat dibagikan'; + String get selectionShareNoFiles => 'No shareable files found'; @override String selectionConvertCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'trek', - one: 'trek', + other: 'tracks', + one: 'track', ); - return 'Konversi $count $_temp0'; + return 'Convert $count $_temp0'; } @override - String get selectionConvertNoConvertible => - 'Tidak ada trek yang dapat dikonversi dipilih'; + String get selectionConvertNoConvertible => 'No convertible tracks selected'; @override - String get selectionBatchConvertConfirmTitle => 'Konversi Massal'; + String get selectionBatchConvertConfirmTitle => 'Batch Convert'; @override String selectionBatchConvertConfirmMessage( @@ -2322,20 +2332,20 @@ class AppLocalizationsId extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'trek', - one: 'trek', + other: 'tracks', + one: 'track', ); - return 'Konversi $count $_temp0 ke $format pada $bitrate?\n\nFile asli akan dihapus setelah konversi.'; + return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; } @override String selectionBatchConvertProgress(int current, int total) { - return 'Mengonversi $current dari $total...'; + return 'Converting $current of $total...'; } @override String selectionBatchConvertSuccess(int success, int total, String format) { - return 'Berhasil mengonversi $success dari $total trek ke $format'; + return 'Converted $success of $total tracks to $format'; } @override diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 9aa5a2fb..9e898bdf 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -15,7 +15,7 @@ class AppLocalizationsJa extends AppLocalizations { String get navHome => 'ホーム'; @override - String get navLibrary => 'Library'; + String get navLibrary => 'ライブラリ'; @override String get navSettings => '設定'; @@ -160,7 +160,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String optionsConcurrentParallel(int count) { - return '$count parallel downloads'; + return '$count 件の分割ダウンロード'; } @override @@ -352,7 +352,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get aboutAppDescription => - 'Tidal、Qobuz から Spotify のトラックをロスレス品質でダウンロードします。'; + 'Tidal、Qobuz、Amazon Music から Spotify のトラックをロスレス品質でダウンロードします。'; @override String get artistAlbums => 'アルバム'; @@ -683,6 +683,17 @@ class AppLocalizationsJa extends AppLocalizations { @override String get errorNoTracksFound => 'トラックがありません'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return '$item を読み込めません: 拡張ソースがありません'; @@ -1111,7 +1122,7 @@ class AppLocalizationsJa extends AppLocalizations { String get trackLyricsLoadFailed => '歌詞の読み込みに失敗しました'; @override - String get trackEmbedLyrics => 'Embed Lyrics'; + String get trackEmbedLyrics => '歌詞を埋め込む'; @override String get trackLyricsEmbedded => 'Lyrics embedded successfully'; @@ -1325,10 +1336,10 @@ class AppLocalizationsJa extends AppLocalizations { 'YouTube provides lossy audio only. Not part of lossless fallback.'; @override - String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate'; + String get youtubeOpusBitrateTitle => 'YouTube Opus のビットレート'; @override - String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate'; + String get youtubeMp3BitrateTitle => 'YouTube MP3 のビットレート'; @override String get downloadAskBeforeDownload => 'ダウンロード前に確認する'; @@ -1375,20 +1386,20 @@ class AppLocalizationsJa extends AppLocalizations { String get queueClearAllMessage => 'すべてのダウンロードを消去してもよろしいですか?'; @override - String get settingsAutoExportFailed => 'Auto-export failed downloads'; + String get settingsAutoExportFailed => 'ダウンロードの自動エクスポートに失敗しました'; @override String get settingsAutoExportFailedSubtitle => 'Save failed downloads to TXT file automatically'; @override - String get settingsDownloadNetwork => 'Download Network'; + String get settingsDownloadNetwork => 'ダウンロードネットワーク'; @override - String get settingsDownloadNetworkAny => 'WiFi + Mobile Data'; + String get settingsDownloadNetworkAny => 'Wi-Fi + モバイルデータ'; @override - String get settingsDownloadNetworkWifiOnly => 'WiFi Only'; + String get settingsDownloadNetworkWifiOnly => 'Wi-Fi のみ'; @override String get settingsDownloadNetworkSubtitle => @@ -1419,7 +1430,7 @@ class AppLocalizationsJa extends AppLocalizations { String get albumFolderYearAlbumSubtitle => 'アルバム/[2005] アルバム名/'; @override - String get albumFolderArtistAlbumSingles => 'Artist / Album + Singles'; + String get albumFolderArtistAlbumSingles => 'アーティスト / アルバム + シングル'; @override String get albumFolderArtistAlbumSinglesSubtitle => @@ -1485,7 +1496,7 @@ class AppLocalizationsJa extends AppLocalizations { String get recentEmpty => 'No recent items yet'; @override - String get recentShowAllDownloads => 'Show All Downloads'; + String get recentShowAllDownloads => 'すべてのダウンロードを表示'; @override String recentPlaylistInfo(String name) { @@ -1559,10 +1570,10 @@ class AppLocalizationsJa extends AppLocalizations { String get discographyFailedToFetch => '一部のアルバムの取得に失敗しました'; @override - String get sectionStorageAccess => 'Storage Access'; + String get sectionStorageAccess => 'ストレージアクセス'; @override - String get allFilesAccess => 'All Files Access'; + String get allFilesAccess => 'すべてのファイルへのアクセス'; @override String get allFilesAccessEnabledSubtitle => 'Can write to any folder'; @@ -1583,35 +1594,35 @@ class AppLocalizationsJa extends AppLocalizations { 'All Files Access disabled. The app will use limited storage access.'; @override - String get settingsLocalLibrary => 'Local Library'; + String get settingsLocalLibrary => 'ローカルライブラリ'; @override String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates'; @override - String get settingsCache => 'Storage & Cache'; + String get settingsCache => 'ストレージとキャッシュ'; @override String get settingsCacheSubtitle => 'View size and clear cached data'; @override - String get libraryTitle => 'Local Library'; + String get libraryTitle => 'ローカルライブラリ'; @override - String get libraryScanSettings => 'Scan Settings'; + String get libraryScanSettings => 'スキャン設定'; @override - String get libraryEnableLocalLibrary => 'Enable Local Library'; + String get libraryEnableLocalLibrary => 'ローカルライブラリを有効'; @override String get libraryEnableLocalLibrarySubtitle => 'Scan and track your existing music'; @override - String get libraryFolder => 'Library Folder'; + String get libraryFolder => 'ライブラリのフォルダ'; @override - String get libraryFolderHint => 'Tap to select folder'; + String get libraryFolderHint => 'タップでフォルダを選択'; @override String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator'; @@ -1621,13 +1632,13 @@ class AppLocalizationsJa extends AppLocalizations { 'Show when searching for existing tracks'; @override - String get libraryActions => 'Actions'; + String get libraryActions => 'アクション'; @override - String get libraryScan => 'Scan Library'; + String get libraryScan => 'ライブラリをスキャン'; @override - String get libraryScanSubtitle => 'Scan for audio files'; + String get libraryScanSubtitle => 'オーディオファイルをスキャン'; @override String get libraryScanSelectFolderFirst => 'Select a folder first'; @@ -1640,20 +1651,20 @@ class AppLocalizationsJa extends AppLocalizations { 'Remove entries for files that no longer exist'; @override - String get libraryClear => 'Clear Library'; + String get libraryClear => 'ライブラリを消去'; @override String get libraryClearSubtitle => 'Remove all scanned tracks'; @override - String get libraryClearConfirmTitle => 'Clear Library'; + String get libraryClearConfirmTitle => 'ライブラリを消去'; @override String get libraryClearConfirmMessage => 'This will remove all scanned tracks from your library. Your actual music files will not be deleted.'; @override - String get libraryAbout => 'About Local Library'; + String get libraryAbout => 'ローカルライブラリについて'; @override String get libraryAboutDescription => @@ -1672,14 +1683,14 @@ class AppLocalizationsJa extends AppLocalizations { @override String libraryLastScanned(String time) { - return 'Last scanned: $time'; + return '最終スキャン: $time'; } @override String get libraryLastScannedNever => 'Never'; @override - String get libraryScanning => 'Scanning...'; + String get libraryScanning => 'スキャン中...'; @override String libraryScanProgress(String progress, int total) { @@ -1687,7 +1698,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get libraryInLibrary => 'In Library'; + String get libraryInLibrary => 'ライブラリ内'; @override String libraryRemovedMissingFiles(int count) { @@ -1698,7 +1709,7 @@ class AppLocalizationsJa extends AppLocalizations { String get libraryCleared => 'Library cleared'; @override - String get libraryStorageAccessRequired => 'Storage Access Required'; + String get libraryStorageAccessRequired => 'ストレージアクセスが必要です'; @override String get libraryStorageAccessMessage => @@ -1708,37 +1719,37 @@ class AppLocalizationsJa extends AppLocalizations { String get libraryFolderNotExist => 'Selected folder does not exist'; @override - String get librarySourceDownloaded => 'Downloaded'; + String get librarySourceDownloaded => 'ダウンロード済み'; @override - String get librarySourceLocal => 'Local'; + String get librarySourceLocal => 'ローカル'; @override - String get libraryFilterAll => 'All'; + String get libraryFilterAll => 'すべて'; @override - String get libraryFilterDownloaded => 'Downloaded'; + String get libraryFilterDownloaded => 'ダウンロード済み'; @override - String get libraryFilterLocal => 'Local'; + String get libraryFilterLocal => 'ローカル'; @override - String get libraryFilterTitle => 'Filters'; + String get libraryFilterTitle => 'フィルター'; @override - String get libraryFilterReset => 'Reset'; + String get libraryFilterReset => 'リセット'; @override - String get libraryFilterApply => 'Apply'; + String get libraryFilterApply => '適用'; @override - String get libraryFilterSource => 'Source'; + String get libraryFilterSource => 'ソース'; @override - String get libraryFilterQuality => 'Quality'; + String get libraryFilterQuality => '品質'; @override - String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + String get libraryFilterQualityHiRes => 'ハイレゾ (24bit)'; @override String get libraryFilterQualityCD => 'CD (16bit)'; @@ -1747,7 +1758,7 @@ class AppLocalizationsJa extends AppLocalizations { String get libraryFilterQualityLossy => 'Lossy'; @override - String get libraryFilterFormat => 'Format'; + String get libraryFilterFormat => '形式'; @override String get libraryFilterSort => 'Sort'; @@ -1766,8 +1777,8 @@ class AppLocalizationsJa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count minutes ago', - one: '1 minute ago', + other: '$count 分前', + one: '1 分前', ); return '$_temp0'; } @@ -1777,14 +1788,14 @@ class AppLocalizationsJa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hours ago', - one: '1 hour ago', + other: '$count 時間前', + one: '1 時間前', ); return '$_temp0'; } @override - String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!'; + String get tutorialWelcomeTitle => 'SpotiFLAC へようこそ!'; @override String get tutorialWelcomeDesc => @@ -1795,7 +1806,8 @@ class AppLocalizationsJa extends AppLocalizations { 'Download music from Spotify, Deezer, or paste any supported URL'; @override - String get tutorialWelcomeTip2 => 'Tidal、Qobuz、Deezer から FLAC 品質のオーディオを取得'; + String get tutorialWelcomeTip2 => + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; @override String get tutorialWelcomeTip3 => @@ -1809,14 +1821,14 @@ class AppLocalizationsJa extends AppLocalizations { 'There are two easy ways to find music you want to download.'; @override - String get tutorialDownloadTitle => 'Downloading Music'; + String get tutorialDownloadTitle => '音楽をダウンロード中'; @override String get tutorialDownloadDesc => 'Downloading music is simple and fast. Here\'s how it works.'; @override - String get tutorialLibraryTitle => 'Your Library'; + String get tutorialLibraryTitle => 'あなたのライブラリ'; @override String get tutorialLibraryDesc => @@ -1835,7 +1847,7 @@ class AppLocalizationsJa extends AppLocalizations { 'Switch between list and grid view for better browsing'; @override - String get tutorialExtensionsTitle => 'Extensions'; + String get tutorialExtensionsTitle => '拡張'; @override String get tutorialExtensionsDesc => @@ -1876,7 +1888,7 @@ class AppLocalizationsJa extends AppLocalizations { 'You\'re all set! Start downloading your favorite music now.'; @override - String get libraryForceFullScan => 'Force Full Scan'; + String get libraryForceFullScan => '強制フルスキャン'; @override String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache'; @@ -1897,10 +1909,10 @@ class AppLocalizationsJa extends AppLocalizations { String get cleanupOrphanedDownloadsNone => 'No orphaned entries found'; @override - String get cacheTitle => 'Storage & Cache'; + String get cacheTitle => 'ストレージとキャッシュ'; @override - String get cacheSummaryTitle => 'Cache overview'; + String get cacheSummaryTitle => 'キャッシュの概要'; @override String get cacheSummarySubtitle => @@ -1912,34 +1924,34 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get cacheSectionStorage => 'Cached Data'; + String get cacheSectionStorage => 'キャッシュ済みデータ'; @override - String get cacheSectionMaintenance => 'Maintenance'; + String get cacheSectionMaintenance => 'メンテナンス'; @override - String get cacheAppDirectory => 'App cache directory'; + String get cacheAppDirectory => 'アプリキャッシュのディレクトリ'; @override String get cacheAppDirectoryDesc => 'HTTP responses, WebView data, and other temporary app data.'; @override - String get cacheTempDirectory => 'Temporary directory'; + String get cacheTempDirectory => '一時ディレクトリ'; @override String get cacheTempDirectoryDesc => 'Temporary files from downloads and audio conversion.'; @override - String get cacheCoverImage => 'Cover image cache'; + String get cacheCoverImage => 'カバー画像のキャッシュ'; @override String get cacheCoverImageDesc => 'Downloaded album and track cover art. Will re-download when viewed.'; @override - String get cacheLibraryCover => 'Library cover cache'; + String get cacheLibraryCover => 'ライブラリのカバーキャッシュ'; @override String get cacheLibraryCoverDesc => @@ -1964,7 +1976,7 @@ class AppLocalizationsJa extends AppLocalizations { 'Remove orphaned download history and library entries for missing files.'; @override - String get cacheNoData => 'No cached data'; + String get cacheNoData => 'キャッシュデータはありません'; @override String cacheSizeWithFiles(String size, int count) { @@ -1978,16 +1990,16 @@ class AppLocalizationsJa extends AppLocalizations { @override String cacheEntries(int count) { - return '$count entries'; + return '$count 個のエントリ'; } @override String cacheClearSuccess(String target) { - return 'Cleared: $target'; + return '消去済み: $target'; } @override - String get cacheClearConfirmTitle => 'Clear cache?'; + String get cacheClearConfirmTitle => 'キャッシュを消去しますか?'; @override String cacheClearConfirmMessage(String target) { @@ -1995,17 +2007,17 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get cacheClearAllConfirmTitle => 'Clear all cache?'; + String get cacheClearAllConfirmTitle => 'すべてのキャッシュを消去しますか?'; @override String get cacheClearAllConfirmMessage => 'This will clear all cache categories on this page. Downloaded music files will not be deleted.'; @override - String get cacheClearAll => 'Clear all cache'; + String get cacheClearAll => 'すべてのキャッシュを消去'; @override - String get cacheCleanupUnused => 'Cleanup unused data'; + String get cacheCleanupUnused => '未使用のデータを削除'; @override String get cacheCleanupUnusedSubtitle => @@ -2017,16 +2029,16 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get cacheRefreshStats => 'Refresh stats'; + String get cacheRefreshStats => '状態を更新'; @override - String get trackSaveCoverArt => 'Save Cover Art'; + String get trackSaveCoverArt => 'カバー画像を保存'; @override String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file'; @override - String get trackSaveLyrics => 'Save Lyrics (.lrc)'; + String get trackSaveLyrics => '歌詞を保存 (.lrc)'; @override String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; @@ -2042,7 +2054,7 @@ class AppLocalizationsJa extends AppLocalizations { 'Search metadata online and embed into file'; @override - String get trackEditMetadata => 'Edit Metadata'; + String get trackEditMetadata => 'メタデータを編集'; @override String trackCoverSaved(String fileName) { @@ -2071,26 +2083,26 @@ class AppLocalizationsJa extends AppLocalizations { @override String trackSaveFailed(String error) { - return 'Failed: $error'; + return '失敗: $error'; } @override - String get trackConvertFormat => 'Convert Format'; + String get trackConvertFormat => '変換の形式'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => 'MP3 または Opus に変換'; @override - String get trackConvertTitle => 'Convert Audio'; + String get trackConvertTitle => 'オーディオを変換'; @override - String get trackConvertTargetFormat => 'Target Format'; + String get trackConvertTargetFormat => 'ターゲットの形式'; @override - String get trackConvertBitrate => 'Bitrate'; + String get trackConvertBitrate => 'ビットレート'; @override - String get trackConvertConfirmTitle => 'Confirm Conversion'; + String get trackConvertConfirmTitle => '変換を確認'; @override String trackConvertConfirmMessage( @@ -2102,7 +2114,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get trackConvertConverting => 'Converting audio...'; + String get trackConvertConverting => 'オーディオを変換中...'; @override String trackConvertSuccess(String format) { @@ -2110,7 +2122,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get trackConvertFailed => 'Conversion failed'; + String get trackConvertFailed => '変換に失敗しました'; @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 3796e499..66c66e99 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -45,41 +45,40 @@ class AppLocalizationsKo extends AppLocalizations { String get historyFilterSingles => 'Singles'; @override - String get historySearchHint => 'Search history...'; + String get historySearchHint => '검색 기록...'; @override String get settingsTitle => 'Settings'; @override - String get settingsDownload => 'Download'; + String get settingsDownload => '다운로드'; @override - String get settingsAppearance => 'Appearance'; + String get settingsAppearance => '외관'; @override - String get settingsOptions => 'Options'; + String get settingsOptions => '옵션'; @override - String get settingsExtensions => 'Extensions'; + String get settingsExtensions => '확장 기능'; @override - String get settingsAbout => 'About'; + String get settingsAbout => '정보'; @override - String get downloadTitle => 'Download'; + String get downloadTitle => '다운로드'; @override - String get downloadAskQualitySubtitle => - 'Show quality picker for each download'; + String get downloadAskQualitySubtitle => '다운로드를 할 때마다 품질을 선택하도록 합니다'; @override - String get downloadFilenameFormat => 'Filename Format'; + String get downloadFilenameFormat => '파일 이름 형식'; @override - String get downloadFolderOrganization => 'Folder Organization'; + String get downloadFolderOrganization => '폴더 분류 형식'; @override - String get appearanceTitle => 'Appearance'; + String get appearanceTitle => '외관'; @override String get appearanceThemeSystem => 'System'; @@ -94,10 +93,10 @@ class AppLocalizationsKo extends AppLocalizations { String get appearanceDynamicColor => 'Dynamic Color'; @override - String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + String get appearanceDynamicColorSubtitle => '배경 화면을 참고하여 강조 색상이 지정됩니다'; @override - String get appearanceHistoryView => 'History View'; + String get appearanceHistoryView => '기록 정렬 방식'; @override String get appearanceHistoryViewList => 'List'; @@ -106,112 +105,104 @@ class AppLocalizationsKo extends AppLocalizations { String get appearanceHistoryViewGrid => 'Grid'; @override - String get optionsTitle => 'Options'; + String get optionsTitle => '옵션'; @override - String get optionsPrimaryProvider => 'Primary Provider'; + String get optionsPrimaryProvider => '기본 제공자'; @override - String get optionsPrimaryProviderSubtitle => - 'Service used when searching by track name.'; + String get optionsPrimaryProviderSubtitle => '음반 이름으로 검색할 때 사용되는 서비스'; @override String optionsUsingExtension(String extensionName) { - return 'Using extension: $extensionName'; + return '확장 기능을 사용: $extensionName'; } @override - String get optionsSwitchBack => - 'Tap Deezer or Spotify to switch back from extension'; + String get optionsSwitchBack => 'Deezer 또는 Spotify를 탭하여 확장 기능에서 다시 전환하세요.'; @override - String get optionsAutoFallback => 'Auto Fallback'; + String get optionsAutoFallback => '자동 재시도'; @override String get optionsAutoFallbackSubtitle => '다운로드가 실패한 경우, 다른 서비스로 재시도'; @override - String get optionsUseExtensionProviders => 'Use Extension Providers'; + String get optionsUseExtensionProviders => '확장 기능 사용'; @override String get optionsUseExtensionProvidersOn => '확장 기능을 우선적으로 사용합니다'; @override - String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + String get optionsUseExtensionProvidersOff => '기본으로 제공되는 기능만 사용'; @override - String get optionsEmbedLyrics => 'Embed Lyrics'; + String get optionsEmbedLyrics => '가사 삽입'; @override - String get optionsEmbedLyricsSubtitle => - 'Embed synced lyrics into FLAC files'; + String get optionsEmbedLyricsSubtitle => 'FLAC 파일에 동기화된 가사를 삽입합니다'; @override - String get optionsMaxQualityCover => 'Max Quality Cover'; + String get optionsMaxQualityCover => '고품질 커버 이미지'; @override - String get optionsMaxQualityCoverSubtitle => - 'Download highest resolution cover art'; + String get optionsMaxQualityCoverSubtitle => '최고 품질의 커버 이미지를 다운로드'; @override - String get optionsConcurrentDownloads => 'Concurrent Downloads'; + String get optionsConcurrentDownloads => '동시 다운로드'; @override - String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + String get optionsConcurrentSequential => '순차 다운로드 (한 번에 하나)'; @override String optionsConcurrentParallel(int count) { - return '$count parallel downloads'; + return '$count개 동시 다운로드'; } @override - String get optionsConcurrentWarning => - 'Parallel downloads may trigger rate limiting'; + String get optionsConcurrentWarning => '동시에 다수의 음반을 다운로드하면 속도 제한이 발생할 수 있습니다'; @override - String get optionsExtensionStore => 'Extension Store'; + String get optionsExtensionStore => '확장 기능 스토어'; @override - String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + String get optionsExtensionStoreSubtitle => '탐색 메뉴에 스토어 탭 표시'; @override - String get optionsCheckUpdates => 'Check for Updates'; + String get optionsCheckUpdates => '업데이트 확인'; @override - String get optionsCheckUpdatesSubtitle => - 'Notify when new version is available'; + String get optionsCheckUpdatesSubtitle => '새로운 버전이 출시되면 알림'; @override - String get optionsUpdateChannel => 'Update Channel'; + String get optionsUpdateChannel => '업데이트 채널'; @override - String get optionsUpdateChannelStable => 'Stable releases only'; + String get optionsUpdateChannelStable => '안정적인 버전만 수령'; @override - String get optionsUpdateChannelPreview => 'Get preview releases'; + String get optionsUpdateChannelPreview => '미리보기 버전을 수령'; @override - String get optionsUpdateChannelWarning => - 'Preview may contain bugs or incomplete features'; + String get optionsUpdateChannelWarning => '미리보기 버전은 불안정할 수 있습니다'; @override - String get optionsClearHistory => 'Clear Download History'; + String get optionsClearHistory => '다운로드 기록 삭제'; @override - String get optionsClearHistorySubtitle => - 'Remove all downloaded tracks from history'; + String get optionsClearHistorySubtitle => '기록에서 모든 다운로드 음반을 제거합니다'; @override - String get optionsDetailedLogging => 'Detailed Logging'; + String get optionsDetailedLogging => '상세 로깅'; @override - String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + String get optionsDetailedLoggingOn => '상세한 로그가 기록되고 있습니다'; @override - String get optionsDetailedLoggingOff => 'Enable for bug reports'; + String get optionsDetailedLoggingOff => '버그 신고를 위한 기능입니다'; @override - String get optionsSpotifyCredentials => 'Spotify Credentials'; + String get optionsSpotifyCredentials => 'Spotify 자격 증명'; @override String optionsSpotifyCredentialsConfigured(String clientId) { @@ -219,21 +210,21 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + String get optionsSpotifyCredentialsRequired => '탭하여 설정'; @override String get optionsSpotifyWarning => - 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + 'Spotify는 사용자 고유의 API 자격 증명을 요구합니다. developer.spotify.com에서 무료로 발급받으세요'; @override String get optionsSpotifyDeprecationWarning => - 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.'; + 'Spotify API 변경으로 인해 Spotify 검색 기능은 2026년 3월 3일부터 더 이상 지원되지 않습니다. Deezer로 전환해 주세요'; @override - String get extensionsTitle => 'Extensions'; + String get extensionsTitle => '확장 기능'; @override - String get extensionsDisabled => 'Disabled'; + String get extensionsDisabled => '비활성화'; @override String extensionsVersion(String version) { @@ -246,80 +237,79 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get extensionsUninstall => 'Uninstall'; + String get extensionsUninstall => '삭제'; @override - String get storeTitle => 'Extension Store'; + String get storeTitle => '확장 기능 스토어'; @override - String get storeSearch => 'Search extensions...'; + String get storeSearch => '확장 기능 검색'; @override - String get storeInstall => 'Install'; + String get storeInstall => '설치'; @override - String get storeInstalled => 'Installed'; + String get storeInstalled => '설치됨'; @override - String get storeUpdate => 'Update'; + String get storeUpdate => '업데이트'; @override - String get aboutTitle => 'About'; + String get aboutTitle => '정보'; @override - String get aboutContributors => 'Contributors'; + String get aboutContributors => '기여자'; @override - String get aboutMobileDeveloper => 'Mobile version developer'; + String get aboutMobileDeveloper => '모바일 버전 개발자'; @override - String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + String get aboutOriginalCreator => '오리지널 SpotiFLAC 제작자'; @override - String get aboutLogoArtist => - 'The talented artist who created our beautiful app logo!'; + String get aboutLogoArtist => '아름다운 로고를 만들어주신 재능 있는 아티스트!'; @override - String get aboutTranslators => 'Translators'; + String get aboutTranslators => '번역가들'; @override - String get aboutSpecialThanks => 'Special Thanks'; + String get aboutSpecialThanks => '특별 감사'; @override - String get aboutLinks => 'Links'; + String get aboutLinks => '바로가기'; @override String get aboutMobileSource => 'Mobile source code'; @override - String get aboutPCSource => 'PC source code'; + String get aboutPCSource => 'PC 소스 코드'; @override - String get aboutReportIssue => 'Report an issue'; + String get aboutReportIssue => '문제 신고'; @override - String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + String get aboutReportIssueSubtitle => '발생하는 모든 문제를 신고하여 주세요.'; @override - String get aboutFeatureRequest => 'Feature request'; + String get aboutFeatureRequest => '기능 요청'; @override - String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + String get aboutFeatureRequestSubtitle => '앱의 새로운 기능을 제안하여 주세요.'; @override String get aboutTelegramChannel => 'Telegram Channel'; @override - String get aboutTelegramChannelSubtitle => 'Announcements and updates'; + String get aboutTelegramChannelSubtitle => '공지 및 업데이트 안내'; @override String get aboutTelegramChat => 'Telegram Community'; @override - String get aboutTelegramChatSubtitle => 'Chat with other users'; + String get aboutTelegramChatSubtitle => '다른 이용자와 소통'; @override - String get aboutSocial => 'Social'; + String get aboutSocial => '소셜'; @override String get aboutApp => 'App'; @@ -329,244 +319,239 @@ class AppLocalizationsKo extends AppLocalizations { @override String get aboutBinimumDesc => - 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + 'QQDL 및 HiFi API 개발자입니다. 이 API가 없었다면 Tidal 다운로드는 불가능했을 것입니다!'; @override - String get aboutSachinsenalDesc => - 'The original HiFi project creator. The foundation of Tidal integration!'; + String get aboutSachinsenalDesc => '최초의 하이파이 프로젝트 창시자. 타이달 연동의 기반을 마련한 사람!'; @override String get aboutSjdonadoDesc => - 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!'; + 'I Don\'t Have Spotify(IDHS) 개발자입니다. 위급 상황 발생 시 해결해 주는 대체 링크 해결 도구를 만들었습니다!'; @override String get aboutDabMusic => 'DAB Music'; @override String get aboutDabMusicDesc => - 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + '최고의 Qobuz 스트리밍 API입니다. 이 API가 없었다면 고해상도 다운로드는 불가능했을 겁니다!'; @override String get aboutSpotiSaver => 'SpotiSaver'; @override String get aboutSpotiSaverDesc => - 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!'; + 'Tidal Hi-Res FLAC 스트리밍 엔드포인트. 무손실 음원 재생의 핵심 요소!'; @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Tidal, Qobuz, Amazon Music에서 Spotify 트랙을 무손실 음질로 다운로드하세요.'; @override - String get artistAlbums => 'Albums'; + String get artistAlbums => '앨범'; @override - String get artistSingles => 'Singles & EPs'; + String get artistSingles => '싱글 및 EP'; @override - String get artistCompilations => 'Compilations'; + String get artistCompilations => '편집'; @override - String get artistPopular => 'Popular'; + String get artistPopular => '인기순'; @override String artistMonthlyListeners(String count) { - return '$count monthly listeners'; + return '월간 청취자: $count'; } @override - String get trackMetadataService => 'Service'; + String get trackMetadataService => '제공업체'; @override - String get trackMetadataPlay => 'Play'; + String get trackMetadataPlay => '재생'; @override - String get trackMetadataShare => 'Share'; + String get trackMetadataShare => '공유'; @override - String get trackMetadataDelete => 'Delete'; + String get trackMetadataDelete => '삭제'; @override - String get setupGrantPermission => 'Grant Permission'; + String get setupGrantPermission => '권한을 제공해 주세요.'; @override - String get setupSkip => 'Skip for now'; + String get setupSkip => '다음에 할래요'; @override - String get setupStorageAccessRequired => 'Storage Access Required'; + String get setupStorageAccessRequired => '스토리지 접근 권한 필요'; @override String get setupStorageAccessMessageAndroid11 => - 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + 'Android 11 이상 버전에서는 선택한 다운로드 폴더에 파일을 저장하려면 \"모든 파일 접근\" 권한이 필요합니다.'; @override - String get setupOpenSettings => 'Open Settings'; + String get setupOpenSettings => '설정으로 이동'; @override String get setupPermissionDeniedMessage => - 'Permission denied. Please grant all permissions to continue.'; + '권한이 거부되었습니다. 계속하려면 모든 권한을 허용해 주세요.'; @override String setupPermissionRequired(String permissionType) { - return '$permissionType Permission Required'; + return '$permissionType 권한 필요'; } @override String setupPermissionRequiredMessage(String permissionType) { - return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + return '최상의 사용 경험을 위해 $permissionType 권한이 필요합니다. 설정에서 나중에 변경할 수 있습니다.'; } @override - String get setupUseDefaultFolder => 'Use Default Folder?'; + String get setupUseDefaultFolder => '기본 폴더를 사용하시겠습니까?'; @override - String get setupNoFolderSelected => - 'No folder selected. Would you like to use the default Music folder?'; + String get setupNoFolderSelected => '선택된 폴더가 없습니다. 기본 음악 폴더를 사용하시겠습니까?'; @override - String get setupUseDefault => 'Use Default'; + String get setupUseDefault => '기본값 사용'; @override - String get setupDownloadLocationTitle => 'Download Location'; + String get setupDownloadLocationTitle => '다운로드 경로'; @override String get setupDownloadLocationIosMessage => - 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + 'iOS에서는 다운로드한 파일이 앱의 문서 폴더에 저장됩니다. 파일 앱을 통해 해당 파일에 접근할 수 있습니다.'; @override - String get setupAppDocumentsFolder => 'App Documents Folder'; + String get setupAppDocumentsFolder => '앱 문서 폴더'; @override - String get setupAppDocumentsFolderSubtitle => - 'Recommended - accessible via Files app'; + String get setupAppDocumentsFolderSubtitle => '권장 사항 - 파일 앱을 통해 접근 가능'; @override - String get setupChooseFromFiles => 'Choose from Files'; + String get setupChooseFromFiles => '파일 탐색기에서 선택'; @override - String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + String get setupChooseFromFilesSubtitle => 'iCloud 또는 다른 위치를 선택하세요'; @override String get setupIosEmptyFolderWarning => - 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + 'iOS 제한 사항: 빈 폴더는 선택할 수 없습니다. 파일이 하나 이상 있는 폴더를 선택하세요.'; @override String get setupIcloudNotSupported => - 'iCloud Drive is not supported. Please use the app Documents folder.'; + 'iCloud Drive는 지원되지 않습니다. 앱의 문서 폴더를 사용해 주세요.'; @override - String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + String get setupDownloadInFlac => 'Spotify 음악을 FLAC 형식으로 다운로드하세요.'; @override - String get setupStorageGranted => 'Storage Permission Granted!'; + String get setupStorageGranted => '저장소 접근 권한이 부여되었습니다!'; @override - String get setupStorageRequired => 'Storage Permission Required'; + String get setupStorageRequired => '저장소 접근 권한이 필요합니다.'; @override String get setupStorageDescription => - 'SpotiFLAC needs storage permission to save your downloaded music files.'; + 'SpotiFLAC은 다운로드한 음악 파일을 저장하기 위해 저장소 접근 권한이 필요합니다.'; @override - String get setupNotificationGranted => 'Notification Permission Granted!'; + String get setupNotificationGranted => '알림 권한이 부여되었습니다!'; @override - String get setupNotificationEnable => 'Enable Notifications'; + String get setupNotificationEnable => '알림 활성화'; @override - String get setupFolderChoose => 'Choose Download Folder'; + String get setupFolderChoose => '다운로드 폴더를 선택하세요'; @override - String get setupFolderDescription => - 'Select a folder where your downloaded music will be saved.'; + String get setupFolderDescription => '다운로드한 음악 파일이 저장될 폴더를 선택하세요.'; @override - String get setupSelectFolder => 'Select Folder'; + String get setupSelectFolder => '폴더 선택'; @override - String get setupEnableNotifications => 'Enable Notifications'; + String get setupEnableNotifications => '알림 활성화'; @override String get setupNotificationBackgroundDescription => - 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + '알림으로 다운로드 진행 상황을 확인하세요. 앱이 백그라운드에서 실행 중일 때 다운로드 상태와 완료 여부를 확인할 수 있습니다.'; @override - String get setupSkipForNow => 'Skip for now'; + String get setupSkipForNow => '다음에 할래요.'; @override - String get setupNext => 'Next'; + String get setupNext => '다음'; @override - String get setupGetStarted => 'Get Started'; + String get setupGetStarted => '시작하기'; @override String get setupAllowAccessToManageFiles => - 'Please enable \"Allow access to manage all files\" in the next screen.'; + '다음 화면에서 \"모든 파일 관리 권한 허용\"을 활성화해 주세요.'; @override - String get dialogCancel => 'Cancel'; + String get dialogCancel => '취소'; @override - String get dialogSave => 'Save'; + String get dialogSave => '저장'; @override - String get dialogDelete => 'Delete'; + String get dialogDelete => '삭제'; @override - String get dialogRetry => 'Retry'; + String get dialogRetry => '재시도'; @override - String get dialogClear => 'Clear'; + String get dialogClear => '지우기'; @override - String get dialogDone => 'Done'; + String get dialogDone => '완료'; @override - String get dialogImport => 'Import'; + String get dialogImport => '불러오기'; @override - String get dialogDiscard => 'Discard'; + String get dialogDiscard => '취소'; @override - String get dialogRemove => 'Remove'; + String get dialogRemove => '제거'; @override - String get dialogUninstall => 'Uninstall'; + String get dialogUninstall => '삭제'; @override - String get dialogDiscardChanges => 'Discard Changes?'; + String get dialogDiscardChanges => '변경사항 취소'; @override - String get dialogUnsavedChanges => - 'You have unsaved changes. Do you want to discard them?'; + String get dialogUnsavedChanges => '저장되지 않은 변경 사항이 있습니다. 삭제하시겠습니까?'; @override - String get dialogClearAll => 'Clear All'; + String get dialogClearAll => '모두 제거:'; @override - String get dialogRemoveExtension => 'Remove Extension'; + String get dialogRemoveExtension => '확장 프로그램 제거'; @override String get dialogRemoveExtensionMessage => - 'Are you sure you want to remove this extension? This cannot be undone.'; + '이 확장 프로그램을 정말로 제거하시겠습니까? 이 작업은 되돌릴 수 없습니다.'; @override - String get dialogUninstallExtension => 'Uninstall Extension?'; + String get dialogUninstallExtension => '확장 프로그램을 제거하시겠습니까?'; @override String dialogUninstallExtensionMessage(String extensionName) { - return 'Are you sure you want to remove $extensionName?'; + return '$extensionName을 정말로 삭제하시겠습니까?'; } @override - String get dialogClearHistoryTitle => 'Clear History'; + String get dialogClearHistoryTitle => '기록 삭제'; @override String get dialogClearHistoryMessage => - 'Are you sure you want to clear all download history? This cannot be undone.'; + '다운로드 기록을 모두 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.'; @override - String get dialogDeleteSelectedTitle => 'Delete Selected'; + String get dialogDeleteSelectedTitle => '선택한 항목 삭제'; @override String dialogDeleteSelectedMessage(int count) { @@ -576,50 +561,50 @@ class AppLocalizationsKo extends AppLocalizations { other: 'tracks', one: 'track', ); - return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + return '기록에서 $count $_temp0를 삭제하시겠습니까?'; } @override - String get dialogImportPlaylistTitle => 'Import Playlist'; + String get dialogImportPlaylistTitle => '재생 목록 가져오기'; @override String dialogImportPlaylistMessage(int count) { - return 'Found $count tracks in CSV. Add them to download queue?'; + return 'CSV 파일에서 $count개의 트랙을 찾았습니다. 다운로드 대기열에 추가하시겠습니까?'; } @override String csvImportTracks(int count) { - return '$count tracks from CSV'; + return 'CSV 파일의 트랙: $count'; } @override String snackbarAddedToQueue(String trackName) { - return 'Added \"$trackName\" to queue'; + return '\"$trackName\"(을)를 대기열에 추가했습니다.'; } @override String snackbarAddedTracksToQueue(int count) { - return 'Added $count tracks to queue'; + return '대기열에 $count개의 트랙을 추가했습니다.'; } @override String snackbarAlreadyDownloaded(String trackName) { - return '\"$trackName\" already downloaded'; + return '\"$trackName\"(은)는 이미 다운로드되었습니다.'; } @override String snackbarAlreadyInLibrary(String trackName) { - return '\"$trackName\" already exists in your library'; + return '라이브러리에 \"$trackName\"(은)는 이미 존재합니다.'; } @override - String get snackbarHistoryCleared => 'History cleared'; + String get snackbarHistoryCleared => '기록 삭제됨'; @override - String get snackbarCredentialsSaved => 'Credentials saved'; + String get snackbarCredentialsSaved => '자격 증명이 저장되었습니다.'; @override - String get snackbarCredentialsCleared => 'Credentials cleared'; + String get snackbarCredentialsCleared => '자격 증명이 제거되었습니다.'; @override String snackbarDeletedTracks(int count) { @@ -629,136 +614,145 @@ class AppLocalizationsKo extends AppLocalizations { other: 'tracks', one: 'track', ); - return 'Deleted $count $_temp0'; + return '$count$_temp0 제거됨'; } @override String snackbarCannotOpenFile(String error) { - return 'Cannot open file: $error'; + return '파일을 열 수 없습니다: $error'; } @override - String get snackbarFillAllFields => 'Please fill all fields'; + String get snackbarFillAllFields => '모든 항목을 입력해 주세요.'; @override String get snackbarViewQueue => 'View Queue'; @override String snackbarUrlCopied(String platform) { - return '$platform URL copied to clipboard'; + return '$platform 링크가 클립보드에 저장됨'; } @override - String get snackbarFileNotFound => 'File not found'; + String get snackbarFileNotFound => '파일을 찾을 수 없음'; @override - String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + String get snackbarSelectExtFile => '.spotiflac-ext 확장자 파일을 선택'; @override - String get snackbarProviderPrioritySaved => 'Provider priority saved'; + String get snackbarProviderPrioritySaved => '제공자 우선순위 저장됨'; @override - String get snackbarMetadataProviderSaved => - 'Metadata provider priority saved'; + String get snackbarMetadataProviderSaved => '메타데이터 제공자 우선순위 저장됨'; @override String snackbarExtensionInstalled(String extensionName) { - return '$extensionName installed.'; + return '$extensionName(이)가 설치됨'; } @override String snackbarExtensionUpdated(String extensionName) { - return '$extensionName updated.'; + return '$extensionName(이)가 설치됨.'; } @override - String get snackbarFailedToInstall => 'Failed to install extension'; + String get snackbarFailedToInstall => '확장 프로그램 설치 실패'; @override - String get snackbarFailedToUpdate => 'Failed to update extension'; + String get snackbarFailedToUpdate => '확장 프로그램 업데이트 실패'; @override String get errorRateLimited => 'Rate Limited'; @override - String get errorRateLimitedMessage => - 'Too many requests. Please wait a moment before searching again.'; + String get errorRateLimitedMessage => '요청이 너무 많습니다. 잠시 후 다시 검색해 주세요.'; @override - String get errorNoTracksFound => 'No tracks found'; + String get errorNoTracksFound => '트랙을 찾을 수 없습니다'; + + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; @override String errorMissingExtensionSource(String item) { - return 'Cannot load $item: missing extension source'; + return '확장 소스가 누락되어, $item(을)를 로드할 수 없습니다'; } @override - String get actionPause => 'Pause'; + String get actionPause => '멈추기'; @override - String get actionResume => 'Resume'; + String get actionResume => '재시작'; @override - String get actionCancel => 'Cancel'; + String get actionCancel => '취소'; @override - String get actionSelectAll => 'Select All'; + String get actionSelectAll => '모두 선택'; @override - String get actionDeselect => 'Deselect'; + String get actionDeselect => '선택 해제'; @override - String get actionRemoveCredentials => 'Remove Credentials'; + String get actionRemoveCredentials => '자격 증명 제거'; @override - String get actionSaveCredentials => 'Save Credentials'; + String get actionSaveCredentials => '자격 증명 저장'; @override String selectionSelected(int count) { - return '$count selected'; + return '$count개 선택됨'; } @override - String get selectionAllSelected => 'All tracks selected'; + String get selectionAllSelected => '모든 트랙 선택됨'; @override - String get selectionSelectToDelete => 'Select tracks to delete'; + String get selectionSelectToDelete => '삭제할 트랙을 선택'; @override String progressFetchingMetadata(int current, int total) { - return 'Fetching metadata... $current/$total'; + return '메타데이터 가져오는 중... $current/$total'; } @override - String get progressReadingCsv => 'Reading CSV...'; + String get progressReadingCsv => 'CSV 파일을 읽는 중...'; @override - String get searchSongs => 'Songs'; + String get searchSongs => '곡들'; @override - String get searchArtists => 'Artists'; + String get searchArtists => '아티스트들'; @override - String get searchAlbums => 'Albums'; + String get searchAlbums => '앨범들'; @override - String get searchPlaylists => 'Playlists'; + String get searchPlaylists => '재생목록들'; @override - String get tooltipPlay => 'Play'; + String get tooltipPlay => '재생'; @override - String get filenameFormat => 'Filename Format'; + String get filenameFormat => ''; @override - String get filenameShowAdvancedTags => 'Show advanced tags'; + String get filenameShowAdvancedTags => '고급 태그 표시'; @override String get filenameShowAdvancedTagsDescription => - 'Enable formatted tags for track padding and date patterns'; + '트랙 패딩 및 날짜 패턴에 대한 서식 있는 태그를 활성화합니다.'; @override - String get folderOrganizationNone => 'No organization'; + String get folderOrganizationNone => '정리하지 않음'; @override String get folderOrganizationByArtist => 'By Artist'; @@ -877,116 +871,114 @@ class AppLocalizationsKo extends AppLocalizations { String get logShareLogs => 'Share logs'; @override - String get logClearLogs => 'Clear logs'; + String get logClearLogs => '로그 제거'; @override - String get logClearLogsTitle => 'Clear Logs'; + String get logClearLogsTitle => '로그 제거'; @override - String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + String get logClearLogsMessage => '모든 로그를 삭제하시겠습니까?'; @override - String get logFilterBySeverity => 'Filter logs by severity'; + String get logFilterBySeverity => '심각성에 따라 로그 분류'; @override - String get logNoLogsYet => 'No logs yet'; + String get logNoLogsYet => '어떠한 로그도 없음'; @override - String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + String get logNoLogsYetSubtitle => '앱을 사용하는 동안 로그가 여기에 표시됩니다.'; @override String logEntriesFiltered(int count) { - return 'Entries ($count filtered)'; + return '($count filtered)개 항목 필터링'; } @override String logEntries(int count) { - return 'Entries ($count)'; + return '항목 수: ($count)'; } @override - String get credentialsTitle => 'Spotify Credentials'; + String get credentialsTitle => 'Spotify 자격 증명'; @override String get credentialsDescription => - 'Enter your Client ID and Secret to use your own Spotify application quota.'; + 'Spotify 애플리케이션 할당량을 사용하려면 클라이언트 ID와 비밀키를 입력하세요.'; @override String get credentialsClientId => 'Client ID'; @override - String get credentialsClientIdHint => 'Paste Client ID'; + String get credentialsClientIdHint => 'Client ID를 붙여넣으세요'; @override - String get credentialsClientSecret => 'Client Secret'; + String get credentialsClientSecret => '비밀키'; @override - String get credentialsClientSecretHint => 'Paste Client Secret'; + String get credentialsClientSecretHint => '비밀키를 붙여넣으세요'; @override - String get channelStable => 'Stable'; + String get channelStable => '안정'; @override - String get channelPreview => 'Preview'; + String get channelPreview => '베타'; @override - String get sectionSearchSource => 'Search Source'; + String get sectionSearchSource => '검색 소스'; @override - String get sectionDownload => 'Download'; + String get sectionDownload => '다운로드'; @override - String get sectionPerformance => 'Performance'; + String get sectionPerformance => '성능'; @override - String get sectionApp => 'App'; + String get sectionApp => '앱'; @override - String get sectionData => 'Data'; + String get sectionData => '데이터'; @override String get sectionDebug => 'Debug'; @override - String get sectionService => 'Service'; + String get sectionService => '서비스'; @override - String get sectionAudioQuality => 'Audio Quality'; + String get sectionAudioQuality => '오디오 품질'; @override - String get sectionFileSettings => 'File Settings'; + String get sectionFileSettings => '파일 설정'; @override - String get sectionLyrics => 'Lyrics'; + String get sectionLyrics => '가사'; @override - String get lyricsMode => 'Lyrics Mode'; + String get lyricsMode => '가사 설정'; @override - String get lyricsModeDescription => - 'Choose how lyrics are saved with your downloads'; + String get lyricsModeDescription => '다운로드한 파일에 가사를 저장하는 방법을 선택하세요.'; @override - String get lyricsModeEmbed => 'Embed in file'; + String get lyricsModeEmbed => '파일에 포함'; @override - String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + String get lyricsModeEmbedSubtitle => 'FLAC 메타데이터 내에 저장됩니다.'; @override - String get lyricsModeExternal => 'External .lrc file'; + String get lyricsModeExternal => '외부 .lrc 파일'; @override - String get lyricsModeExternalSubtitle => - 'Separate .lrc file for players like Samsung Music'; + String get lyricsModeExternalSubtitle => '삼성 뮤직과 같은 플레이어용 별도 .lrc 파일'; @override - String get lyricsModeBoth => 'Both'; + String get lyricsModeBoth => '둘 다'; @override - String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + String get lyricsModeBothSubtitle => '.lrc 파일을 삽입하고 저장합니다.'; @override - String get sectionColor => 'Color'; + String get sectionColor => '색상'; @override String get sectionTheme => 'Theme'; @@ -1807,7 +1799,8 @@ class AppLocalizationsKo extends AppLocalizations { 'Download music from Spotify, Deezer, or paste any supported URL'; @override - String get tutorialWelcomeTip2 => 'Tidal, Qobuz 또는 Deezer에서 FLAC 품질 오디오 받기'; + String get tutorialWelcomeTip2 => + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; @override String get tutorialWelcomeTip3 => diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 81ecfd12..6144f9b6 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -356,7 +356,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; @override String get artistAlbums => 'Albums'; @@ -688,6 +688,17 @@ class AppLocalizationsNl extends AppLocalizations { @override String get errorNoTracksFound => 'No tracks found'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return 'Cannot load $item: missing extension source'; @@ -1809,7 +1820,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get tutorialWelcomeTip2 => - 'Krijg FLAC-kwaliteit audio van Tidal, Qobuz of Deezer'; + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; @override String get tutorialWelcomeTip3 => diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 24b813f3..d5c8d821 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -688,6 +688,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String get errorNoTracksFound => 'No tracks found'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return 'Cannot load $item: missing extension source'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 77913294..909b5842 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -67,7 +67,7 @@ class AppLocalizationsRu extends AppLocalizations { String get settingsAbout => 'О программе'; @override - String get downloadTitle => 'Скачивание'; + String get downloadTitle => 'Скачать'; @override String get downloadAskQualitySubtitle => @@ -146,11 +146,11 @@ class AppLocalizationsRu extends AppLocalizations { 'Использование только встроенных провайдеров'; @override - String get optionsEmbedLyrics => 'Вставить текст песни'; + String get optionsEmbedLyrics => 'Вписать текст песни'; @override String get optionsEmbedLyricsSubtitle => - 'Вставить синхронизированные тексты в FLAC файлы'; + 'Вписать синхронизированные тексты во FLAC файлы'; @override String get optionsMaxQualityCover => 'Максимальное качество обложки'; @@ -337,7 +337,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get aboutBinimumDesc => - 'Создатель QQDL & HiFi API. Без этого API загрузки Tidal не существовали бы!'; + 'Создатель QQDL & HiFi API. Без него API загрузки Tidal не существовали бы!'; @override String get aboutSachinsenalDesc => @@ -363,7 +363,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get aboutAppDescription => - 'Скачайте треки Spotify в Lossless качестве из Tidal и Qobuz.'; + 'Скачайте треки Spotify в Lossless качестве из Tidal, Qobuz и Amazon Music.'; @override String get artistAlbums => 'Альбомы'; @@ -601,7 +601,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String csvImportTracks(int count) { - return '$count треков из CSV'; + return '$count трек(-ов) из CSV'; } @override @@ -702,6 +702,17 @@ class AppLocalizationsRu extends AppLocalizations { @override String get errorNoTracksFound => 'Треки не найдены'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return 'Невозможно загрузить $item: отсутствует источник расширения'; @@ -766,11 +777,11 @@ class AppLocalizationsRu extends AppLocalizations { String get filenameFormat => 'Формат имени файла'; @override - String get filenameShowAdvancedTags => 'Show advanced tags'; + String get filenameShowAdvancedTags => 'Показать расширенные теги'; @override String get filenameShowAdvancedTagsDescription => - 'Enable formatted tags for track padding and date patterns'; + 'Включить форматированные теги для отслеживания заполнения и шаблонов дат'; @override String get folderOrganizationNone => 'Без организации'; @@ -983,7 +994,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Выберите как сохранить тексты песен при скачивании'; @override - String get lyricsModeEmbed => 'Вставить в файл'; + String get lyricsModeEmbed => 'Вписать в файл'; @override String get lyricsModeEmbedSubtitle => 'Встроить текст в метаданные FLAC'; @@ -999,7 +1010,7 @@ class AppLocalizationsRu extends AppLocalizations { String get lyricsModeBoth => 'Оба варианта'; @override - String get lyricsModeBothSubtitle => 'Вставить и сохранить файл .lrc'; + String get lyricsModeBothSubtitle => 'Вписать и сохранить .lrc файл'; @override String get sectionColor => 'Цвет'; @@ -1138,7 +1149,7 @@ class AppLocalizationsRu extends AppLocalizations { String get trackLyricsLoadFailed => 'Не удалось загрузить текст песни'; @override - String get trackEmbedLyrics => 'Вставить текст песни'; + String get trackEmbedLyrics => 'Вписать текст песни'; @override String get trackLyricsEmbedded => 'Текст успешно добавлен'; @@ -1361,10 +1372,10 @@ class AppLocalizationsRu extends AppLocalizations { 'YouTube обеспечивает только звук с потерями(Lossy).'; @override - String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate'; + String get youtubeOpusBitrateTitle => 'Битрейт YouTube Opus'; @override - String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate'; + String get youtubeMp3BitrateTitle => 'Битрейт YouTube MP3'; @override String get downloadAskBeforeDownload => 'Спрашивать перед скачиванием'; @@ -1383,7 +1394,8 @@ class AppLocalizationsRu extends AppLocalizations { 'Использовать исполнителя альбома для папок'; @override - String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + String get downloadUsePrimaryArtistOnly => + 'Основной исполнитель только для папок'; @override String get downloadUsePrimaryArtistOnlyEnabled => @@ -1391,7 +1403,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadUsePrimaryArtistOnlyDisabled => - 'Full artist string used for folder name'; + 'Полная строка исполнителя, используемая для имени папки'; @override String get downloadSelectQuality => 'Выбор качества'; @@ -1423,7 +1435,7 @@ class AppLocalizationsRu extends AppLocalizations { String get settingsDownloadNetwork => 'Сеть для скачивания'; @override - String get settingsDownloadNetworkAny => 'WiFi и мобильная сеть'; + String get settingsDownloadNetworkAny => 'WiFi и Мобильная сеть'; @override String get settingsDownloadNetworkWifiOnly => 'Только WiFi'; @@ -1712,8 +1724,10 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'треков', + many: 'треков', + few: 'трека', + one: 'трек', ); return '$_temp0'; } @@ -1800,7 +1814,7 @@ class AppLocalizationsRu extends AppLocalizations { String get libraryFilterQualityCD => 'CD (16 бит)'; @override - String get libraryFilterQualityLossy => 'С потерями'; + String get libraryFilterQualityLossy => 'Lossy'; @override String get libraryFilterFormat => 'Формат'; @@ -1855,7 +1869,8 @@ class AppLocalizationsRu extends AppLocalizations { 'Скачивайте музыку из Spotify, Deezer, или вставьте любой поддерживаемый URL'; @override - String get tutorialWelcomeTip2 => 'Скачайте FLAC с Tidal, Qobuz или Deezer'; + String get tutorialWelcomeTip2 => + 'Скачайте FLAC с Tidal, Qobuz или Amazon Music'; @override String get tutorialWelcomeTip3 => @@ -1903,7 +1918,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get tutorialExtensionsTip1 => - 'Browse the Store tab to discover useful extensions'; + 'Просмотрите вкладку Магазина, чтобы найти полезные расширения'; @override String get tutorialExtensionsTip2 => @@ -1911,14 +1926,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get tutorialExtensionsTip3 => - 'Get lyrics, enhanced metadata, and more features'; + 'Получайте тексты песен, улучшенные метаданные и другие возможности'; @override String get tutorialSettingsTitle => 'Настройте приложение под себя'; @override String get tutorialSettingsDesc => - 'Personalize the app in Settings to match your preferences.'; + 'Персонализируйте приложение в Настройках, чтобы оно соответствовало вашим предпочтениям.'; @override String get tutorialSettingsTip1 => @@ -1943,11 +1958,11 @@ class AppLocalizationsRu extends AppLocalizations { 'Пересканировать все файлы, игнорировать кэш'; @override - String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads'; + String get cleanupOrphanedDownloads => 'Очистка отложенных скачиваний'; @override String get cleanupOrphanedDownloadsSubtitle => - 'Remove history entries for files that no longer exist'; + 'Удалить историю записи для файлов, которых больше не существует'; @override String cleanupOrphanedDownloadsResult(int count) { @@ -1955,7 +1970,7 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get cleanupOrphanedDownloadsNone => 'No orphaned entries found'; + String get cleanupOrphanedDownloadsNone => 'Записей без описания не найдено'; @override String get cacheTitle => 'Хранилище и кэш'; @@ -1965,11 +1980,11 @@ class AppLocalizationsRu extends AppLocalizations { @override String get cacheSummarySubtitle => - 'Clearing cache will not remove downloaded music files.'; + 'Очистка кэша не приведет к удалению загруженных музыкальных файлов.'; @override String cacheEstimatedTotal(String size) { - return 'Estimated cache usage: $size'; + return 'Приблизительное использование кэша: $size'; } @override @@ -1983,42 +1998,42 @@ class AppLocalizationsRu extends AppLocalizations { @override String get cacheAppDirectoryDesc => - 'HTTP responses, WebView data, and other temporary app data.'; + 'HTTP-ответы, данные WebView и другие временные данные приложения.'; @override - String get cacheTempDirectory => 'Temporary directory'; + String get cacheTempDirectory => 'Временная директория'; @override String get cacheTempDirectoryDesc => - 'Temporary files from downloads and audio conversion.'; + 'Временные файлы из загрузок и аудио конвертации.'; @override - String get cacheCoverImage => 'Cover image cache'; + String get cacheCoverImage => 'Кэш обложек'; @override String get cacheCoverImageDesc => - 'Downloaded album and track cover art. Will re-download when viewed.'; + 'Скачанный альбом и трек обложки. Будет заново скачан после просмотра.'; @override - String get cacheLibraryCover => 'Library cover cache'; + String get cacheLibraryCover => 'Кэш обложек библиотеки'; @override String get cacheLibraryCoverDesc => - 'Cover art extracted from local music files. Will re-extract on next scan.'; + 'Обложка извлечена из локальных музыкальных файлов. Будет повторно извлечено при следующем сканировании.'; @override - String get cacheExploreFeed => 'Explore feed cache'; + String get cacheExploreFeed => 'Просмотреть кэш ленты'; @override String get cacheExploreFeedDesc => - 'Explore tab content (new releases, trending). Will refresh on next visit.'; + 'Изучите содержимое вкладки (новые релизы, тренды). Они обновятся при следующем посещении.'; @override - String get cacheTrackLookup => 'Track lookup cache'; + String get cacheTrackLookup => 'Отслеживать кэш поиска'; @override String get cacheTrackLookupDesc => - 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.'; + 'Поиск ID трека в Spotify/Deezer. Очистка может замедлить следующие несколько поисков.'; @override String get cacheCleanupUnusedDesc => @@ -2039,7 +2054,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String cacheEntries(int count) { - return '$count entries'; + return '$count записей'; } @override @@ -2052,7 +2067,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String cacheClearConfirmMessage(String target) { - return 'This will clear cached data for $target. Downloaded music files will not be deleted.'; + return 'Это очистит кэш для $target. Загруженные музыкальные файлы не будут удалены.'; } @override @@ -2074,7 +2089,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String cacheCleanupResult(int downloadCount, int libraryCount) { - return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries'; + return 'Очистка завершена: $downloadCount потерянных загрузок, $libraryCount отсутствующих записей в библиотеке'; } @override @@ -2094,14 +2109,14 @@ class AppLocalizationsRu extends AppLocalizations { 'Получить и сохранить текст песни в формате .lrc'; @override - String get trackSaveLyricsProgress => 'Saving lyrics...'; + String get trackSaveLyricsProgress => 'Сохранение текста...'; @override - String get trackReEnrich => 'Re-enrich'; + String get trackReEnrich => 'Обновить'; @override String get trackReEnrichOnlineSubtitle => - 'Search metadata online and embed into file'; + 'Поиск в сети метаданных и встраивание в файл'; @override String get trackEditMetadata => 'Редактировать метаданные'; @@ -2120,13 +2135,13 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get trackReEnrichProgress => 'Re-enriching metadata...'; + String get trackReEnrichProgress => 'Обновление метаданных...'; @override String get trackReEnrichSearching => 'Поиск метаданных в сети...'; @override - String get trackReEnrichSuccess => 'Metadata re-enriched successfully'; + String get trackReEnrichSuccess => 'Метаданные успешно обновлены'; @override String get trackReEnrichFfmpegFailed => @@ -2138,22 +2153,22 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get trackConvertFormat => 'Convert Format'; + String get trackConvertFormat => 'Переконвертировать формат'; @override - String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + String get trackConvertFormatSubtitle => 'Конвертировать в MP3 или Opus'; @override - String get trackConvertTitle => 'Convert Audio'; + String get trackConvertTitle => 'Конвертировать аудио'; @override - String get trackConvertTargetFormat => 'Target Format'; + String get trackConvertTargetFormat => 'Целевой формат'; @override - String get trackConvertBitrate => 'Bitrate'; + String get trackConvertBitrate => 'Битрейт'; @override - String get trackConvertConfirmTitle => 'Confirm Conversion'; + String get trackConvertConfirmTitle => 'Подтвердить конвертацию'; @override String trackConvertConfirmMessage( @@ -2161,177 +2176,181 @@ class AppLocalizationsRu extends AppLocalizations { String targetFormat, String bitrate, ) { - return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + return 'Конвертировать из $sourceFormat в $targetFormat $bitrate?\n\nОригинальный файл будет удален после конвертации.'; } @override - String get trackConvertConverting => 'Converting audio...'; + String get trackConvertConverting => 'Конвертация аудио...'; @override String trackConvertSuccess(String format) { - return 'Converted to $format successfully'; + return 'Успешно конвертировано в $format'; } @override - String get trackConvertFailed => 'Conversion failed'; + String get trackConvertFailed => 'Ошибка конвертации'; @override - String get actionCreate => 'Create'; + String get actionCreate => 'Создать'; @override - String get collectionFoldersTitle => 'My folders'; + String get collectionFoldersTitle => 'Мои папки'; @override - String get collectionWishlist => 'Wishlist'; + String get collectionWishlist => 'Список желаемого'; @override - String get collectionLoved => 'Loved'; + String get collectionLoved => 'Любимые'; @override - String get collectionPlaylists => 'Playlists'; + String get collectionPlaylists => 'Плейлисты'; @override - String get collectionPlaylist => 'Playlist'; + String get collectionPlaylist => 'Плейлист'; @override - String get collectionAddToPlaylist => 'Add to playlist'; + String get collectionAddToPlaylist => 'Добавить в плейлист'; @override - String get collectionCreatePlaylist => 'Create playlist'; + String get collectionCreatePlaylist => 'Создать плейлист'; @override - String get collectionNoPlaylistsYet => 'No playlists yet'; + String get collectionNoPlaylistsYet => 'Плейлисты отсутствуют'; @override String get collectionNoPlaylistsSubtitle => - 'Create a playlist to start categorizing tracks'; + 'Создайте плейлист, чтобы начать классифицировать треки'; @override String collectionPlaylistTracks(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count tracks', - one: '1 track', + other: '$count треков', + many: '$count треков', + few: '$count трека', + one: '$count трек', ); return '$_temp0'; } @override String collectionAddedToPlaylist(String playlistName) { - return 'Added to \"$playlistName\"'; + return 'Добавлено в \"$playlistName\"'; } @override String collectionAlreadyInPlaylist(String playlistName) { - return 'Already in \"$playlistName\"'; + return 'Уже в \"$playlistName\"'; } @override - String get collectionPlaylistCreated => 'Playlist created'; + String get collectionPlaylistCreated => 'Плейлист создан'; @override - String get collectionPlaylistNameHint => 'Playlist name'; + String get collectionPlaylistNameHint => 'Название плейлиста'; @override - String get collectionPlaylistNameRequired => 'Playlist name is required'; + String get collectionPlaylistNameRequired => 'Имя плейлиста обязательно'; @override - String get collectionRenamePlaylist => 'Rename playlist'; + String get collectionRenamePlaylist => 'Переименовать плейлист'; @override - String get collectionDeletePlaylist => 'Delete playlist'; + String get collectionDeletePlaylist => 'Удалить плейлист'; @override String collectionDeletePlaylistMessage(String playlistName) { - return 'Delete \"$playlistName\" and all tracks inside it?'; + return 'Удалить \"$playlistName\" и все треки внутри него?'; } @override - String get collectionPlaylistDeleted => 'Playlist deleted'; + String get collectionPlaylistDeleted => 'Плейлист удалён'; @override - String get collectionPlaylistRenamed => 'Playlist renamed'; + String get collectionPlaylistRenamed => 'Плейлист переименован'; @override - String get collectionWishlistEmptyTitle => 'Wishlist is empty'; + String get collectionWishlistEmptyTitle => 'Список желаний пуст'; @override String get collectionWishlistEmptySubtitle => - 'Tap + on tracks to save what you want to download later'; + 'Нажмите + на треках, чтобы сохранить то, что вы хотите скачать позже'; @override - String get collectionLovedEmptyTitle => 'Loved folder is empty'; + String get collectionLovedEmptyTitle => 'Папка Любимые пуста'; @override String get collectionLovedEmptySubtitle => - 'Tap love on tracks to keep your favorites'; + 'Нажмите \"любовь\" на треках, чтобы сохранить ваши избранные'; @override - String get collectionPlaylistEmptyTitle => 'Playlist is empty'; + String get collectionPlaylistEmptyTitle => 'Плейлист пуст'; @override String get collectionPlaylistEmptySubtitle => - 'Long-press + on any track to add it here'; + 'Удерживайте + на любом треке, чтобы добавить его сюда'; @override - String get collectionRemoveFromPlaylist => 'Remove from playlist'; + String get collectionRemoveFromPlaylist => 'Удалить из плейлиста'; @override - String get collectionRemoveFromFolder => 'Remove from folder'; + String get collectionRemoveFromFolder => 'Убрать из папки'; @override String collectionRemoved(String trackName) { - return '\"$trackName\" removed'; + return '\"$trackName\" удалён'; } @override String collectionAddedToLoved(String trackName) { - return '\"$trackName\" added to Loved'; + return '\"$trackName\" добавлен в Любимые'; } @override String collectionRemovedFromLoved(String trackName) { - return '\"$trackName\" removed from Loved'; + return '\"$trackName\" удалено из Любимых'; } @override String collectionAddedToWishlist(String trackName) { - return '\"$trackName\" added to Wishlist'; + return '\"$trackName\" добавлен в список желаний'; } @override String collectionRemovedFromWishlist(String trackName) { - return '\"$trackName\" removed from Wishlist'; + return '\"$trackName\" удалён из списка желаний'; } @override - String get trackOptionAddToLoved => 'Add to Loved'; + String get trackOptionAddToLoved => 'Добавить в Любимое'; @override - String get trackOptionRemoveFromLoved => 'Remove from Loved'; + String get trackOptionRemoveFromLoved => 'Исключить из Любимых'; @override - String get trackOptionAddToWishlist => 'Add to Wishlist'; + String get trackOptionAddToWishlist => 'Добавить в список желаний'; @override - String get trackOptionRemoveFromWishlist => 'Remove from Wishlist'; + String get trackOptionRemoveFromWishlist => 'Удалить из списка желаний'; @override - String get collectionPlaylistChangeCover => 'Change cover image'; + String get collectionPlaylistChangeCover => 'Изменить обложку'; @override - String get collectionPlaylistRemoveCover => 'Remove cover image'; + String get collectionPlaylistRemoveCover => 'Удалить обложку'; @override String selectionShareCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'треков', + many: 'треков', + few: 'трека', + one: 'трек', ); - return 'Share $count $_temp0'; + return 'Отправить $count $_temp0'; } @override @@ -2342,17 +2361,19 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'треков', + many: 'треков', + few: 'трека', + one: 'трек', ); - return 'Convert $count $_temp0'; + return 'Конвертировать $count $_temp0'; } @override - String get selectionConvertNoConvertible => 'No convertible tracks selected'; + String get selectionConvertNoConvertible => 'Не выбраны конвертируемые треки'; @override - String get selectionBatchConvertConfirmTitle => 'Batch Convert'; + String get selectionBatchConvertConfirmTitle => 'Пакетная конвертация'; @override String selectionBatchConvertConfirmMessage( @@ -2371,12 +2392,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String selectionBatchConvertProgress(int current, int total) { - return 'Converting $current of $total...'; + return 'Конвертация $current из $total...'; } @override String selectionBatchConvertSuccess(int success, int total, String format) { - return 'Converted $success of $total tracks to $format'; + return 'Конвертировано $success треков $total в $format'; } @override @@ -2386,7 +2407,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersAlbumSubtitle => - 'Artist folders use Album Artist when available'; + 'Для папок исполнителей используется исполнитель альбома, если он указан'; @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 53034492..b608cff7 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -693,6 +693,17 @@ class AppLocalizationsTr extends AppLocalizations { @override String get errorNoTracksFound => 'Parça bulunamadı'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return '$item yüklenemedi: Eksik eklenti kaynağı'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index a4fc7e77..4104845c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -688,6 +688,17 @@ class AppLocalizationsZh extends AppLocalizations { @override String get errorNoTracksFound => 'No tracks found'; + @override + String get errorUrlNotRecognized => 'Link not recognized'; + + @override + String get errorUrlNotRecognizedMessage => + 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'; + + @override + String get errorUrlFetchFailed => + 'Failed to load content from this link. Please try again.'; + @override String errorMissingExtensionSource(String item) { return 'Cannot load $item: missing extension source'; @@ -2696,7 +2707,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; @override String get artistAlbums => 'Albums'; @@ -3091,6 +3102,13 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get filenameFormat => 'Filename Format'; + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganizationNone => 'No organization'; @@ -3668,6 +3686,12 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { String get youtubeQualityNote => 'YouTube provides lossy audio only. Not part of lossless fallback.'; + @override + String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate'; + + @override + String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate'; + @override String get downloadAskBeforeDownload => 'Ask Before Download'; @@ -3999,6 +4023,17 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { String get libraryAboutDescription => 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.'; + @override + String libraryTracksUnit(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return '$_temp0'; + } + @override String libraryLastScanned(String time) { return 'Last scanned: $time'; @@ -4124,7 +4159,8 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { 'Download music from Spotify, Deezer, or paste any supported URL'; @override - String get tutorialWelcomeTip2 => '从 Tidal、Qobuz 或 Deezer 获取 FLAC 品质音频'; + String get tutorialWelcomeTip2 => + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; @override String get tutorialWelcomeTip3 => @@ -4441,6 +4477,210 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get actionCreate => 'Create'; + + @override + String get collectionFoldersTitle => 'My folders'; + + @override + String get collectionWishlist => 'Wishlist'; + + @override + String get collectionLoved => 'Loved'; + + @override + String get collectionPlaylists => 'Playlists'; + + @override + String get collectionPlaylist => 'Playlist'; + + @override + String get collectionAddToPlaylist => 'Add to playlist'; + + @override + String get collectionCreatePlaylist => 'Create playlist'; + + @override + String get collectionNoPlaylistsYet => 'No playlists yet'; + + @override + String get collectionNoPlaylistsSubtitle => + 'Create a playlist to start categorizing tracks'; + + @override + String collectionPlaylistTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String collectionAddedToPlaylist(String playlistName) { + return 'Added to \"$playlistName\"'; + } + + @override + String collectionAlreadyInPlaylist(String playlistName) { + return 'Already in \"$playlistName\"'; + } + + @override + String get collectionPlaylistCreated => 'Playlist created'; + + @override + String get collectionPlaylistNameHint => 'Playlist name'; + + @override + String get collectionPlaylistNameRequired => 'Playlist name is required'; + + @override + String get collectionRenamePlaylist => 'Rename playlist'; + + @override + String get collectionDeletePlaylist => 'Delete playlist'; + + @override + String collectionDeletePlaylistMessage(String playlistName) { + return 'Delete \"$playlistName\" and all tracks inside it?'; + } + + @override + String get collectionPlaylistDeleted => 'Playlist deleted'; + + @override + String get collectionPlaylistRenamed => 'Playlist renamed'; + + @override + String get collectionWishlistEmptyTitle => 'Wishlist is empty'; + + @override + String get collectionWishlistEmptySubtitle => + 'Tap + on tracks to save what you want to download later'; + + @override + String get collectionLovedEmptyTitle => 'Loved folder is empty'; + + @override + String get collectionLovedEmptySubtitle => + 'Tap love on tracks to keep your favorites'; + + @override + String get collectionPlaylistEmptyTitle => 'Playlist is empty'; + + @override + String get collectionPlaylistEmptySubtitle => + 'Long-press + on any track to add it here'; + + @override + String get collectionRemoveFromPlaylist => 'Remove from playlist'; + + @override + String get collectionRemoveFromFolder => 'Remove from folder'; + + @override + String collectionRemoved(String trackName) { + return '\"$trackName\" removed'; + } + + @override + String collectionAddedToLoved(String trackName) { + return '\"$trackName\" added to Loved'; + } + + @override + String collectionRemovedFromLoved(String trackName) { + return '\"$trackName\" removed from Loved'; + } + + @override + String collectionAddedToWishlist(String trackName) { + return '\"$trackName\" added to Wishlist'; + } + + @override + String collectionRemovedFromWishlist(String trackName) { + return '\"$trackName\" removed from Wishlist'; + } + + @override + String get trackOptionAddToLoved => 'Add to Loved'; + + @override + String get trackOptionRemoveFromLoved => 'Remove from Loved'; + + @override + String get trackOptionAddToWishlist => 'Add to Wishlist'; + + @override + String get trackOptionRemoveFromWishlist => 'Remove from Wishlist'; + + @override + String get collectionPlaylistChangeCover => 'Change cover image'; + + @override + String get collectionPlaylistRemoveCover => 'Remove cover image'; + + @override + String selectionShareCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Share $count $_temp0'; + } + + @override + String get selectionShareNoFiles => 'No shareable files found'; + + @override + String selectionConvertCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0'; + } + + @override + String get selectionConvertNoConvertible => 'No convertible tracks selected'; + + @override + String get selectionBatchConvertConfirmTitle => 'Batch Convert'; + + @override + String selectionBatchConvertConfirmMessage( + int count, + String format, + String bitrate, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; + } + + @override + String selectionBatchConvertProgress(int current, int total) { + return 'Converting $current of $total...'; + } + + @override + String selectionBatchConvertSuccess(int success, int total, String format) { + return 'Converted $success of $total tracks to $format'; + } + @override String downloadedAlbumDownloadedCount(int count) { return '$count downloaded'; @@ -4807,7 +5047,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; @override String get artistAlbums => 'Albums'; @@ -5202,6 +5442,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get filenameFormat => 'Filename Format'; + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganizationNone => 'No organization'; @@ -5779,6 +6026,12 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { String get youtubeQualityNote => 'YouTube provides lossy audio only. Not part of lossless fallback.'; + @override + String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate'; + + @override + String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate'; + @override String get downloadAskBeforeDownload => 'Ask Before Download'; @@ -6110,6 +6363,17 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { String get libraryAboutDescription => 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.'; + @override + String libraryTracksUnit(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return '$_temp0'; + } + @override String libraryLastScanned(String time) { return 'Last scanned: $time'; @@ -6235,7 +6499,8 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { 'Download music from Spotify, Deezer, or paste any supported URL'; @override - String get tutorialWelcomeTip2 => '從 Tidal、Qobuz 或 Deezer 取得 FLAC 品質音訊'; + String get tutorialWelcomeTip2 => + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; @override String get tutorialWelcomeTip3 => @@ -6552,6 +6817,210 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get actionCreate => 'Create'; + + @override + String get collectionFoldersTitle => 'My folders'; + + @override + String get collectionWishlist => 'Wishlist'; + + @override + String get collectionLoved => 'Loved'; + + @override + String get collectionPlaylists => 'Playlists'; + + @override + String get collectionPlaylist => 'Playlist'; + + @override + String get collectionAddToPlaylist => 'Add to playlist'; + + @override + String get collectionCreatePlaylist => 'Create playlist'; + + @override + String get collectionNoPlaylistsYet => 'No playlists yet'; + + @override + String get collectionNoPlaylistsSubtitle => + 'Create a playlist to start categorizing tracks'; + + @override + String collectionPlaylistTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String collectionAddedToPlaylist(String playlistName) { + return 'Added to \"$playlistName\"'; + } + + @override + String collectionAlreadyInPlaylist(String playlistName) { + return 'Already in \"$playlistName\"'; + } + + @override + String get collectionPlaylistCreated => 'Playlist created'; + + @override + String get collectionPlaylistNameHint => 'Playlist name'; + + @override + String get collectionPlaylistNameRequired => 'Playlist name is required'; + + @override + String get collectionRenamePlaylist => 'Rename playlist'; + + @override + String get collectionDeletePlaylist => 'Delete playlist'; + + @override + String collectionDeletePlaylistMessage(String playlistName) { + return 'Delete \"$playlistName\" and all tracks inside it?'; + } + + @override + String get collectionPlaylistDeleted => 'Playlist deleted'; + + @override + String get collectionPlaylistRenamed => 'Playlist renamed'; + + @override + String get collectionWishlistEmptyTitle => 'Wishlist is empty'; + + @override + String get collectionWishlistEmptySubtitle => + 'Tap + on tracks to save what you want to download later'; + + @override + String get collectionLovedEmptyTitle => 'Loved folder is empty'; + + @override + String get collectionLovedEmptySubtitle => + 'Tap love on tracks to keep your favorites'; + + @override + String get collectionPlaylistEmptyTitle => 'Playlist is empty'; + + @override + String get collectionPlaylistEmptySubtitle => + 'Long-press + on any track to add it here'; + + @override + String get collectionRemoveFromPlaylist => 'Remove from playlist'; + + @override + String get collectionRemoveFromFolder => 'Remove from folder'; + + @override + String collectionRemoved(String trackName) { + return '\"$trackName\" removed'; + } + + @override + String collectionAddedToLoved(String trackName) { + return '\"$trackName\" added to Loved'; + } + + @override + String collectionRemovedFromLoved(String trackName) { + return '\"$trackName\" removed from Loved'; + } + + @override + String collectionAddedToWishlist(String trackName) { + return '\"$trackName\" added to Wishlist'; + } + + @override + String collectionRemovedFromWishlist(String trackName) { + return '\"$trackName\" removed from Wishlist'; + } + + @override + String get trackOptionAddToLoved => 'Add to Loved'; + + @override + String get trackOptionRemoveFromLoved => 'Remove from Loved'; + + @override + String get trackOptionAddToWishlist => 'Add to Wishlist'; + + @override + String get trackOptionRemoveFromWishlist => 'Remove from Wishlist'; + + @override + String get collectionPlaylistChangeCover => 'Change cover image'; + + @override + String get collectionPlaylistRemoveCover => 'Remove cover image'; + + @override + String selectionShareCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Share $count $_temp0'; + } + + @override + String get selectionShareNoFiles => 'No shareable files found'; + + @override + String selectionConvertCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0'; + } + + @override + String get selectionConvertNoConvertible => 'No convertible tracks selected'; + + @override + String get selectionBatchConvertConfirmTitle => 'Batch Convert'; + + @override + String selectionBatchConvertConfirmMessage( + int count, + String format, + String bitrate, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.'; + } + + @override + String selectionBatchConvertProgress(int current, int total) { + return 'Converting $current of $total...'; + } + + @override + String selectionBatchConvertSuccess(int success, int total, String format) { + return 'Converted $success of $total tracks to $format'; + } + @override String downloadedAlbumDownloadedCount(int count) { return '$count downloaded'; diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 139b620e..e32aa56e 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -728,7 +728,7 @@ "@dialogDeleteSelectedTitle": { "description": "Dialog title - delete selected items" }, - "dialogDeleteSelectedMessage": "Lösche {count} {count, plural, one {}=1{Track} other{Tracks}} aus dem Verlauf?\n\nDies löscht auch die Dateien aus dem Speicher.", + "dialogDeleteSelectedMessage": "Lösche {count} {count, plural, one {Track} other{Tracks}} aus dem Verlauf?\n\nDies löscht auch die Dateien aus dem Speicher.", "@dialogDeleteSelectedMessage": { "description": "Dialog message - delete selected tracks", "placeholders": { @@ -807,7 +807,7 @@ "@snackbarCredentialsCleared": { "description": "Snackbar - Spotify credentials removed" }, - "snackbarDeletedTracks": "{count} {count, plural, one {}=1{Titel} other{Titel}}", + "snackbarDeletedTracks": "{count} {count, plural, one {Titel} other{Titel}}", "@snackbarDeletedTracks": { "description": "Snackbar - tracks deleted", "placeholders": { @@ -1889,7 +1889,7 @@ "@downloadedAlbumDeleteSelected": { "description": "Button - delete selected tracks" }, - "downloadedAlbumDeleteMessage": "{count} {count, plural, one {}=1{Titel} other{Titel}} aus diesem Album löschen?\n\nDadurch werden auch die Dateien aus dem Speicher gelöscht.", + "downloadedAlbumDeleteMessage": "{count} {count, plural, one {Titel} other{Titel}} aus diesem Album löschen?\n\nDadurch werden auch die Dateien aus dem Speicher gelöscht.", "@downloadedAlbumDeleteMessage": { "description": "Delete confirmation with count", "placeholders": { @@ -1915,7 +1915,7 @@ "@downloadedAlbumTapToSelect": { "description": "Selection hint" }, - "downloadedAlbumDeleteCount": "Lösche {count} {count, plural, one {}=1{Titel}other{Titel}}", + "downloadedAlbumDeleteCount": "Lösche {count} {count, plural, one {Titel} other{Titel}}", "@downloadedAlbumDeleteCount": { "description": "Delete button text with count", "placeholders": { @@ -2353,7 +2353,7 @@ "@timeJustNow": { "description": "Relative time - less than a minute ago" }, - "timeMinutesAgo": "{count, plural, one {}=1{vor 1 Minute} other{vor {count} Minuten}}", + "timeMinutesAgo": "{count, plural, one {vor {count} Minute} other{vor {count} Minuten}}", "@timeMinutesAgo": { "description": "Relative time - minutes ago", "placeholders": { @@ -2362,7 +2362,7 @@ } } }, - "timeHoursAgo": "{count, plural, one {}=1{vor 1 Stunde} other{vor {count} Stunden}}", + "timeHoursAgo": "{count, plural, one {vor {count} Stunde} other{vor {count} Stunden}}", "@timeHoursAgo": { "description": "Relative time - hours ago", "placeholders": { @@ -3013,7 +3013,7 @@ "@collectionPlaylistRemoveCover": { "description": "Bottom sheet action to remove custom cover image from a playlist" }, - "selectionShareCount": "Teile {count} {count, plural, one {}=1{Titel}other{Titel}}", + "selectionShareCount": "Teile {count} {count, plural, one {Titel} other{Titel}}", "@selectionShareCount": { "description": "Share button text with count in selection mode", "placeholders": { @@ -3026,7 +3026,7 @@ "@selectionShareNoFiles": { "description": "Snackbar when no selected files exist on disk" }, - "selectionConvertCount": "Konvertiere {count} {count, plural, one {}=1{Titel}other{Titel}}", + "selectionConvertCount": "Konvertiere {count} {count, plural, one {Titel} other{Titel}}", "@selectionConvertCount": { "description": "Convert button text with count in selection mode", "placeholders": { @@ -3043,7 +3043,7 @@ "@selectionBatchConvertConfirmTitle": { "description": "Confirmation dialog title for batch conversion" }, - "selectionBatchConvertConfirmMessage": "Konvertiere {count} {format} {count, plural, one {}=1{Titel} other{Titel}} zu {bitrate}?\n\nOriginaldateien werden nach der Konvertierung gelöscht.", + "selectionBatchConvertConfirmMessage": "Konvertiere {count} {format} {count, plural, one {Titel} other{Titel}} zu {bitrate}?\n\nOriginaldateien werden nach der Konvertierung gelöscht.", "@selectionBatchConvertConfirmMessage": { "description": "Confirmation dialog message for batch conversion", "placeholders": { diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7eee4649..bb8bac99 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -897,6 +897,18 @@ "@errorNoTracksFound": { "description": "Error - search returned no results" }, + "errorUrlNotRecognized": "Link not recognized", + "@errorUrlNotRecognized": { + "description": "Error title - URL not handled by any extension or service" + }, + "errorUrlNotRecognizedMessage": "This link is not supported. Make sure the URL is correct and a compatible extension is installed.", + "@errorUrlNotRecognizedMessage": { + "description": "Error message - URL not recognized explanation" + }, + "errorUrlFetchFailed": "Failed to load content from this link. Please try again.", + "@errorUrlFetchFailed": { + "description": "Error message - generic URL fetch failure" + }, "errorMissingExtensionSource": "Cannot load {item}: missing extension source", "@errorMissingExtensionSource": { "description": "Error - extension source not available", diff --git a/lib/l10n/arb/app_es-ES.arb b/lib/l10n/arb/app_es-ES.arb deleted file mode 100644 index c48b16f4..00000000 --- a/lib/l10n/arb/app_es-ES.arb +++ /dev/null @@ -1,3105 +0,0 @@ -{ - "@@locale": "es-ES", - "@@last_modified": "2026-01-16", - "appName": "SpotiFLAC", - "@appName": { - "description": "App name - DO NOT TRANSLATE" - }, - "navHome": "Inicio", - "@navHome": { - "description": "Bottom navigation - Home tab" - }, - "navLibrary": "Biblioteca", - "@navLibrary": { - "description": "Bottom navigation - Library tab" - }, - "navSettings": "Ajustes", - "@navSettings": { - "description": "Bottom navigation - Settings tab" - }, - "navStore": "Tienda", - "@navStore": { - "description": "Bottom navigation - Extension store tab" - }, - "homeTitle": "Inicio", - "@homeTitle": { - "description": "Home screen title" - }, - "homeSubtitle": "Pegar enlace de Spotify o buscar por nombre", - "@homeSubtitle": { - "description": "Subtitle shown below search box" - }, - "homeSupports": "Soportes: Pista, Álbum, Lista de reproducción, URLs de Artistas", - "@homeSupports": { - "description": "Info text about supported URL types" - }, - "homeRecent": "Recientes", - "@homeRecent": { - "description": "Section header for recent searches" - }, - "historyFilterAll": "Todo", - "@historyFilterAll": { - "description": "Filter chip - show all items" - }, - "historyFilterAlbums": "Álbumes", - "@historyFilterAlbums": { - "description": "Filter chip - show albums only" - }, - "historyFilterSingles": "Pistas", - "@historyFilterSingles": { - "description": "Filter chip - show singles only" - }, - "historySearchHint": "Buscar en historial...", - "@historySearchHint": { - "description": "Search bar placeholder in history" - }, - "settingsTitle": "Ajustes", - "@settingsTitle": { - "description": "Settings screen title" - }, - "settingsDownload": "Descargar", - "@settingsDownload": { - "description": "Settings section - download options" - }, - "settingsAppearance": "Apariencia", - "@settingsAppearance": { - "description": "Settings section - visual customization" - }, - "settingsOptions": "Opciones", - "@settingsOptions": { - "description": "Settings section - app options" - }, - "settingsExtensions": "Extensiones", - "@settingsExtensions": { - "description": "Settings section - extension management" - }, - "settingsAbout": "Acerca de", - "@settingsAbout": { - "description": "Settings section - app info" - }, - "downloadTitle": "Descargar", - "@downloadTitle": { - "description": "Download settings page title" - }, - "downloadAskQualitySubtitle": "Mostrar selector de calidad para cada descarga", - "@downloadAskQualitySubtitle": { - "description": "Subtitle for ask quality toggle" - }, - "downloadFilenameFormat": "Formato del nombre del archivo", - "@downloadFilenameFormat": { - "description": "Setting for output filename pattern" - }, - "downloadFolderOrganization": "Organización de carpetas", - "@downloadFolderOrganization": { - "description": "Setting for folder structure" - }, - "appearanceTitle": "Apariencia", - "@appearanceTitle": { - "description": "Appearance settings page title" - }, - "appearanceThemeSystem": "Sistema", - "@appearanceThemeSystem": { - "description": "Follow system theme" - }, - "appearanceThemeLight": "Claro", - "@appearanceThemeLight": { - "description": "Light theme" - }, - "appearanceThemeDark": "Oscuro", - "@appearanceThemeDark": { - "description": "Dark theme" - }, - "appearanceDynamicColor": "Color dinámico", - "@appearanceDynamicColor": { - "description": "Material You dynamic colors" - }, - "appearanceDynamicColorSubtitle": "Usar colores de tu fondo de pantalla", - "@appearanceDynamicColorSubtitle": { - "description": "Subtitle for dynamic color" - }, - "appearanceHistoryView": "Vista de Historial", - "@appearanceHistoryView": { - "description": "Layout style for history" - }, - "appearanceHistoryViewList": "Lista", - "@appearanceHistoryViewList": { - "description": "List layout option" - }, - "appearanceHistoryViewGrid": "Cuadrícula", - "@appearanceHistoryViewGrid": { - "description": "Grid layout option" - }, - "optionsTitle": "Opciones", - "@optionsTitle": { - "description": "Options settings page title" - }, - "optionsPrimaryProvider": "Proveedor Principal", - "@optionsPrimaryProvider": { - "description": "Main search provider setting" - }, - "optionsPrimaryProviderSubtitle": "Servicio usado al buscar por nombre de la pista.", - "@optionsPrimaryProviderSubtitle": { - "description": "Subtitle for primary provider" - }, - "optionsUsingExtension": "Usando la extensión: {extensionName}", - "@optionsUsingExtension": { - "description": "Shows active extension name", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "optionsSwitchBack": "Toque Deezer o Spotify para volver desde la extensión", - "@optionsSwitchBack": { - "description": "Hint to switch back to built-in providers" - }, - "optionsAutoFallback": "Alternativa automática", - "@optionsAutoFallback": { - "description": "Auto-retry with other services" - }, - "optionsAutoFallbackSubtitle": "Pruebe otros servicios si falla la descarga", - "@optionsAutoFallbackSubtitle": { - "description": "Subtitle for auto fallback" - }, - "optionsUseExtensionProviders": "Usar proveedores de extensiones", - "@optionsUseExtensionProviders": { - "description": "Enable extension download providers" - }, - "optionsUseExtensionProvidersOn": "Las extensiones serán probadas primero", - "@optionsUseExtensionProvidersOn": { - "description": "Status when extension providers enabled" - }, - "optionsUseExtensionProvidersOff": "Utilizando sólo proveedores integrados", - "@optionsUseExtensionProvidersOff": { - "description": "Status when extension providers disabled" - }, - "optionsEmbedLyrics": "Incrustar Letras", - "@optionsEmbedLyrics": { - "description": "Embed lyrics in audio files" - }, - "optionsEmbedLyricsSubtitle": "Insertar letras sincronizadas en archivos FLAC", - "@optionsEmbedLyricsSubtitle": { - "description": "Subtitle for embed lyrics" - }, - "optionsMaxQualityCover": "Carátula de calidad máxima", - "@optionsMaxQualityCover": { - "description": "Download highest quality album art" - }, - "optionsMaxQualityCoverSubtitle": "Descargar carátula de resolución máxima", - "@optionsMaxQualityCoverSubtitle": { - "description": "Subtitle for max quality cover" - }, - "optionsConcurrentDownloads": "Descargas Simultáneas", - "@optionsConcurrentDownloads": { - "description": "Number of parallel downloads" - }, - "optionsConcurrentSequential": "Secuencial (1 a la vez)", - "@optionsConcurrentSequential": { - "description": "Download one at a time" - }, - "optionsConcurrentParallel": "{count} descargas paralelas", - "@optionsConcurrentParallel": { - "description": "Multiple parallel downloads", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "optionsConcurrentWarning": "Las descargas paralelas pueden activar la limitación de velocidad", - "@optionsConcurrentWarning": { - "description": "Warning about rate limits" - }, - "optionsExtensionStore": "Tienda de extensiones", - "@optionsExtensionStore": { - "description": "Show/hide store tab" - }, - "optionsExtensionStoreSubtitle": "Mostrar pestaña de tienda en la navegación", - "@optionsExtensionStoreSubtitle": { - "description": "Subtitle for extension store toggle" - }, - "optionsCheckUpdates": "Comprobar actualizaciones", - "@optionsCheckUpdates": { - "description": "Auto update check toggle" - }, - "optionsCheckUpdatesSubtitle": "Notificar cuando una nueva versión esté disponible", - "@optionsCheckUpdatesSubtitle": { - "description": "Subtitle for update check" - }, - "optionsUpdateChannel": "Tipo de actualizaciones", - "@optionsUpdateChannel": { - "description": "Stable vs preview releases" - }, - "optionsUpdateChannelStable": "Sólo versiones estables", - "@optionsUpdateChannelStable": { - "description": "Only stable updates" - }, - "optionsUpdateChannelPreview": "Versión preliminar", - "@optionsUpdateChannelPreview": { - "description": "Include beta/preview updates" - }, - "optionsUpdateChannelWarning": "La Versión preliminar puede contener errores o características incompletas", - "@optionsUpdateChannelWarning": { - "description": "Warning about preview channel" - }, - "optionsClearHistory": "Borrar el historial de descargas", - "@optionsClearHistory": { - "description": "Delete all download history" - }, - "optionsClearHistorySubtitle": "Eliminar todas las pistas descargadas del historial", - "@optionsClearHistorySubtitle": { - "description": "Subtitle for clear history" - }, - "optionsDetailedLogging": "Registro detallado", - "@optionsDetailedLogging": { - "description": "Enable verbose logs for debugging" - }, - "optionsDetailedLoggingOn": "Registros detallados están siendo registrados", - "@optionsDetailedLoggingOn": { - "description": "Status when logging enabled" - }, - "optionsDetailedLoggingOff": "Habilitar para informes de errores", - "@optionsDetailedLoggingOff": { - "description": "Status when logging disabled" - }, - "optionsSpotifyCredentials": "Credenciales de Spotify", - "@optionsSpotifyCredentials": { - "description": "Spotify API credentials setting" - }, - "optionsSpotifyCredentialsConfigured": "ID de cliente: {clientId}...", - "@optionsSpotifyCredentialsConfigured": { - "description": "Shows configured client ID preview", - "placeholders": { - "clientId": { - "type": "String" - } - } - }, - "optionsSpotifyCredentialsRequired": "Requerido - toque para configurar", - "@optionsSpotifyCredentialsRequired": { - "description": "Prompt to set up credentials" - }, - "optionsSpotifyWarning": "Spotify requiere tus propias credenciales API. Obténgalas gratis de developer.spotify.com", - "@optionsSpotifyWarning": { - "description": "Info about Spotify API requirement" - }, - "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", - "@optionsSpotifyDeprecationWarning": { - "description": "Warning about Spotify API deprecation" - }, - "extensionsTitle": "Extensiones", - "@extensionsTitle": { - "description": "Extensions page title" - }, - "extensionsDisabled": "Deshabilitado", - "@extensionsDisabled": { - "description": "Extension status - inactive" - }, - "extensionsVersion": "Versión {version}", - "@extensionsVersion": { - "description": "Extension version display", - "placeholders": { - "version": { - "type": "String" - } - } - }, - "extensionsAuthor": "por {author}", - "@extensionsAuthor": { - "description": "Extension author credit", - "placeholders": { - "author": { - "type": "String" - } - } - }, - "extensionsUninstall": "Desinstalar", - "@extensionsUninstall": { - "description": "Uninstall extension button" - }, - "storeTitle": "Tienda de extensiones", - "@storeTitle": { - "description": "Store screen title" - }, - "storeSearch": "Buscar extensiones...", - "@storeSearch": { - "description": "Store search placeholder" - }, - "storeInstall": "Instalar", - "@storeInstall": { - "description": "Install extension button" - }, - "storeInstalled": "Instalada", - "@storeInstalled": { - "description": "Already installed badge" - }, - "storeUpdate": "Actualizar", - "@storeUpdate": { - "description": "Update available button" - }, - "aboutTitle": "Acerca de", - "@aboutTitle": { - "description": "About page title" - }, - "aboutContributors": "Colaboradores", - "@aboutContributors": { - "description": "Section for contributors" - }, - "aboutMobileDeveloper": "Desarrollador de versiones móviles", - "@aboutMobileDeveloper": { - "description": "Role description for mobile dev" - }, - "aboutOriginalCreator": "Creador original de SpotiFLAC", - "@aboutOriginalCreator": { - "description": "Role description for original creator" - }, - "aboutLogoArtist": "¡El talentoso artista que creó nuestro hermoso logo!", - "@aboutLogoArtist": { - "description": "Role description for logo artist" - }, - "aboutTranslators": "Traductores", - "@aboutTranslators": { - "description": "Section for translators" - }, - "aboutSpecialThanks": "Agradecimientos especiales", - "@aboutSpecialThanks": { - "description": "Section for special thanks" - }, - "aboutLinks": "Enlaces", - "@aboutLinks": { - "description": "Section for external links" - }, - "aboutMobileSource": "Código fuente móvil", - "@aboutMobileSource": { - "description": "Link to mobile GitHub repo" - }, - "aboutPCSource": "Código fuente de PC", - "@aboutPCSource": { - "description": "Link to PC GitHub repo" - }, - "aboutReportIssue": "Reportar un problema", - "@aboutReportIssue": { - "description": "Link to report bugs" - }, - "aboutReportIssueSubtitle": "Reporta cualquier problema que encuentres", - "@aboutReportIssueSubtitle": { - "description": "Subtitle for report issue" - }, - "aboutFeatureRequest": "Sugerir una función", - "@aboutFeatureRequest": { - "description": "Link to suggest features" - }, - "aboutFeatureRequestSubtitle": "Sugerir nuevas funciones para la aplicación", - "@aboutFeatureRequestSubtitle": { - "description": "Subtitle for feature request" - }, - "aboutTelegramChannel": "Canal de Telegram", - "@aboutTelegramChannel": { - "description": "Link to Telegram channel" - }, - "aboutTelegramChannelSubtitle": "Anuncios y actualizaciones", - "@aboutTelegramChannelSubtitle": { - "description": "Subtitle for Telegram channel" - }, - "aboutTelegramChat": "Comunidad de Telegram", - "@aboutTelegramChat": { - "description": "Link to Telegram chat group" - }, - "aboutTelegramChatSubtitle": "Chatear con otros usuarios", - "@aboutTelegramChatSubtitle": { - "description": "Subtitle for Telegram chat" - }, - "aboutSocial": "Redes sociales", - "@aboutSocial": { - "description": "Section for social links" - }, - "aboutApp": "Aplicación", - "@aboutApp": { - "description": "Section for app info" - }, - "aboutVersion": "Versión", - "@aboutVersion": { - "description": "Version info label" - }, - "aboutBinimumDesc": "El creador de la API QQDL & Hi-Fi. ¡Sin esta API, las descargas de Tidal no existiría!", - "@aboutBinimumDesc": { - "description": "Credit description for binimum" - }, - "aboutSachinsenalDesc": "El creador original del proyecto Hi-Fi. ¡La base de la integración de Tidal!", - "@aboutSachinsenalDesc": { - "description": "Credit description for sachinsenal0x64" - }, - "aboutSjdonadoDesc": "Creador de I No tengo Spotify (IDHS). ¡La solución de enlace de reserva que salva el día!", - "@aboutSjdonadoDesc": { - "description": "Credit description for sjdonado" - }, - "aboutDabMusic": "Música DAB", - "@aboutDabMusic": { - "description": "Name of Qobuz API service - DO NOT TRANSLATE" - }, - "aboutDabMusicDesc": "La mejor API de streaming de Qobuz. ¡Las descargas de Hi-Res no serían posibles sin esto!", - "@aboutDabMusicDesc": { - "description": "Credit for DAB Music API" - }, - "aboutSpotiSaver": "SpotiSaver", - "@aboutSpotiSaver": { - "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" - }, - "aboutSpotiSaverDesc": "Tidal de transmisión Hi-Res FLAC. ¡Una pieza clave del rompecabezas sin pérdida!", - "@aboutSpotiSaverDesc": { - "description": "Credit for SpotiSaver API" - }, - "aboutAppDescription": "Descarga pistas de Spotify con calidad sin pérdida de Tidal, Qobuz y Amazon Music.", - "@aboutAppDescription": { - "description": "App description in header card" - }, - "artistAlbums": "Álbumes", - "@artistAlbums": { - "description": "Section header for artist albums" - }, - "artistSingles": "Pistas y EPs", - "@artistSingles": { - "description": "Section header for singles/EPs" - }, - "artistCompilations": "Compilaciones", - "@artistCompilations": { - "description": "Section header for compilations" - }, - "artistPopular": "Populares", - "@artistPopular": { - "description": "Section header for popular/top tracks" - }, - "artistMonthlyListeners": "{count} oyentes mensuales", - "@artistMonthlyListeners": { - "description": "Monthly listener count display", - "placeholders": { - "count": { - "type": "String", - "description": "Formatted listener count" - } - } - }, - "trackMetadataService": "Servicio", - "@trackMetadataService": { - "description": "Metadata field - download service used" - }, - "trackMetadataPlay": "Reproducir", - "@trackMetadataPlay": { - "description": "Action button - play track" - }, - "trackMetadataShare": "Compartir", - "@trackMetadataShare": { - "description": "Action button - share track" - }, - "trackMetadataDelete": "Eliminar", - "@trackMetadataDelete": { - "description": "Action button - delete track" - }, - "setupGrantPermission": "Conceder permiso", - "@setupGrantPermission": { - "description": "Button to request permission" - }, - "setupSkip": "Omitir por ahora", - "@setupSkip": { - "description": "Skip current step button" - }, - "setupStorageAccessRequired": "Acceso al almacenamiento requerido", - "@setupStorageAccessRequired": { - "description": "Title when storage access needed" - }, - "setupStorageAccessMessageAndroid11": "Android 11+ requiere permiso \"Todos los archivos de acceso\" para guardar los archivos en la carpeta de descargas elegida.", - "@setupStorageAccessMessageAndroid11": { - "description": "Android 11+ specific explanation" - }, - "setupOpenSettings": "Abrir ajustes", - "@setupOpenSettings": { - "description": "Button to open system settings" - }, - "setupPermissionDeniedMessage": "Permiso denegado. Por favor, conceda todos los permisos para continuar.", - "@setupPermissionDeniedMessage": { - "description": "Error when permission denied" - }, - "setupPermissionRequired": "Permiso de {permissionType} requerido", - "@setupPermissionRequired": { - "description": "Generic permission required title", - "placeholders": { - "permissionType": { - "type": "String", - "description": "Type of permission (Storage/Notification)" - } - } - }, - "setupPermissionRequiredMessage": "Se requiere un permiso {permissionType} para la mejor experiencia. Puedes cambiar esto más tarde en ajustes.", - "@setupPermissionRequiredMessage": { - "description": "Generic permission required message", - "placeholders": { - "permissionType": { - "type": "String" - } - } - }, - "setupUseDefaultFolder": "¿Usar carpeta por defecto?", - "@setupUseDefaultFolder": { - "description": "Dialog title for default folder" - }, - "setupNoFolderSelected": "No se ha seleccionado ninguna carpeta. ¿Desea utilizar la carpeta por defecto?", - "@setupNoFolderSelected": { - "description": "Prompt when no folder selected" - }, - "setupUseDefault": "Usar por defecto", - "@setupUseDefault": { - "description": "Button to use default folder" - }, - "setupDownloadLocationTitle": "Ubicación de descarga", - "@setupDownloadLocationTitle": { - "description": "Download location dialog title" - }, - "setupDownloadLocationIosMessage": "En iOS, las descargas se guardan en la carpeta de documentos de la aplicación. Puede acceder a ellas desde la aplicación Archivos.", - "@setupDownloadLocationIosMessage": { - "description": "iOS-specific folder info" - }, - "setupAppDocumentsFolder": "Carpeta de documentos de App", - "@setupAppDocumentsFolder": { - "description": "iOS documents folder option" - }, - "setupAppDocumentsFolderSubtitle": "Recomendado - accesible desde la aplicación Archivos", - "@setupAppDocumentsFolderSubtitle": { - "description": "Subtitle for documents folder" - }, - "setupChooseFromFiles": "Elegir de archivos", - "@setupChooseFromFiles": { - "description": "iOS file picker option" - }, - "setupChooseFromFilesSubtitle": "Seleccione iCloud u otra ubicación", - "@setupChooseFromFilesSubtitle": { - "description": "Subtitle for file picker" - }, - "setupIosEmptyFolderWarning": "Limitación de iOS: No se pueden seleccionar carpetas vacías. Elige una carpeta con al menos un archivo.", - "@setupIosEmptyFolderWarning": { - "description": "iOS folder selection warning" - }, - "setupIcloudNotSupported": "iCloud Drive no es compatible. Utilice la carpeta Documentos de la aplicación.", - "@setupIcloudNotSupported": { - "description": "Error when user selects iCloud Drive on iOS" - }, - "setupDownloadInFlac": "Descargar pistas de Spotify en FLAC", - "@setupDownloadInFlac": { - "description": "App tagline in setup" - }, - "setupStorageGranted": "¡Permiso de almacenamiento concedido!", - "@setupStorageGranted": { - "description": "Success message for storage permission" - }, - "setupStorageRequired": "Permiso de almacenamiento requerido", - "@setupStorageRequired": { - "description": "Title when storage permission needed" - }, - "setupStorageDescription": "SpotiFLAC necesita permiso de almacenamiento para guardar sus archivos de música descargados.", - "@setupStorageDescription": { - "description": "Explanation for storage permission" - }, - "setupNotificationGranted": "¡Acceso a las notificaciones permitido!", - "@setupNotificationGranted": { - "description": "Success message for notification permission" - }, - "setupNotificationEnable": "Activar notificaciones", - "@setupNotificationEnable": { - "description": "Button to enable notifications" - }, - "setupFolderChoose": "Cambiar carpeta de descargas", - "@setupFolderChoose": { - "description": "Button to choose folder" - }, - "setupFolderDescription": "Seleccione una carpeta donde se guardará la música descargada.", - "@setupFolderDescription": { - "description": "Explanation for folder selection" - }, - "setupSelectFolder": "Seleccionar Carpeta", - "@setupSelectFolder": { - "description": "Button to select folder" - }, - "setupEnableNotifications": "Activar notificaciones", - "@setupEnableNotifications": { - "description": "Button to enable notifications" - }, - "setupNotificationBackgroundDescription": "Recibe notificaciones sobre el progreso de la descarga y la finalización. Esto te ayuda a rastrear las descargas cuando la aplicación está en segundo plano.", - "@setupNotificationBackgroundDescription": { - "description": "Detailed notification explanation" - }, - "setupSkipForNow": "Omitir por ahora", - "@setupSkipForNow": { - "description": "Skip button text" - }, - "setupNext": "Siguiente", - "@setupNext": { - "description": "Next button text" - }, - "setupGetStarted": "Empezar", - "@setupGetStarted": { - "description": "Final setup button" - }, - "setupAllowAccessToManageFiles": "Por favor, activa \"Permitir el acceso para gestionar todos los archivos\" en la siguiente pantalla.", - "@setupAllowAccessToManageFiles": { - "description": "Instruction for file access permission" - }, - "dialogCancel": "Cancelar", - "@dialogCancel": { - "description": "Dialog button - cancel action" - }, - "dialogSave": "Guardar", - "@dialogSave": { - "description": "Dialog button - save changes" - }, - "dialogDelete": "Eliminar", - "@dialogDelete": { - "description": "Dialog button - delete item" - }, - "dialogRetry": "Volver a intentar", - "@dialogRetry": { - "description": "Dialog button - retry action" - }, - "dialogClear": "Borrar", - "@dialogClear": { - "description": "Dialog button - clear items" - }, - "dialogDone": "Hecho", - "@dialogDone": { - "description": "Dialog button - action completed" - }, - "dialogImport": "Importar", - "@dialogImport": { - "description": "Dialog button - import data" - }, - "dialogDiscard": "Descartar", - "@dialogDiscard": { - "description": "Dialog button - discard changes" - }, - "dialogRemove": "Eliminar", - "@dialogRemove": { - "description": "Dialog button - remove item" - }, - "dialogUninstall": "Desinstalar", - "@dialogUninstall": { - "description": "Dialog button - uninstall extension" - }, - "dialogDiscardChanges": "¿Descartar cambios?", - "@dialogDiscardChanges": { - "description": "Dialog title - unsaved changes warning" - }, - "dialogUnsavedChanges": "Tienes cambios sin guardar. ¿Quieres descartarlos?", - "@dialogUnsavedChanges": { - "description": "Dialog message - unsaved changes" - }, - "dialogClearAll": "Eliminar todo", - "@dialogClearAll": { - "description": "Dialog title - clear all items" - }, - "dialogRemoveExtension": "Eliminar extensión", - "@dialogRemoveExtension": { - "description": "Dialog title - uninstall extension" - }, - "dialogRemoveExtensionMessage": "¿Estás seguro de que quieres eliminar esta extensión? Esto no se puede deshacer.", - "@dialogRemoveExtensionMessage": { - "description": "Dialog message - uninstall confirmation" - }, - "dialogUninstallExtension": "¿Desinstalar extensión?", - "@dialogUninstallExtension": { - "description": "Dialog title - uninstall extension" - }, - "dialogUninstallExtensionMessage": "¿Estás seguro de que quieres eliminar {extensionName}?", - "@dialogUninstallExtensionMessage": { - "description": "Dialog message - uninstall specific extension", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "dialogClearHistoryTitle": "Borrar historial", - "@dialogClearHistoryTitle": { - "description": "Dialog title - clear download history" - }, - "dialogClearHistoryMessage": "¿Estás seguro de que quieres borrar todo el historial de descargas? Esta acción no se puede deshacer.", - "@dialogClearHistoryMessage": { - "description": "Dialog message - clear history confirmation" - }, - "dialogDeleteSelectedTitle": "Borrar Seleccionados", - "@dialogDeleteSelectedTitle": { - "description": "Dialog title - delete selected items" - }, - "dialogDeleteSelectedMessage": "¿Eliminar {count} {count, plural, one {}=1{pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.", - "@dialogDeleteSelectedMessage": { - "description": "Dialog message - delete selected tracks", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dialogImportPlaylistTitle": "Importar lista de reproducción", - "@dialogImportPlaylistTitle": { - "description": "Dialog title - import CSV playlist" - }, - "dialogImportPlaylistMessage": "Se han encontrado pistas {count} en CSV. ¿Añadirlas para descargar la cola?", - "csvImportTracks": "{count} pistas de CSV", - "@csvImportTracks": { - "description": "Label shown in quality picker for CSV import", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@dialogImportPlaylistMessage": { - "description": "Dialog message - import playlist confirmation", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarAddedToQueue": "Añadido \"{trackName}\" a la cola", - "@snackbarAddedToQueue": { - "description": "Snackbar - track added to download queue", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarAddedTracksToQueue": "Añadidas pistas {count} a la cola", - "@snackbarAddedTracksToQueue": { - "description": "Snackbar - multiple tracks added to queue", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarAlreadyDownloaded": "\"{trackName}\" ya descargado", - "@snackbarAlreadyDownloaded": { - "description": "Snackbar - track already exists", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarAlreadyInLibrary": "\"{trackName}\" ya existe en tu biblioteca", - "@snackbarAlreadyInLibrary": { - "description": "Snackbar - track already exists in local library", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarHistoryCleared": "Historial borrado", - "@snackbarHistoryCleared": { - "description": "Snackbar - history deleted" - }, - "snackbarCredentialsSaved": "Credenciales guardadas", - "@snackbarCredentialsSaved": { - "description": "Snackbar - Spotify credentials saved" - }, - "snackbarCredentialsCleared": "Credenciales borradas", - "@snackbarCredentialsCleared": { - "description": "Snackbar - Spotify credentials removed" - }, - "snackbarDeletedTracks": "Eliminado {count} {count, plural, one {}=1{pista} other{pistas}}", - "@snackbarDeletedTracks": { - "description": "Snackbar - tracks deleted", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarCannotOpenFile": "No se puede abrir el archivo: {error}", - "@snackbarCannotOpenFile": { - "description": "Snackbar - file open error", - "placeholders": { - "error": { - "type": "String" - } - } - }, - "snackbarFillAllFields": "Por favor, completa todos los campos", - "@snackbarFillAllFields": { - "description": "Snackbar - validation error" - }, - "snackbarViewQueue": "Ver cola", - "@snackbarViewQueue": { - "description": "Snackbar action - view download queue" - }, - "snackbarUrlCopied": "URL {platform} copiada al portapapeles", - "@snackbarUrlCopied": { - "description": "Snackbar - URL copied", - "placeholders": { - "platform": { - "type": "String", - "description": "Platform name (Spotify/Deezer)" - } - } - }, - "snackbarFileNotFound": "Archivo no encontrado", - "@snackbarFileNotFound": { - "description": "Snackbar - file doesn't exist" - }, - "snackbarSelectExtFile": "Por favor, seleccione un archivo .spotiflac-ext", - "@snackbarSelectExtFile": { - "description": "Snackbar - wrong file type selected" - }, - "snackbarProviderPrioritySaved": "Prioridad de proveedor guardada", - "@snackbarProviderPrioritySaved": { - "description": "Snackbar - provider order saved" - }, - "snackbarMetadataProviderSaved": "Prioridad de proveedor de metadatos guardada", - "@snackbarMetadataProviderSaved": { - "description": "Snackbar - metadata provider order saved" - }, - "snackbarExtensionInstalled": "{extensionName} instalado.", - "@snackbarExtensionInstalled": { - "description": "Snackbar - extension installed successfully", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "snackbarExtensionUpdated": "{extensionName} actualizada.", - "@snackbarExtensionUpdated": { - "description": "Snackbar - extension updated successfully", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "snackbarFailedToInstall": "Fallo al instalar la extensión", - "@snackbarFailedToInstall": { - "description": "Snackbar - extension install error" - }, - "snackbarFailedToUpdate": "Error al actualizar la extensión", - "@snackbarFailedToUpdate": { - "description": "Snackbar - extension update error" - }, - "errorRateLimited": "Límite Excedido", - "@errorRateLimited": { - "description": "Error title - too many requests" - }, - "errorRateLimitedMessage": "Demasiadas solicitudes. Por favor, espere un momento antes de buscar de nuevo.", - "@errorRateLimitedMessage": { - "description": "Error message - rate limit explanation" - }, - "errorNoTracksFound": "No se encontraron pistas", - "@errorNoTracksFound": { - "description": "Error - search returned no results" - }, - "errorMissingExtensionSource": "No se puede cargar {item}: falta una fuente de extensión", - "@errorMissingExtensionSource": { - "description": "Error - extension source not available", - "placeholders": { - "item": { - "type": "String" - } - } - }, - "actionPause": "Pausar", - "@actionPause": { - "description": "Action button - pause download" - }, - "actionResume": "Reanudar", - "@actionResume": { - "description": "Action button - resume download" - }, - "actionCancel": "Cancelar", - "@actionCancel": { - "description": "Action button - cancel operation" - }, - "actionSelectAll": "Seleccionar Todo", - "@actionSelectAll": { - "description": "Action button - select all items" - }, - "actionDeselect": "Deseleccionar", - "@actionDeselect": { - "description": "Action button - deselect all" - }, - "actionRemoveCredentials": "Eliminar credenciales", - "@actionRemoveCredentials": { - "description": "Action button - delete Spotify credentials" - }, - "actionSaveCredentials": "Guardar credenciales", - "@actionSaveCredentials": { - "description": "Action button - save Spotify credentials" - }, - "selectionSelected": "{count} seleccionado", - "@selectionSelected": { - "description": "Selection count indicator", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionAllSelected": "Todas las pistas seleccionadas", - "@selectionAllSelected": { - "description": "Status - all items selected" - }, - "selectionSelectToDelete": "Seleccionar pistas a eliminar", - "@selectionSelectToDelete": { - "description": "Placeholder when nothing selected" - }, - "progressFetchingMetadata": "Obteniendo metadatos... {current}/{total}", - "@progressFetchingMetadata": { - "description": "Progress indicator - loading track info", - "placeholders": { - "current": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "progressReadingCsv": "Leyendo CSV...", - "@progressReadingCsv": { - "description": "Progress indicator - parsing CSV file" - }, - "searchSongs": "Canciones", - "@searchSongs": { - "description": "Search result category - songs" - }, - "searchArtists": "Artistas", - "@searchArtists": { - "description": "Search result category - artists" - }, - "searchAlbums": "Álbumes", - "@searchAlbums": { - "description": "Search result category - albums" - }, - "searchPlaylists": "Listas de reproducción", - "@searchPlaylists": { - "description": "Search result category - playlists" - }, - "tooltipPlay": "Reproducir", - "@tooltipPlay": { - "description": "Tooltip - play button" - }, - "filenameFormat": "Formato del nombre del archivo", - "@filenameFormat": { - "description": "Setting title - filename pattern" - }, - "filenameShowAdvancedTags": "Show advanced tags", - "@filenameShowAdvancedTags": { - "description": "Toggle label for showing advanced filename tags" - }, - "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns", - "@filenameShowAdvancedTagsDescription": { - "description": "Description for advanced filename tag toggle" - }, - "folderOrganizationNone": "Ninguna organización", - "@folderOrganizationNone": { - "description": "Folder option - flat structure" - }, - "folderOrganizationByArtist": "Por Artista", - "@folderOrganizationByArtist": { - "description": "Folder option - artist folders" - }, - "folderOrganizationByAlbum": "Por Álbum", - "@folderOrganizationByAlbum": { - "description": "Folder option - album folders" - }, - "folderOrganizationByArtistAlbum": "Artista/Álbum", - "@folderOrganizationByArtistAlbum": { - "description": "Folder option - nested folders" - }, - "folderOrganizationDescription": "Organizar los archivos descargados en carpetas", - "@folderOrganizationDescription": { - "description": "Folder organization sheet description" - }, - "folderOrganizationNoneSubtitle": "Todos los archivos de la carpeta de descargas", - "@folderOrganizationNoneSubtitle": { - "description": "Subtitle for no organization option" - }, - "folderOrganizationByArtistSubtitle": "Carpeta separada para cada artista", - "@folderOrganizationByArtistSubtitle": { - "description": "Subtitle for artist folder option" - }, - "folderOrganizationByAlbumSubtitle": "Carpeta separada para cada artista", - "@folderOrganizationByAlbumSubtitle": { - "description": "Subtitle for album folder option" - }, - "folderOrganizationByArtistAlbumSubtitle": "Carpetas organizadas por artista y álbum", - "@folderOrganizationByArtistAlbumSubtitle": { - "description": "Subtitle for nested folder option" - }, - "updateAvailable": "Actualización Disponible", - "@updateAvailable": { - "description": "Update dialog title" - }, - "updateLater": "Más tarde", - "@updateLater": { - "description": "Update button - dismiss" - }, - "updateStartingDownload": "Iniciando descarga...", - "@updateStartingDownload": { - "description": "Update status - initializing" - }, - "updateDownloadFailed": "Descarga fallida", - "@updateDownloadFailed": { - "description": "Update error title" - }, - "updateFailedMessage": "Error al descargar la actualización", - "@updateFailedMessage": { - "description": "Update error message" - }, - "updateNewVersionReady": "Una nueva versión está lista", - "@updateNewVersionReady": { - "description": "Update subtitle" - }, - "updateCurrent": "Actual", - "@updateCurrent": { - "description": "Label for current version" - }, - "updateNew": "Nuevo", - "@updateNew": { - "description": "Label for new version" - }, - "updateDownloading": "Descargando...", - "@updateDownloading": { - "description": "Update status - downloading" - }, - "updateWhatsNew": "Novedades", - "@updateWhatsNew": { - "description": "Changelog section title" - }, - "updateDownloadInstall": "Descargar & Instalar", - "@updateDownloadInstall": { - "description": "Update button - download and install" - }, - "updateDontRemind": "No recordar", - "@updateDontRemind": { - "description": "Update button - skip this version" - }, - "providerPriorityTitle": "Prioridad del proveedor", - "@providerPriorityTitle": { - "description": "Provider priority page title" - }, - "providerPriorityDescription": "Arrastra para reordenar los proveedores de descarga. La aplicación intentará usar los proveedores de arriba hacia abajo al descargar las pistas.", - "@providerPriorityDescription": { - "description": "Provider priority page description" - }, - "providerPriorityInfo": "Si una pista no está disponible en el primer proveedor, la aplicación intentará automáticamente el siguiente.", - "@providerPriorityInfo": { - "description": "Info tip about fallback behavior" - }, - "providerBuiltIn": "Integrado", - "@providerBuiltIn": { - "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" - }, - "providerExtension": "Extensión", - "@providerExtension": { - "description": "Label for extension-provided providers" - }, - "metadataProviderPriorityTitle": "Prioridad de los metadatos", - "@metadataProviderPriorityTitle": { - "description": "Metadata priority page title" - }, - "metadataProviderPriorityDescription": "Arrastra para reordenar los proveedores de metadatos. La aplicación probará los proveedores de arriba hacia abajo al buscar pistas y obtener los metadatos.", - "@metadataProviderPriorityDescription": { - "description": "Metadata priority page description" - }, - "metadataProviderPriorityInfo": "Deezer no tiene límites de tasa y se recomienda como principal. Spotify puede valorar el límite después de muchas solicitudes.", - "@metadataProviderPriorityInfo": { - "description": "Info tip about rate limits" - }, - "metadataNoRateLimits": "Sin límites de tasa", - "@metadataNoRateLimits": { - "description": "Deezer provider description" - }, - "metadataMayRateLimit": "Sin límites de tasa", - "@metadataMayRateLimit": { - "description": "Spotify provider description" - }, - "logTitle": "Registros", - "@logTitle": { - "description": "Logs screen title" - }, - "logCopied": "Registros copiados al portapapeles", - "@logCopied": { - "description": "Snackbar - logs copied" - }, - "logSearchHint": "Buscar registros...", - "@logSearchHint": { - "description": "Log search placeholder" - }, - "logFilterLevel": "Nivel", - "@logFilterLevel": { - "description": "Filter by log level" - }, - "logFilterSection": "Filtrar", - "@logFilterSection": { - "description": "Filter section title" - }, - "logShareLogs": "Compartir registros", - "@logShareLogs": { - "description": "Share button tooltip" - }, - "logClearLogs": "Borrar registros", - "@logClearLogs": { - "description": "Clear button tooltip" - }, - "logClearLogsTitle": "Limpiar registros", - "@logClearLogsTitle": { - "description": "Clear logs dialog title" - }, - "logClearLogsMessage": "¿Estás seguro que deseas limpiar todos los registros?", - "@logClearLogsMessage": { - "description": "Clear logs confirmation message" - }, - "logFilterBySeverity": "Filtrar los registros por gravedad", - "@logFilterBySeverity": { - "description": "Filter dialog title" - }, - "logNoLogsYet": "No hay registros aún", - "@logNoLogsYet": { - "description": "Empty state title" - }, - "logNoLogsYetSubtitle": "Los registros aparecerán aquí mientras usas la aplicación", - "@logNoLogsYetSubtitle": { - "description": "Empty state subtitle" - }, - "logEntriesFiltered": "Entradas ({count} filtradas)", - "@logEntriesFiltered": { - "description": "Log count with filter active", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "logEntries": "Entradas ({count})", - "@logEntries": { - "description": "Total log count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "credentialsTitle": "Credenciales de Spotify", - "@credentialsTitle": { - "description": "Credentials dialog title" - }, - "credentialsDescription": "Introduzca su ID de cliente y secreto para utilizar su propia cuota de aplicación de Spotify.", - "@credentialsDescription": { - "description": "Credentials dialog explanation" - }, - "credentialsClientId": "ID del cliente", - "@credentialsClientId": { - "description": "Client ID field label - DO NOT TRANSLATE" - }, - "credentialsClientIdHint": "Pegar ID de cliente", - "@credentialsClientIdHint": { - "description": "Client ID placeholder" - }, - "credentialsClientSecret": "Client Secret", - "@credentialsClientSecret": { - "description": "Client Secret field label - DO NOT TRANSLATE" - }, - "credentialsClientSecretHint": "Pegar Client Secret", - "@credentialsClientSecretHint": { - "description": "Client Secret placeholder" - }, - "channelStable": "Estable", - "@channelStable": { - "description": "Update channel - stable releases" - }, - "channelPreview": "Vista previa", - "@channelPreview": { - "description": "Update channel - beta/preview releases" - }, - "sectionSearchSource": "Buscar Fuente", - "@sectionSearchSource": { - "description": "Settings section header" - }, - "sectionDownload": "Descargar", - "@sectionDownload": { - "description": "Settings section header" - }, - "sectionPerformance": "Alto rendimiento", - "@sectionPerformance": { - "description": "Settings section header" - }, - "sectionApp": "Aplicación", - "@sectionApp": { - "description": "Settings section header" - }, - "sectionData": "Datos", - "@sectionData": { - "description": "Settings section header" - }, - "sectionDebug": "Depuración", - "@sectionDebug": { - "description": "Settings section header" - }, - "sectionService": "Servicio", - "@sectionService": { - "description": "Settings section header" - }, - "sectionAudioQuality": "Calidad de Sonido", - "@sectionAudioQuality": { - "description": "Settings section header" - }, - "sectionFileSettings": "Ajustes del archivo", - "@sectionFileSettings": { - "description": "Settings section header" - }, - "sectionLyrics": "Letras", - "@sectionLyrics": { - "description": "Settings section header" - }, - "lyricsMode": "Modo Letras", - "@lyricsMode": { - "description": "Setting - how to save lyrics" - }, - "lyricsModeDescription": "Elige cómo se guardan las letras de tus descargas", - "@lyricsModeDescription": { - "description": "Lyrics mode picker description" - }, - "lyricsModeEmbed": "Insertar en archivo", - "@lyricsModeEmbed": { - "description": "Lyrics mode option - embed in audio file" - }, - "lyricsModeEmbedSubtitle": "Letras almacenadas en los metadatos FLAC", - "@lyricsModeEmbedSubtitle": { - "description": "Subtitle for embed option" - }, - "lyricsModeExternal": "Archivo .lrc externo", - "@lyricsModeExternal": { - "description": "Lyrics mode option - separate LRC file" - }, - "lyricsModeExternalSubtitle": "Archivo .lrc separado para reproductores como Samsung Music", - "@lyricsModeExternalSubtitle": { - "description": "Subtitle for external option" - }, - "lyricsModeBoth": "Ambos", - "@lyricsModeBoth": { - "description": "Lyrics mode option - embed and external" - }, - "lyricsModeBothSubtitle": "Insertar y guardar archivo .lrc", - "@lyricsModeBothSubtitle": { - "description": "Subtitle for both option" - }, - "sectionColor": "Colores", - "@sectionColor": { - "description": "Settings section header" - }, - "sectionTheme": "Tema", - "@sectionTheme": { - "description": "Settings section header" - }, - "sectionLayout": "Diseño", - "@sectionLayout": { - "description": "Settings section header" - }, - "sectionLanguage": "Idioma", - "@sectionLanguage": { - "description": "Settings section header for language" - }, - "appearanceLanguage": "Idioma de la aplicación", - "@appearanceLanguage": { - "description": "Language setting title" - }, - "settingsAppearanceSubtitle": "Tema, colores, pantalla", - "@settingsAppearanceSubtitle": { - "description": "Appearance settings description" - }, - "settingsDownloadSubtitle": "Servicio, calidad, formato del nombre del archivo", - "@settingsDownloadSubtitle": { - "description": "Download settings description" - }, - "settingsOptionsSubtitle": "Alternativa, letras, carátula, actualizaciones", - "@settingsOptionsSubtitle": { - "description": "Options settings description" - }, - "settingsExtensionsSubtitle": "Administrar proveedores de descarga", - "@settingsExtensionsSubtitle": { - "description": "Extensions settings description" - }, - "settingsLogsSubtitle": "Ver registros de aplicaciones para depuración", - "@settingsLogsSubtitle": { - "description": "Logs settings description" - }, - "loadingSharedLink": "Cargando enlace compartido...", - "@loadingSharedLink": { - "description": "Status when opening shared URL" - }, - "pressBackAgainToExit": "Presione de nuevo para salir", - "@pressBackAgainToExit": { - "description": "Exit confirmation message" - }, - "downloadAllCount": "Descargar Todo ({count})", - "@downloadAllCount": { - "description": "Download all button with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "tracksCount": "{count, plural, one {}=1{1 pista} other{{count} pistas}}", - "@tracksCount": { - "description": "Track count display", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "trackCopyFilePath": "Copiar ruta de archivo", - "@trackCopyFilePath": { - "description": "Action - copy file path" - }, - "trackRemoveFromDevice": "Eliminar del dispositivo", - "@trackRemoveFromDevice": { - "description": "Action - delete downloaded file" - }, - "trackLoadLyrics": "Cargar letras", - "@trackLoadLyrics": { - "description": "Action - fetch lyrics" - }, - "trackMetadata": "Metadatos", - "@trackMetadata": { - "description": "Tab title - track metadata" - }, - "trackFileInfo": "Información de archivo", - "@trackFileInfo": { - "description": "Tab title - file information" - }, - "trackLyrics": "Letras", - "@trackLyrics": { - "description": "Tab title - lyrics" - }, - "trackFileNotFound": "Archivo no encontrado", - "@trackFileNotFound": { - "description": "Error - file doesn't exist" - }, - "trackOpenInDeezer": "Abrir en Deezer", - "@trackOpenInDeezer": { - "description": "Action - open track in Deezer app" - }, - "trackOpenInSpotify": "Abrir en Spotify", - "@trackOpenInSpotify": { - "description": "Action - open track in Spotify app" - }, - "trackTrackName": "Nombre de pista", - "@trackTrackName": { - "description": "Metadata label - track title" - }, - "trackArtist": "Artista", - "@trackArtist": { - "description": "Metadata label - artist name" - }, - "trackAlbumArtist": "Artista del álbum", - "@trackAlbumArtist": { - "description": "Metadata label - album artist" - }, - "trackAlbum": "Álbum", - "@trackAlbum": { - "description": "Metadata label - album name" - }, - "trackTrackNumber": "Número de pista", - "@trackTrackNumber": { - "description": "Metadata label - track number" - }, - "trackDiscNumber": "Número de disco", - "@trackDiscNumber": { - "description": "Metadata label - disc number" - }, - "trackDuration": "Duración", - "@trackDuration": { - "description": "Metadata label - track length" - }, - "trackAudioQuality": "Calidad del sonido", - "@trackAudioQuality": { - "description": "Metadata label - audio quality" - }, - "trackReleaseDate": "Fecha de lanzamiento", - "@trackReleaseDate": { - "description": "Metadata label - release date" - }, - "trackGenre": "Género", - "@trackGenre": { - "description": "Metadata label - music genre" - }, - "trackLabel": "Etiqueta", - "@trackLabel": { - "description": "Metadata label - record label" - }, - "trackCopyright": "Derechos de autor", - "@trackCopyright": { - "description": "Metadata label - copyright information" - }, - "trackDownloaded": "Descargado", - "@trackDownloaded": { - "description": "Metadata label - download date" - }, - "trackCopyLyrics": "Copiar letras", - "@trackCopyLyrics": { - "description": "Action - copy lyrics to clipboard" - }, - "trackLyricsNotAvailable": "Letras no disponibles para este tema", - "@trackLyricsNotAvailable": { - "description": "Message when lyrics not found" - }, - "trackLyricsTimeout": "Tiempo de espera agotado. Inténtalo de nuevo más tarde.", - "@trackLyricsTimeout": { - "description": "Message when lyrics request times out" - }, - "trackLyricsLoadFailed": "Error al cargar la letra", - "@trackLyricsLoadFailed": { - "description": "Message when lyrics loading fails" - }, - "trackEmbedLyrics": "Incrustar Letras", - "@trackEmbedLyrics": { - "description": "Action - embed lyrics into audio file" - }, - "trackLyricsEmbedded": "Letra incrustada con éxito", - "@trackLyricsEmbedded": { - "description": "Snackbar - lyrics saved to file" - }, - "trackInstrumental": "Pista intrumental", - "@trackInstrumental": { - "description": "Message when track is instrumental (no lyrics)" - }, - "trackCopiedToClipboard": "Copiado al portapapeles", - "@trackCopiedToClipboard": { - "description": "Snackbar - content copied" - }, - "trackDeleteConfirmTitle": "¿Eliminar del dispositivo?", - "@trackDeleteConfirmTitle": { - "description": "Delete confirmation title" - }, - "trackDeleteConfirmMessage": "Esto eliminará permanentemente el archivo descargado y lo eliminará de tu historial.", - "@trackDeleteConfirmMessage": { - "description": "Delete confirmation message" - }, - "dateToday": "Hoy", - "@dateToday": { - "description": "Relative date - today" - }, - "dateYesterday": "Ayer", - "@dateYesterday": { - "description": "Relative date - yesterday" - }, - "dateDaysAgo": "Hace {count} días", - "@dateDaysAgo": { - "description": "Relative date - days ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dateWeeksAgo": "{count} semanas antes", - "@dateWeeksAgo": { - "description": "Relative date - weeks ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dateMonthsAgo": "{count} meses atrás", - "@dateMonthsAgo": { - "description": "Relative date - months ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "storeFilterAll": "Todo", - "@storeFilterAll": { - "description": "Store filter - all extensions" - }, - "storeFilterMetadata": "Metadatos", - "@storeFilterMetadata": { - "description": "Store filter - metadata providers" - }, - "storeFilterDownload": "Descargar", - "@storeFilterDownload": { - "description": "Store filter - download providers" - }, - "storeFilterUtility": "Utilidad", - "@storeFilterUtility": { - "description": "Store filter - utility extensions" - }, - "storeFilterLyrics": "Letras", - "@storeFilterLyrics": { - "description": "Store filter - lyrics providers" - }, - "storeFilterIntegration": "Integración", - "@storeFilterIntegration": { - "description": "Store filter - integrations" - }, - "storeClearFilters": "Limpiar filtros", - "@storeClearFilters": { - "description": "Button to clear all filters" - }, - "extensionDefaultProvider": "Por defecto (Deezer/Spotify)", - "@extensionDefaultProvider": { - "description": "Default search provider option" - }, - "extensionDefaultProviderSubtitle": "Usar búsqueda integrada", - "@extensionDefaultProviderSubtitle": { - "description": "Subtitle for default provider" - }, - "extensionAuthor": "Autor/a", - "@extensionAuthor": { - "description": "Extension detail - author" - }, - "extensionId": "ID", - "@extensionId": { - "description": "Extension detail - unique ID" - }, - "extensionError": "Error", - "@extensionError": { - "description": "Extension detail - error message" - }, - "extensionCapabilities": "Recursos", - "@extensionCapabilities": { - "description": "Section header - extension features" - }, - "extensionMetadataProvider": "Proveedor de metadatos", - "@extensionMetadataProvider": { - "description": "Capability - provides metadata" - }, - "extensionDownloadProvider": "Proveedor de descargas", - "@extensionDownloadProvider": { - "description": "Capability - provides downloads" - }, - "extensionLyricsProvider": "Proveedor de letras", - "@extensionLyricsProvider": { - "description": "Capability - provides lyrics" - }, - "extensionUrlHandler": "Gestor de URL", - "@extensionUrlHandler": { - "description": "Capability - handles URLs" - }, - "extensionQualityOptions": "Opciones de calidad", - "@extensionQualityOptions": { - "description": "Capability - quality selection" - }, - "extensionPostProcessingHooks": "Hooks post-procesamiento", - "@extensionPostProcessingHooks": { - "description": "Capability - post-processing" - }, - "extensionPermissions": "Permisos", - "@extensionPermissions": { - "description": "Section header - required permissions" - }, - "extensionSettings": "Ajustes", - "@extensionSettings": { - "description": "Section header - extension settings" - }, - "extensionRemoveButton": "Eliminar extensión", - "@extensionRemoveButton": { - "description": "Button to uninstall extension" - }, - "extensionUpdated": "Actualizado", - "@extensionUpdated": { - "description": "Extension detail - last update" - }, - "extensionMinAppVersion": "Versión Mínima de la aplicación", - "@extensionMinAppVersion": { - "description": "Extension detail - minimum app version" - }, - "extensionCustomTrackMatching": "Coincidencia de pista personalizada", - "@extensionCustomTrackMatching": { - "description": "Capability - custom track matching algorithm" - }, - "extensionPostProcessing": "Post-Procesamiento", - "@extensionPostProcessing": { - "description": "Capability - post-download processing" - }, - "extensionHooksAvailable": "{count} hook(s) disponibles", - "@extensionHooksAvailable": { - "description": "Post-processing hooks count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "extensionPatternsCount": "Patrón(es) {count}", - "@extensionPatternsCount": { - "description": "URL patterns count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "extensionStrategy": "Estrategia: {strategy}", - "@extensionStrategy": { - "description": "Track matching strategy name", - "placeholders": { - "strategy": { - "type": "String" - } - } - }, - "extensionsProviderPrioritySection": "Prioridad del proveedor", - "@extensionsProviderPrioritySection": { - "description": "Section header - provider priority" - }, - "extensionsInstalledSection": "Extensiones instaladas", - "@extensionsInstalledSection": { - "description": "Section header - installed extensions" - }, - "extensionsNoExtensions": "No hay extensiones instaladas", - "@extensionsNoExtensions": { - "description": "Empty state - no extensions" - }, - "extensionsNoExtensionsSubtitle": "Instalar archivos .spotiflac-ext para añadir nuevos proveedores", - "@extensionsNoExtensionsSubtitle": { - "description": "Empty state subtitle" - }, - "extensionsInstallButton": "Instalar extensión", - "@extensionsInstallButton": { - "description": "Button to install extension from file" - }, - "extensionsInfoTip": "Las extensiones pueden añadir nuevos metadatos y proveedores de descargas. Sólo instalar extensiones desde fuentes confiables.", - "@extensionsInfoTip": { - "description": "Security warning about extensions" - }, - "extensionsInstalledSuccess": "Extensión instalada correctamente", - "@extensionsInstalledSuccess": { - "description": "Success message after install" - }, - "extensionsDownloadPriority": "Prioridad de descarga", - "@extensionsDownloadPriority": { - "description": "Setting - download provider order" - }, - "extensionsDownloadPrioritySubtitle": "Establecer orden de servicio de descarga", - "@extensionsDownloadPrioritySubtitle": { - "description": "Subtitle for download priority" - }, - "extensionsNoDownloadProvider": "No hay extensiones con proveedor de descargas", - "@extensionsNoDownloadProvider": { - "description": "Empty state - no download providers" - }, - "extensionsMetadataPriority": "Prioridad de los metadatos", - "@extensionsMetadataPriority": { - "description": "Setting - metadata provider order" - }, - "extensionsMetadataPrioritySubtitle": "Establecer orden de búsqueda y metadatos", - "@extensionsMetadataPrioritySubtitle": { - "description": "Subtitle for metadata priority" - }, - "extensionsNoMetadataProvider": "No hay extensiones con el proveedor de metadatos", - "@extensionsNoMetadataProvider": { - "description": "Empty state - no metadata providers" - }, - "extensionsSearchProvider": "Proveedor de búsqueda", - "@extensionsSearchProvider": { - "description": "Setting - search provider selection" - }, - "extensionsNoCustomSearch": "No hay extensiones con búsqueda personalizada", - "@extensionsNoCustomSearch": { - "description": "Empty state - no search providers" - }, - "extensionsSearchProviderDescription": "Elegir qué servicio usar para buscar pistas", - "@extensionsSearchProviderDescription": { - "description": "Search provider setting description" - }, - "extensionsCustomSearch": "Búsqueda personalizada", - "@extensionsCustomSearch": { - "description": "Label for custom search provider" - }, - "extensionsErrorLoading": "Error al cargar la extensión", - "@extensionsErrorLoading": { - "description": "Error message when extension fails to load" - }, - "qualityFlacLossless": "FLAC Lossless", - "@qualityFlacLossless": { - "description": "Quality option - CD quality FLAC" - }, - "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", - "@qualityFlacLosslessSubtitle": { - "description": "Technical spec for lossless" - }, - "qualityHiResFlac": "Hi-Res FLAC", - "@qualityHiResFlac": { - "description": "Quality option - high resolution FLAC" - }, - "qualityHiResFlacSubtitle": "24 bits/hasta 96kHz", - "@qualityHiResFlacSubtitle": { - "description": "Technical spec for hi-res" - }, - "qualityHiResFlacMax": "Hi-Res FLAC Max", - "@qualityHiResFlacMax": { - "description": "Quality option - maximum resolution FLAC" - }, - "qualityHiResFlacMaxSubtitle": "24 bits / hasta 192kHz", - "@qualityHiResFlacMaxSubtitle": { - "description": "Technical spec for hi-res max" - }, - "qualityNote": "La calidad real depende de la disponibilidad de la pista del servicio", - "@qualityNote": { - "description": "Note about quality availability" - }, - "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", - "@youtubeQualityNote": { - "description": "Note for YouTube service explaining lossy-only quality" - }, - "youtubeOpusBitrateTitle": "YouTube Opus Bitrate", - "@youtubeOpusBitrateTitle": { - "description": "Title for YouTube Opus bitrate setting" - }, - "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate", - "@youtubeMp3BitrateTitle": { - "description": "Title for YouTube MP3 bitrate setting" - }, - "downloadAskBeforeDownload": "Preguntar antes de descargar", - "@downloadAskBeforeDownload": { - "description": "Setting - show quality picker" - }, - "downloadDirectory": "Carpeta de descarga", - "@downloadDirectory": { - "description": "Setting - download folder" - }, - "downloadSeparateSinglesFolder": "Carpeta separada para pistas", - "@downloadSeparateSinglesFolder": { - "description": "Setting - separate folder for singles" - }, - "downloadAlbumFolderStructure": "Estructura de carpeta del álbum", - "@downloadAlbumFolderStructure": { - "description": "Setting - album folder organization" - }, - "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", - "@downloadUseAlbumArtistForFolders": { - "description": "Setting - choose whether artist folders use Album Artist or Track Artist" - }, - "downloadUsePrimaryArtistOnly": "Primary artist only for folders", - "@downloadUsePrimaryArtistOnly": { - "description": "Setting - strip featured artists from folder name" - }, - "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", - "@downloadUsePrimaryArtistOnlyEnabled": { - "description": "Subtitle when primary artist only is enabled" - }, - "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", - "@downloadUsePrimaryArtistOnlyDisabled": { - "description": "Subtitle when primary artist only is disabled" - }, - "downloadSelectQuality": "Seleccionar Calidad", - "@downloadSelectQuality": { - "description": "Dialog title - choose audio quality" - }, - "downloadFrom": "Descargar Desde", - "@downloadFrom": { - "description": "Label - download source" - }, - "appearanceAmoledDark": "AMOLED Oscuro", - "@appearanceAmoledDark": { - "description": "Theme option - pure black" - }, - "appearanceAmoledDarkSubtitle": "Fondo negro puro", - "@appearanceAmoledDarkSubtitle": { - "description": "Subtitle for AMOLED dark" - }, - "queueClearAll": "Eliminar todo", - "@queueClearAll": { - "description": "Button - clear all queue items" - }, - "queueClearAllMessage": "¿Estás seguro de que quieres borrar todas las descargas?", - "@queueClearAllMessage": { - "description": "Clear queue confirmation" - }, - "settingsAutoExportFailed": "Autoexportar descargas fallidas", - "@settingsAutoExportFailed": { - "description": "Setting toggle for auto-export" - }, - "settingsAutoExportFailedSubtitle": "Guardar descargas fallidas en el archivo TXT automáticamente", - "@settingsAutoExportFailedSubtitle": { - "description": "Subtitle for auto-export setting" - }, - "settingsDownloadNetwork": "Red de descarga", - "@settingsDownloadNetwork": { - "description": "Setting for network type preference" - }, - "settingsDownloadNetworkAny": "WiFi + Datos móviles", - "@settingsDownloadNetworkAny": { - "description": "Network option - use any connection" - }, - "settingsDownloadNetworkWifiOnly": "Iniciar solo por Wifi", - "@settingsDownloadNetworkWifiOnly": { - "description": "Network option - only use WiFi" - }, - "settingsDownloadNetworkSubtitle": "Elegir qué red usar para descargas. Cuando se establece en WiFi solamente, las descargas se detendrán en los datos móviles.", - "@settingsDownloadNetworkSubtitle": { - "description": "Subtitle explaining network preference" - }, - "albumFolderArtistAlbum": "Artista / Álbum", - "@albumFolderArtistAlbum": { - "description": "Album folder option" - }, - "albumFolderArtistAlbumSubtitle": "Álbumes/Nombre del Artista/Nombre del Álbum/", - "@albumFolderArtistAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderArtistYearAlbum": "Artista / [Año] Álbum", - "@albumFolderArtistYearAlbum": { - "description": "Album folder option with year" - }, - "albumFolderArtistYearAlbumSubtitle": "Álbumes/Nombre del Artista /[2005] Nombre del Álbum/", - "@albumFolderArtistYearAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderAlbumOnly": "Sólo álbum", - "@albumFolderAlbumOnly": { - "description": "Album folder option" - }, - "albumFolderAlbumOnlySubtitle": "Álbumes/Nombre del Álbum/", - "@albumFolderAlbumOnlySubtitle": { - "description": "Folder structure example" - }, - "albumFolderYearAlbum": "Álbum [Año]", - "@albumFolderYearAlbum": { - "description": "Album folder option with year" - }, - "albumFolderYearAlbumSubtitle": "Álbumes/[2005] Nombre del Álbum/", - "@albumFolderYearAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderArtistAlbumSingles": "Artista / Álbum + Pistas", - "@albumFolderArtistAlbumSingles": { - "description": "Album folder option with singles inside artist" - }, - "albumFolderArtistAlbumSinglesSubtitle": "Artista/Álbum/ y Artista/pistas/", - "@albumFolderArtistAlbumSinglesSubtitle": { - "description": "Folder structure example" - }, - "downloadedAlbumDeleteSelected": "Borrar Seleccionados", - "@downloadedAlbumDeleteSelected": { - "description": "Button - delete selected tracks" - }, - "downloadedAlbumDeleteMessage": "¿Eliminar {count} {count, plural, one {}=1{pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.", - "@downloadedAlbumDeleteMessage": { - "description": "Delete confirmation with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumSelectedCount": "{count} seleccionado", - "@downloadedAlbumSelectedCount": { - "description": "Selection count indicator", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumAllSelected": "Todas las pistas seleccionadas", - "@downloadedAlbumAllSelected": { - "description": "Status - all items selected" - }, - "downloadedAlbumTapToSelect": "Toca las pistas para seleccionar", - "@downloadedAlbumTapToSelect": { - "description": "Selection hint" - }, - "downloadedAlbumDeleteCount": "¡Eliminar {count} {count, plural, one {}=1{pista} other{pistas}}", - "@downloadedAlbumDeleteCount": { - "description": "Delete button text with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumSelectToDelete": "Seleccionar pistas a eliminar", - "@downloadedAlbumSelectToDelete": { - "description": "Placeholder when nothing selected" - }, - "downloadedAlbumDiscHeader": "Disco {discNumber}", - "@downloadedAlbumDiscHeader": { - "description": "Header for disc separator in multi-disc albums", - "placeholders": { - "discNumber": { - "type": "int", - "example": "1" - } - } - }, - "recentTypeArtist": "Artista", - "@recentTypeArtist": { - "description": "Recent access item type - artist" - }, - "recentTypeAlbum": "Álbum", - "@recentTypeAlbum": { - "description": "Recent access item type - album" - }, - "recentTypeSong": "Canción", - "@recentTypeSong": { - "description": "Recent access item type - song/track" - }, - "recentTypePlaylist": "Lista de reproducción", - "@recentTypePlaylist": { - "description": "Recent access item type - playlist" - }, - "recentEmpty": "No recent items yet", - "@recentEmpty": { - "description": "Empty state text for recent access list" - }, - "recentShowAllDownloads": "Show All Downloads", - "@recentShowAllDownloads": { - "description": "Button label to unhide hidden downloads in recent access" - }, - "recentPlaylistInfo": "Lista de reproducción: {name}", - "@recentPlaylistInfo": { - "description": "Snackbar message when tapping playlist in recent access", - "placeholders": { - "name": { - "type": "String", - "description": "Playlist name" - } - } - }, - "discographyDownload": "Descargar Discografía", - "@discographyDownload": { - "description": "Button - download artist discography" - }, - "discographyDownloadAll": "Descargar Todo", - "@discographyDownloadAll": { - "description": "Option - download entire discography" - }, - "discographyDownloadAllSubtitle": "{count} pistas de {albumCount} lanzamientos", - "@discographyDownloadAllSubtitle": { - "description": "Subtitle showing total tracks and albums", - "placeholders": { - "count": { - "type": "int" - }, - "albumCount": { - "type": "int" - } - } - }, - "discographyAlbumsOnly": "Sólo álbumes", - "@discographyAlbumsOnly": { - "description": "Option - download only albums" - }, - "discographyAlbumsOnlySubtitle": "{count} pistas de {albumCount} álbumes", - "@discographyAlbumsOnlySubtitle": { - "description": "Subtitle showing album tracks count", - "placeholders": { - "count": { - "type": "int" - }, - "albumCount": { - "type": "int" - } - } - }, - "discographySinglesOnly": "Solo sencillos & EPs ", - "@discographySinglesOnly": { - "description": "Option - download only singles" - }, - "discographySinglesOnlySubtitle": "{count} tracks from {albumCount} singles", - "@discographySinglesOnlySubtitle": { - "description": "Subtitle showing singles tracks count", - "placeholders": { - "count": { - "type": "int" - }, - "albumCount": { - "type": "int" - } - } - }, - "discographySelectAlbums": "Select Albums...", - "@discographySelectAlbums": { - "description": "Option - manually select albums to download" - }, - "discographySelectAlbumsSubtitle": "Choose specific albums or singles", - "@discographySelectAlbumsSubtitle": { - "description": "Subtitle for select albums option" - }, - "discographyFetchingTracks": "Fetching tracks...", - "@discographyFetchingTracks": { - "description": "Progress - fetching album tracks" - }, - "discographyFetchingAlbum": "Fetching {current} of {total}...", - "@discographyFetchingAlbum": { - "description": "Progress - fetching specific album", - "placeholders": { - "current": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "discographySelectedCount": "{count} selected", - "@discographySelectedCount": { - "description": "Selection count badge", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "discographyDownloadSelected": "Download Selected", - "@discographyDownloadSelected": { - "description": "Button - download selected albums" - }, - "discographyAddedToQueue": "Added {count} tracks to queue", - "@discographyAddedToQueue": { - "description": "Snackbar - tracks added from discography", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "discographySkippedDownloaded": "{added} added, {skipped} already downloaded", - "@discographySkippedDownloaded": { - "description": "Snackbar - with skipped tracks count", - "placeholders": { - "added": { - "type": "int" - }, - "skipped": { - "type": "int" - } - } - }, - "discographyNoAlbums": "No albums available", - "@discographyNoAlbums": { - "description": "Error - no albums found for artist" - }, - "discographyFailedToFetch": "Failed to fetch some albums", - "@discographyFailedToFetch": { - "description": "Error - some albums failed to load" - }, - "sectionStorageAccess": "Storage Access", - "@sectionStorageAccess": { - "description": "Section header for storage access settings" - }, - "allFilesAccess": "All Files Access", - "@allFilesAccess": { - "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" - }, - "allFilesAccessEnabledSubtitle": "Can write to any folder", - "@allFilesAccessEnabledSubtitle": { - "description": "Subtitle when all files access is enabled" - }, - "allFilesAccessDisabledSubtitle": "Limited to media folders only", - "@allFilesAccessDisabledSubtitle": { - "description": "Subtitle when all files access is disabled" - }, - "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", - "@allFilesAccessDescription": { - "description": "Description explaining when to enable all files access" - }, - "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", - "@allFilesAccessDeniedMessage": { - "description": "Message when permission is permanently denied" - }, - "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", - "@allFilesAccessDisabledMessage": { - "description": "Snackbar message when user disables all files access" - }, - "settingsLocalLibrary": "Local Library", - "@settingsLocalLibrary": { - "description": "Settings menu item - local library" - }, - "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", - "@settingsLocalLibrarySubtitle": { - "description": "Subtitle for local library settings" - }, - "settingsCache": "Storage & Cache", - "@settingsCache": { - "description": "Settings menu item - cache management" - }, - "settingsCacheSubtitle": "View size and clear cached data", - "@settingsCacheSubtitle": { - "description": "Subtitle for cache management menu" - }, - "libraryTitle": "Local Library", - "@libraryTitle": { - "description": "Library settings page title" - }, - "libraryScanSettings": "Scan Settings", - "@libraryScanSettings": { - "description": "Section header for scan settings" - }, - "libraryEnableLocalLibrary": "Enable Local Library", - "@libraryEnableLocalLibrary": { - "description": "Toggle to enable library scanning" - }, - "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", - "@libraryEnableLocalLibrarySubtitle": { - "description": "Subtitle for enable toggle" - }, - "libraryFolder": "Library Folder", - "@libraryFolder": { - "description": "Folder selection setting" - }, - "libraryFolderHint": "Tap to select folder", - "@libraryFolderHint": { - "description": "Placeholder when no folder selected" - }, - "libraryShowDuplicateIndicator": "Show Duplicate Indicator", - "@libraryShowDuplicateIndicator": { - "description": "Toggle for duplicate indicator in search" - }, - "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", - "@libraryShowDuplicateIndicatorSubtitle": { - "description": "Subtitle for duplicate indicator toggle" - }, - "libraryActions": "Actions", - "@libraryActions": { - "description": "Section header for library actions" - }, - "libraryScan": "Scan Library", - "@libraryScan": { - "description": "Button to start library scan" - }, - "libraryScanSubtitle": "Scan for audio files", - "@libraryScanSubtitle": { - "description": "Subtitle for scan button" - }, - "libraryScanSelectFolderFirst": "Select a folder first", - "@libraryScanSelectFolderFirst": { - "description": "Message when trying to scan without folder" - }, - "libraryCleanupMissingFiles": "Cleanup Missing Files", - "@libraryCleanupMissingFiles": { - "description": "Button to remove entries for missing files" - }, - "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", - "@libraryCleanupMissingFilesSubtitle": { - "description": "Subtitle for cleanup button" - }, - "libraryClear": "Clear Library", - "@libraryClear": { - "description": "Button to clear all library entries" - }, - "libraryClearSubtitle": "Remove all scanned tracks", - "@libraryClearSubtitle": { - "description": "Subtitle for clear button" - }, - "libraryClearConfirmTitle": "Clear Library", - "@libraryClearConfirmTitle": { - "description": "Dialog title for clear confirmation" - }, - "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", - "@libraryClearConfirmMessage": { - "description": "Dialog message for clear confirmation" - }, - "libraryAbout": "About Local Library", - "@libraryAbout": { - "description": "Section header for about info" - }, - "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", - "@libraryAboutDescription": { - "description": "Description of local library feature" - }, - "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}", - "@libraryTracksUnit": { - "description": "Unit label for tracks count (without the number itself)", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "libraryLastScanned": "Last scanned: {time}", - "@libraryLastScanned": { - "description": "Last scan time display", - "placeholders": { - "time": { - "type": "String" - } - } - }, - "libraryLastScannedNever": "Never", - "@libraryLastScannedNever": { - "description": "Shown when library has never been scanned" - }, - "libraryScanning": "Scanning...", - "@libraryScanning": { - "description": "Status during scan" - }, - "libraryScanProgress": "{progress}% of {total} files", - "@libraryScanProgress": { - "description": "Scan progress display", - "placeholders": { - "progress": { - "type": "String" - }, - "total": { - "type": "int" - } - } - }, - "libraryInLibrary": "In Library", - "@libraryInLibrary": { - "description": "Badge shown on tracks that exist in local library" - }, - "libraryRemovedMissingFiles": "Removed {count} missing files from library", - "@libraryRemovedMissingFiles": { - "description": "Snackbar after cleanup", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "libraryCleared": "Library cleared", - "@libraryCleared": { - "description": "Snackbar after clearing library" - }, - "libraryStorageAccessRequired": "Storage Access Required", - "@libraryStorageAccessRequired": { - "description": "Dialog title for storage permission" - }, - "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", - "@libraryStorageAccessMessage": { - "description": "Dialog message for storage permission" - }, - "libraryFolderNotExist": "Selected folder does not exist", - "@libraryFolderNotExist": { - "description": "Error when folder doesn't exist" - }, - "librarySourceDownloaded": "Downloaded", - "@librarySourceDownloaded": { - "description": "Badge for tracks downloaded via SpotiFLAC" - }, - "librarySourceLocal": "Local", - "@librarySourceLocal": { - "description": "Badge for tracks from local library scan" - }, - "libraryFilterAll": "All", - "@libraryFilterAll": { - "description": "Filter chip - show all library items" - }, - "libraryFilterDownloaded": "Downloaded", - "@libraryFilterDownloaded": { - "description": "Filter chip - show only downloaded items" - }, - "libraryFilterLocal": "Local", - "@libraryFilterLocal": { - "description": "Filter chip - show only local library items" - }, - "libraryFilterTitle": "Filters", - "@libraryFilterTitle": { - "description": "Filter bottom sheet title" - }, - "libraryFilterReset": "Reset", - "@libraryFilterReset": { - "description": "Reset all filters button" - }, - "libraryFilterApply": "Apply", - "@libraryFilterApply": { - "description": "Apply filters button" - }, - "libraryFilterSource": "Source", - "@libraryFilterSource": { - "description": "Filter section - source type" - }, - "libraryFilterQuality": "Quality", - "@libraryFilterQuality": { - "description": "Filter section - audio quality" - }, - "libraryFilterQualityHiRes": "Hi-Res (24bit)", - "@libraryFilterQualityHiRes": { - "description": "Filter option - high resolution audio" - }, - "libraryFilterQualityCD": "CD (16bit)", - "@libraryFilterQualityCD": { - "description": "Filter option - CD quality audio" - }, - "libraryFilterQualityLossy": "Lossy", - "@libraryFilterQualityLossy": { - "description": "Filter option - lossy compressed audio" - }, - "libraryFilterFormat": "Format", - "@libraryFilterFormat": { - "description": "Filter section - file format" - }, - "libraryFilterSort": "Sort", - "@libraryFilterSort": { - "description": "Filter section - sort order" - }, - "libraryFilterSortLatest": "Latest", - "@libraryFilterSortLatest": { - "description": "Sort option - newest first" - }, - "libraryFilterSortOldest": "Oldest", - "@libraryFilterSortOldest": { - "description": "Sort option - oldest first" - }, - "timeJustNow": "Just now", - "@timeJustNow": { - "description": "Relative time - less than a minute ago" - }, - "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", - "@timeMinutesAgo": { - "description": "Relative time - minutes ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", - "@timeHoursAgo": { - "description": "Relative time - hours ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", - "@tutorialWelcomeTitle": { - "description": "Tutorial welcome page title" - }, - "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", - "@tutorialWelcomeDesc": { - "description": "Tutorial welcome page description" - }, - "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", - "@tutorialWelcomeTip1": { - "description": "Tutorial welcome tip 1" - }, - "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", - "@tutorialWelcomeTip2": { - "description": "Tutorial welcome tip 2" - }, - "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", - "@tutorialWelcomeTip3": { - "description": "Tutorial welcome tip 3" - }, - "tutorialSearchTitle": "Finding Music", - "@tutorialSearchTitle": { - "description": "Tutorial search page title" - }, - "tutorialSearchDesc": "There are two easy ways to find music you want to download.", - "@tutorialSearchDesc": { - "description": "Tutorial search page description" - }, - "tutorialDownloadTitle": "Downloading Music", - "@tutorialDownloadTitle": { - "description": "Tutorial download page title" - }, - "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", - "@tutorialDownloadDesc": { - "description": "Tutorial download page description" - }, - "tutorialLibraryTitle": "Your Library", - "@tutorialLibraryTitle": { - "description": "Tutorial library page title" - }, - "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", - "@tutorialLibraryDesc": { - "description": "Tutorial library page description" - }, - "tutorialLibraryTip1": "View download progress and queue in the Library tab", - "@tutorialLibraryTip1": { - "description": "Tutorial library tip 1" - }, - "tutorialLibraryTip2": "Tap any track to play it with your music player", - "@tutorialLibraryTip2": { - "description": "Tutorial library tip 2" - }, - "tutorialLibraryTip3": "Switch between list and grid view for better browsing", - "@tutorialLibraryTip3": { - "description": "Tutorial library tip 3" - }, - "tutorialExtensionsTitle": "Extensions", - "@tutorialExtensionsTitle": { - "description": "Tutorial extensions page title" - }, - "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", - "@tutorialExtensionsDesc": { - "description": "Tutorial extensions page description" - }, - "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", - "@tutorialExtensionsTip1": { - "description": "Tutorial extensions tip 1" - }, - "tutorialExtensionsTip2": "Add new download providers or search sources", - "@tutorialExtensionsTip2": { - "description": "Tutorial extensions tip 2" - }, - "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", - "@tutorialExtensionsTip3": { - "description": "Tutorial extensions tip 3" - }, - "tutorialSettingsTitle": "Customize Your Experience", - "@tutorialSettingsTitle": { - "description": "Tutorial settings page title" - }, - "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", - "@tutorialSettingsDesc": { - "description": "Tutorial settings page description" - }, - "tutorialSettingsTip1": "Change download location and folder organization", - "@tutorialSettingsTip1": { - "description": "Tutorial settings tip 1" - }, - "tutorialSettingsTip2": "Set default audio quality and format preferences", - "@tutorialSettingsTip2": { - "description": "Tutorial settings tip 2" - }, - "tutorialSettingsTip3": "Customize app theme and appearance", - "@tutorialSettingsTip3": { - "description": "Tutorial settings tip 3" - }, - "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", - "@tutorialReadyMessage": { - "description": "Tutorial completion message" - }, - "libraryForceFullScan": "Force Full Scan", - "@libraryForceFullScan": { - "description": "Button to force a complete rescan of library" - }, - "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", - "@libraryForceFullScanSubtitle": { - "description": "Subtitle for force full scan button" - }, - "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", - "@cleanupOrphanedDownloads": { - "description": "Button to remove history entries for deleted files" - }, - "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", - "@cleanupOrphanedDownloadsSubtitle": { - "description": "Subtitle for orphaned cleanup button" - }, - "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", - "@cleanupOrphanedDownloadsResult": { - "description": "Snackbar after orphan cleanup", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "cleanupOrphanedDownloadsNone": "No orphaned entries found", - "@cleanupOrphanedDownloadsNone": { - "description": "Snackbar when no orphans found" - }, - "cacheTitle": "Storage & Cache", - "@cacheTitle": { - "description": "Cache management page title" - }, - "cacheSummaryTitle": "Cache overview", - "@cacheSummaryTitle": { - "description": "Heading for cache summary card" - }, - "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", - "@cacheSummarySubtitle": { - "description": "Helper text for cache summary card" - }, - "cacheEstimatedTotal": "Estimated cache usage: {size}", - "@cacheEstimatedTotal": { - "description": "Total cache size shown in summary", - "placeholders": { - "size": { - "type": "String" - } - } - }, - "cacheSectionStorage": "Cached Data", - "@cacheSectionStorage": { - "description": "Section header for cache entries" - }, - "cacheSectionMaintenance": "Maintenance", - "@cacheSectionMaintenance": { - "description": "Section header for cleanup actions" - }, - "cacheAppDirectory": "App cache directory", - "@cacheAppDirectory": { - "description": "Cache item title for app cache directory" - }, - "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", - "@cacheAppDirectoryDesc": { - "description": "Description of what app cache directory contains" - }, - "cacheTempDirectory": "Temporary directory", - "@cacheTempDirectory": { - "description": "Cache item title for temporary files directory" - }, - "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", - "@cacheTempDirectoryDesc": { - "description": "Description of what temporary directory contains" - }, - "cacheCoverImage": "Cover image cache", - "@cacheCoverImage": { - "description": "Cache item title for persistent cover images" - }, - "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", - "@cacheCoverImageDesc": { - "description": "Description of what cover image cache contains" - }, - "cacheLibraryCover": "Library cover cache", - "@cacheLibraryCover": { - "description": "Cache item title for local library cover art images" - }, - "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", - "@cacheLibraryCoverDesc": { - "description": "Description of what library cover cache contains" - }, - "cacheExploreFeed": "Explore feed cache", - "@cacheExploreFeed": { - "description": "Cache item title for explore home feed cache" - }, - "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", - "@cacheExploreFeedDesc": { - "description": "Description of what explore feed cache contains" - }, - "cacheTrackLookup": "Track lookup cache", - "@cacheTrackLookup": { - "description": "Cache item title for track ID lookup cache" - }, - "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", - "@cacheTrackLookupDesc": { - "description": "Description of what track lookup cache contains" - }, - "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", - "@cacheCleanupUnusedDesc": { - "description": "Description of what cleanup unused data does" - }, - "cacheNoData": "No cached data", - "@cacheNoData": { - "description": "Label when cache category has no data" - }, - "cacheSizeWithFiles": "{size} in {count} files", - "@cacheSizeWithFiles": { - "description": "Cache size and file count", - "placeholders": { - "size": { - "type": "String" - }, - "count": { - "type": "int" - } - } - }, - "cacheSizeOnly": "{size}", - "@cacheSizeOnly": { - "description": "Cache size only", - "placeholders": { - "size": { - "type": "String" - } - } - }, - "cacheEntries": "{count} entries", - "@cacheEntries": { - "description": "Track cache entry count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "cacheClearSuccess": "Cleared: {target}", - "@cacheClearSuccess": { - "description": "Snackbar after clearing selected cache", - "placeholders": { - "target": { - "type": "String" - } - } - }, - "cacheClearConfirmTitle": "Clear cache?", - "@cacheClearConfirmTitle": { - "description": "Dialog title before clearing one cache category" - }, - "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", - "@cacheClearConfirmMessage": { - "description": "Dialog message before clearing selected cache", - "placeholders": { - "target": { - "type": "String" - } - } - }, - "cacheClearAllConfirmTitle": "Clear all cache?", - "@cacheClearAllConfirmTitle": { - "description": "Dialog title before clearing all caches" - }, - "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", - "@cacheClearAllConfirmMessage": { - "description": "Dialog message before clearing all caches" - }, - "cacheClearAll": "Clear all cache", - "@cacheClearAll": { - "description": "Button label to clear all caches" - }, - "cacheCleanupUnused": "Cleanup unused data", - "@cacheCleanupUnused": { - "description": "Action title for cleaning unused entries" - }, - "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", - "@cacheCleanupUnusedSubtitle": { - "description": "Subtitle for cleanup unused data action" - }, - "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", - "@cacheCleanupResult": { - "description": "Snackbar after unused data cleanup", - "placeholders": { - "downloadCount": { - "type": "int" - }, - "libraryCount": { - "type": "int" - } - } - }, - "cacheRefreshStats": "Refresh stats", - "@cacheRefreshStats": { - "description": "Button label to refresh cache statistics" - }, - "trackSaveCoverArt": "Save Cover Art", - "@trackSaveCoverArt": { - "description": "Menu action - save album cover art as file" - }, - "trackSaveCoverArtSubtitle": "Save album art as .jpg file", - "@trackSaveCoverArtSubtitle": { - "description": "Subtitle for save cover art action" - }, - "trackSaveLyrics": "Save Lyrics (.lrc)", - "@trackSaveLyrics": { - "description": "Menu action - save lyrics as .lrc file" - }, - "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", - "@trackSaveLyricsSubtitle": { - "description": "Subtitle for save lyrics action" - }, - "trackSaveLyricsProgress": "Saving lyrics...", - "@trackSaveLyricsProgress": { - "description": "Snackbar while saving lyrics to file" - }, - "trackReEnrich": "Re-enrich", - "@trackReEnrich": { - "description": "Menu action - re-embed metadata into audio file" - }, - "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", - "@trackReEnrichOnlineSubtitle": { - "description": "Subtitle for re-enrich metadata action for local items" - }, - "trackEditMetadata": "Edit Metadata", - "@trackEditMetadata": { - "description": "Menu action - edit embedded metadata" - }, - "trackCoverSaved": "Cover art saved to {fileName}", - "@trackCoverSaved": { - "description": "Snackbar after cover art saved", - "placeholders": { - "fileName": { - "type": "String" - } - } - }, - "trackCoverNoSource": "No cover art source available", - "@trackCoverNoSource": { - "description": "Snackbar when no cover art URL or embedded cover" - }, - "trackLyricsSaved": "Lyrics saved to {fileName}", - "@trackLyricsSaved": { - "description": "Snackbar after lyrics saved", - "placeholders": { - "fileName": { - "type": "String" - } - } - }, - "trackReEnrichProgress": "Re-enriching metadata...", - "@trackReEnrichProgress": { - "description": "Snackbar while re-enriching metadata" - }, - "trackReEnrichSearching": "Searching metadata online...", - "@trackReEnrichSearching": { - "description": "Snackbar while searching metadata from internet for local items" - }, - "trackReEnrichSuccess": "Metadata re-enriched successfully", - "@trackReEnrichSuccess": { - "description": "Snackbar after successful re-enrichment" - }, - "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", - "@trackReEnrichFfmpegFailed": { - "description": "Snackbar when FFmpeg embed fails for MP3/Opus" - }, - "trackSaveFailed": "Failed: {error}", - "@trackSaveFailed": { - "description": "Snackbar when save operation fails", - "placeholders": { - "error": { - "type": "String" - } - } - }, - "trackConvertFormat": "Convert Format", - "@trackConvertFormat": { - "description": "Menu item - convert audio format" - }, - "trackConvertFormatSubtitle": "Convert to MP3 or Opus", - "@trackConvertFormatSubtitle": { - "description": "Subtitle for convert format menu item" - }, - "trackConvertTitle": "Convert Audio", - "@trackConvertTitle": { - "description": "Title of convert bottom sheet" - }, - "trackConvertTargetFormat": "Target Format", - "@trackConvertTargetFormat": { - "description": "Label for format selection" - }, - "trackConvertBitrate": "Bitrate", - "@trackConvertBitrate": { - "description": "Label for bitrate selection" - }, - "trackConvertConfirmTitle": "Confirm Conversion", - "@trackConvertConfirmTitle": { - "description": "Confirmation dialog title" - }, - "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", - "@trackConvertConfirmMessage": { - "description": "Confirmation dialog message", - "placeholders": { - "sourceFormat": { - "type": "String" - }, - "targetFormat": { - "type": "String" - }, - "bitrate": { - "type": "String" - } - } - }, - "trackConvertConverting": "Converting audio...", - "@trackConvertConverting": { - "description": "Snackbar while converting" - }, - "trackConvertSuccess": "Converted to {format} successfully", - "@trackConvertSuccess": { - "description": "Snackbar after successful conversion", - "placeholders": { - "format": { - "type": "String" - } - } - }, - "trackConvertFailed": "Conversion failed", - "@trackConvertFailed": { - "description": "Snackbar when conversion fails" - }, - "actionCreate": "Create", - "@actionCreate": { - "description": "Generic action button - create" - }, - "collectionFoldersTitle": "My folders", - "@collectionFoldersTitle": { - "description": "Library section title for custom folders" - }, - "collectionWishlist": "Wishlist", - "@collectionWishlist": { - "description": "Custom folder for saved tracks to download later" - }, - "collectionLoved": "Loved", - "@collectionLoved": { - "description": "Custom folder for favorite tracks" - }, - "collectionPlaylists": "Playlists", - "@collectionPlaylists": { - "description": "Custom user playlists folder" - }, - "collectionPlaylist": "Playlist", - "@collectionPlaylist": { - "description": "Single playlist label" - }, - "collectionAddToPlaylist": "Add to playlist", - "@collectionAddToPlaylist": { - "description": "Action to add a track to user playlist" - }, - "collectionCreatePlaylist": "Create playlist", - "@collectionCreatePlaylist": { - "description": "Action to create a new playlist" - }, - "collectionNoPlaylistsYet": "No playlists yet", - "@collectionNoPlaylistsYet": { - "description": "Empty state title when user has no playlists" - }, - "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks", - "@collectionNoPlaylistsSubtitle": { - "description": "Empty state subtitle when user has no playlists" - }, - "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}", - "@collectionPlaylistTracks": { - "description": "Track count label for custom playlists", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "collectionAddedToPlaylist": "Added to \"{playlistName}\"", - "@collectionAddedToPlaylist": { - "description": "Snackbar after adding track to playlist", - "placeholders": { - "playlistName": { - "type": "String" - } - } - }, - "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"", - "@collectionAlreadyInPlaylist": { - "description": "Snackbar when track already exists in playlist", - "placeholders": { - "playlistName": { - "type": "String" - } - } - }, - "collectionPlaylistCreated": "Playlist created", - "@collectionPlaylistCreated": { - "description": "Snackbar after creating playlist" - }, - "collectionPlaylistNameHint": "Playlist name", - "@collectionPlaylistNameHint": { - "description": "Hint text for playlist name input" - }, - "collectionPlaylistNameRequired": "Playlist name is required", - "@collectionPlaylistNameRequired": { - "description": "Validation error for empty playlist name" - }, - "collectionRenamePlaylist": "Rename playlist", - "@collectionRenamePlaylist": { - "description": "Action to rename playlist" - }, - "collectionDeletePlaylist": "Delete playlist", - "@collectionDeletePlaylist": { - "description": "Action to delete playlist" - }, - "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?", - "@collectionDeletePlaylistMessage": { - "description": "Confirmation message for deleting playlist", - "placeholders": { - "playlistName": { - "type": "String" - } - } - }, - "collectionPlaylistDeleted": "Playlist deleted", - "@collectionPlaylistDeleted": { - "description": "Snackbar after deleting playlist" - }, - "collectionPlaylistRenamed": "Playlist renamed", - "@collectionPlaylistRenamed": { - "description": "Snackbar after renaming playlist" - }, - "collectionWishlistEmptyTitle": "Wishlist is empty", - "@collectionWishlistEmptyTitle": { - "description": "Wishlist empty state title" - }, - "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later", - "@collectionWishlistEmptySubtitle": { - "description": "Wishlist empty state subtitle" - }, - "collectionLovedEmptyTitle": "Loved folder is empty", - "@collectionLovedEmptyTitle": { - "description": "Loved empty state title" - }, - "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites", - "@collectionLovedEmptySubtitle": { - "description": "Loved empty state subtitle" - }, - "collectionPlaylistEmptyTitle": "Playlist is empty", - "@collectionPlaylistEmptyTitle": { - "description": "Playlist empty state title" - }, - "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here", - "@collectionPlaylistEmptySubtitle": { - "description": "Playlist empty state subtitle" - }, - "collectionRemoveFromPlaylist": "Remove from playlist", - "@collectionRemoveFromPlaylist": { - "description": "Tooltip for removing track from playlist" - }, - "collectionRemoveFromFolder": "Remove from folder", - "@collectionRemoveFromFolder": { - "description": "Tooltip for removing track from wishlist/loved folder" - }, - "collectionRemoved": "\"{trackName}\" removed", - "@collectionRemoved": { - "description": "Snackbar after removing a track from a collection", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionAddedToLoved": "\"{trackName}\" added to Loved", - "@collectionAddedToLoved": { - "description": "Snackbar after adding track to loved folder", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved", - "@collectionRemovedFromLoved": { - "description": "Snackbar after removing track from loved folder", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist", - "@collectionAddedToWishlist": { - "description": "Snackbar after adding track to wishlist", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist", - "@collectionRemovedFromWishlist": { - "description": "Snackbar after removing track from wishlist", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "trackOptionAddToLoved": "Add to Loved", - "@trackOptionAddToLoved": { - "description": "Bottom sheet action label - add track to loved folder" - }, - "trackOptionRemoveFromLoved": "Remove from Loved", - "@trackOptionRemoveFromLoved": { - "description": "Bottom sheet action label - remove track from loved folder" - }, - "trackOptionAddToWishlist": "Add to Wishlist", - "@trackOptionAddToWishlist": { - "description": "Bottom sheet action label - add track to wishlist" - }, - "trackOptionRemoveFromWishlist": "Remove from Wishlist", - "@trackOptionRemoveFromWishlist": { - "description": "Bottom sheet action label - remove track from wishlist" - }, - "collectionPlaylistChangeCover": "Change cover image", - "@collectionPlaylistChangeCover": { - "description": "Bottom sheet action to pick a custom cover image for a playlist" - }, - "collectionPlaylistRemoveCover": "Remove cover image", - "@collectionPlaylistRemoveCover": { - "description": "Bottom sheet action to remove custom cover image from a playlist" - }, - "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}", - "@selectionShareCount": { - "description": "Share button text with count in selection mode", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionShareNoFiles": "No shareable files found", - "@selectionShareNoFiles": { - "description": "Snackbar when no selected files exist on disk" - }, - "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}", - "@selectionConvertCount": { - "description": "Convert button text with count in selection mode", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionConvertNoConvertible": "No convertible tracks selected", - "@selectionConvertNoConvertible": { - "description": "Snackbar when no selected tracks support conversion" - }, - "selectionBatchConvertConfirmTitle": "Batch Convert", - "@selectionBatchConvertConfirmTitle": { - "description": "Confirmation dialog title for batch conversion" - }, - "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.", - "@selectionBatchConvertConfirmMessage": { - "description": "Confirmation dialog message for batch conversion", - "placeholders": { - "count": { - "type": "int" - }, - "format": { - "type": "String" - }, - "bitrate": { - "type": "String" - } - } - }, - "selectionBatchConvertProgress": "Converting {current} of {total}...", - "@selectionBatchConvertProgress": { - "description": "Snackbar during batch conversion progress", - "placeholders": { - "current": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}", - "@selectionBatchConvertSuccess": { - "description": "Snackbar after batch conversion completes", - "placeholders": { - "success": { - "type": "int" - }, - "total": { - "type": "int" - }, - "format": { - "type": "String" - } - } - }, - "downloadedAlbumDownloadedCount": "{count} descargado", - "@downloadedAlbumDownloadedCount": { - "description": "Downloaded tracks count badge", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", - "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { - "description": "Subtitle when Album Artist is used for folder naming" - }, - "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", - "@downloadUseAlbumArtistForFoldersTrackSubtitle": { - "description": "Subtitle when Track Artist is used for folder naming" - } -} \ No newline at end of file diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index dcd14626..de9d90d0 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -897,6 +897,18 @@ "@errorNoTracksFound": { "description": "Error - search returned no results" }, + "errorUrlNotRecognized": "Link tidak dikenali", + "@errorUrlNotRecognized": { + "description": "Error title - URL not handled by any extension or service" + }, + "errorUrlNotRecognizedMessage": "Link ini tidak didukung. Pastikan URL benar dan ekstensi yang kompatibel sudah terpasang.", + "@errorUrlNotRecognizedMessage": { + "description": "Error message - URL not recognized explanation" + }, + "errorUrlFetchFailed": "Gagal memuat konten dari link ini. Silakan coba lagi.", + "@errorUrlFetchFailed": { + "description": "Error message - generic URL fetch failure" + }, "errorMissingExtensionSource": "Tidak dapat memuat {item}: sumber ekstensi tidak ada", "@errorMissingExtensionSource": { "description": "Error - extension source not available", diff --git a/lib/l10n/arb/app_pt-PT.arb b/lib/l10n/arb/app_pt-PT.arb deleted file mode 100644 index 7d06cfa8..00000000 --- a/lib/l10n/arb/app_pt-PT.arb +++ /dev/null @@ -1,3105 +0,0 @@ -{ - "@@locale": "pt-PT", - "@@last_modified": "2026-01-16", - "appName": "SpotiFLAC", - "@appName": { - "description": "App name - DO NOT TRANSLATE" - }, - "navHome": "Início", - "@navHome": { - "description": "Bottom navigation - Home tab" - }, - "navLibrary": "Library", - "@navLibrary": { - "description": "Bottom navigation - Library tab" - }, - "navSettings": "Configurações", - "@navSettings": { - "description": "Bottom navigation - Settings tab" - }, - "navStore": "Loja", - "@navStore": { - "description": "Bottom navigation - Extension store tab" - }, - "homeTitle": "Início", - "@homeTitle": { - "description": "Home screen title" - }, - "homeSubtitle": "Cole um link do Spotify ou procure por nome", - "@homeSubtitle": { - "description": "Subtitle shown below search box" - }, - "homeSupports": "Suporte: Faixas, Álbuns, Playlists, URLs de Artista", - "@homeSupports": { - "description": "Info text about supported URL types" - }, - "homeRecent": "Recentes", - "@homeRecent": { - "description": "Section header for recent searches" - }, - "historyFilterAll": "Tudo", - "@historyFilterAll": { - "description": "Filter chip - show all items" - }, - "historyFilterAlbums": "Álbuns", - "@historyFilterAlbums": { - "description": "Filter chip - show albums only" - }, - "historyFilterSingles": "Singles", - "@historyFilterSingles": { - "description": "Filter chip - show singles only" - }, - "historySearchHint": "Pesquisar histórico...", - "@historySearchHint": { - "description": "Search bar placeholder in history" - }, - "settingsTitle": "Configurações", - "@settingsTitle": { - "description": "Settings screen title" - }, - "settingsDownload": "Download", - "@settingsDownload": { - "description": "Settings section - download options" - }, - "settingsAppearance": "Aparência", - "@settingsAppearance": { - "description": "Settings section - visual customization" - }, - "settingsOptions": "Opções", - "@settingsOptions": { - "description": "Settings section - app options" - }, - "settingsExtensions": "Extensões", - "@settingsExtensions": { - "description": "Settings section - extension management" - }, - "settingsAbout": "Sobre", - "@settingsAbout": { - "description": "Settings section - app info" - }, - "downloadTitle": "Download", - "@downloadTitle": { - "description": "Download settings page title" - }, - "downloadAskQualitySubtitle": "Mostrar seletor de qualidade para cada download", - "@downloadAskQualitySubtitle": { - "description": "Subtitle for ask quality toggle" - }, - "downloadFilenameFormat": "Formato do Nome do Arquivo", - "@downloadFilenameFormat": { - "description": "Setting for output filename pattern" - }, - "downloadFolderOrganization": "Organização de Pastas", - "@downloadFolderOrganization": { - "description": "Setting for folder structure" - }, - "appearanceTitle": "Aparência", - "@appearanceTitle": { - "description": "Appearance settings page title" - }, - "appearanceThemeSystem": "Sistema", - "@appearanceThemeSystem": { - "description": "Follow system theme" - }, - "appearanceThemeLight": "Claro", - "@appearanceThemeLight": { - "description": "Light theme" - }, - "appearanceThemeDark": "Escuro", - "@appearanceThemeDark": { - "description": "Dark theme" - }, - "appearanceDynamicColor": "Cores Dinâmicas", - "@appearanceDynamicColor": { - "description": "Material You dynamic colors" - }, - "appearanceDynamicColorSubtitle": "Usar cores do seu papel de parede", - "@appearanceDynamicColorSubtitle": { - "description": "Subtitle for dynamic color" - }, - "appearanceHistoryView": "Visualização do Histórico", - "@appearanceHistoryView": { - "description": "Layout style for history" - }, - "appearanceHistoryViewList": "Lista", - "@appearanceHistoryViewList": { - "description": "List layout option" - }, - "appearanceHistoryViewGrid": "Grade", - "@appearanceHistoryViewGrid": { - "description": "Grid layout option" - }, - "optionsTitle": "Opções", - "@optionsTitle": { - "description": "Options settings page title" - }, - "optionsPrimaryProvider": "Provedor Primário", - "@optionsPrimaryProvider": { - "description": "Main search provider setting" - }, - "optionsPrimaryProviderSubtitle": "Serviço usado ao pesquisar por nome da faixa.", - "@optionsPrimaryProviderSubtitle": { - "description": "Subtitle for primary provider" - }, - "optionsUsingExtension": "Usando a extensão: {extensionName}", - "@optionsUsingExtension": { - "description": "Shows active extension name", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "optionsSwitchBack": "Toque no Deezer ou Spotify para alternar de volta da extensão", - "@optionsSwitchBack": { - "description": "Hint to switch back to built-in providers" - }, - "optionsAutoFallback": "Fallback Automático", - "@optionsAutoFallback": { - "description": "Auto-retry with other services" - }, - "optionsAutoFallbackSubtitle": "Tentar outros serviços se o download falhar", - "@optionsAutoFallbackSubtitle": { - "description": "Subtitle for auto fallback" - }, - "optionsUseExtensionProviders": "Usar Provedores de Extensão", - "@optionsUseExtensionProviders": { - "description": "Enable extension download providers" - }, - "optionsUseExtensionProvidersOn": "Extensões serão tentadas primeiro", - "@optionsUseExtensionProvidersOn": { - "description": "Status when extension providers enabled" - }, - "optionsUseExtensionProvidersOff": "Usando apenas provedores integrados", - "@optionsUseExtensionProvidersOff": { - "description": "Status when extension providers disabled" - }, - "optionsEmbedLyrics": "Incorporar Letras", - "@optionsEmbedLyrics": { - "description": "Embed lyrics in audio files" - }, - "optionsEmbedLyricsSubtitle": "Incorporar letras sincronizadas aos arquivos FLAC", - "@optionsEmbedLyricsSubtitle": { - "description": "Subtitle for embed lyrics" - }, - "optionsMaxQualityCover": "Capa de Qualidade Máxima", - "@optionsMaxQualityCover": { - "description": "Download highest quality album art" - }, - "optionsMaxQualityCoverSubtitle": "Baixar capa do álbum com a mais alta resolução", - "@optionsMaxQualityCoverSubtitle": { - "description": "Subtitle for max quality cover" - }, - "optionsConcurrentDownloads": "Downloads Simultâneos", - "@optionsConcurrentDownloads": { - "description": "Number of parallel downloads" - }, - "optionsConcurrentSequential": "Sequencial (1 por vez)", - "@optionsConcurrentSequential": { - "description": "Download one at a time" - }, - "optionsConcurrentParallel": "{count} downloads paralelos", - "@optionsConcurrentParallel": { - "description": "Multiple parallel downloads", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "optionsConcurrentWarning": "Downloads simultâneos podem causar um limite da taxa (ratelimit)", - "@optionsConcurrentWarning": { - "description": "Warning about rate limits" - }, - "optionsExtensionStore": "Loja de Extensões", - "@optionsExtensionStore": { - "description": "Show/hide store tab" - }, - "optionsExtensionStoreSubtitle": "Mostrar aba da Loja na navegação", - "@optionsExtensionStoreSubtitle": { - "description": "Subtitle for extension store toggle" - }, - "optionsCheckUpdates": "Procurar Atualizações", - "@optionsCheckUpdates": { - "description": "Auto update check toggle" - }, - "optionsCheckUpdatesSubtitle": "Notificar quando uma nova versão estiver disponível", - "@optionsCheckUpdatesSubtitle": { - "description": "Subtitle for update check" - }, - "optionsUpdateChannel": "Canal de Atualização", - "@optionsUpdateChannel": { - "description": "Stable vs preview releases" - }, - "optionsUpdateChannelStable": "Somente versões estáveis", - "@optionsUpdateChannelStable": { - "description": "Only stable updates" - }, - "optionsUpdateChannelPreview": "Obter versões de prévia", - "@optionsUpdateChannelPreview": { - "description": "Include beta/preview updates" - }, - "optionsUpdateChannelWarning": "A prévia pode conter erros ou recursos incompletos", - "@optionsUpdateChannelWarning": { - "description": "Warning about preview channel" - }, - "optionsClearHistory": "Limpar Histórico de Download", - "@optionsClearHistory": { - "description": "Delete all download history" - }, - "optionsClearHistorySubtitle": "Remover todas as faixas baixadas do histórico", - "@optionsClearHistorySubtitle": { - "description": "Subtitle for clear history" - }, - "optionsDetailedLogging": "Registro detalhado", - "@optionsDetailedLogging": { - "description": "Enable verbose logs for debugging" - }, - "optionsDetailedLoggingOn": "Registros detalhados estão sendo gravados", - "@optionsDetailedLoggingOn": { - "description": "Status when logging enabled" - }, - "optionsDetailedLoggingOff": "Habilitar para relatórios de erros", - "@optionsDetailedLoggingOff": { - "description": "Status when logging disabled" - }, - "optionsSpotifyCredentials": "Credenciais do Spotify", - "@optionsSpotifyCredentials": { - "description": "Spotify API credentials setting" - }, - "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", - "@optionsSpotifyCredentialsConfigured": { - "description": "Shows configured client ID preview", - "placeholders": { - "clientId": { - "type": "String" - } - } - }, - "optionsSpotifyCredentialsRequired": "Obrigatório - toque para configurar", - "@optionsSpotifyCredentialsRequired": { - "description": "Prompt to set up credentials" - }, - "optionsSpotifyWarning": "O Spotify requer as suas próprias credenciais de API. Consiga gratuitamente em developer.spotify.com", - "@optionsSpotifyWarning": { - "description": "Info about Spotify API requirement" - }, - "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", - "@optionsSpotifyDeprecationWarning": { - "description": "Warning about Spotify API deprecation" - }, - "extensionsTitle": "Extensões", - "@extensionsTitle": { - "description": "Extensions page title" - }, - "extensionsDisabled": "Desabilitado", - "@extensionsDisabled": { - "description": "Extension status - inactive" - }, - "extensionsVersion": "Versão {version}", - "@extensionsVersion": { - "description": "Extension version display", - "placeholders": { - "version": { - "type": "String" - } - } - }, - "extensionsAuthor": "por {author}", - "@extensionsAuthor": { - "description": "Extension author credit", - "placeholders": { - "author": { - "type": "String" - } - } - }, - "extensionsUninstall": "Desinstalar", - "@extensionsUninstall": { - "description": "Uninstall extension button" - }, - "storeTitle": "Loja de Extensões", - "@storeTitle": { - "description": "Store screen title" - }, - "storeSearch": "Pesquisar extensões...", - "@storeSearch": { - "description": "Store search placeholder" - }, - "storeInstall": "Instalar", - "@storeInstall": { - "description": "Install extension button" - }, - "storeInstalled": "Instalado", - "@storeInstalled": { - "description": "Already installed badge" - }, - "storeUpdate": "Atualizar", - "@storeUpdate": { - "description": "Update available button" - }, - "aboutTitle": "Sobre", - "@aboutTitle": { - "description": "About page title" - }, - "aboutContributors": "Colaboradores", - "@aboutContributors": { - "description": "Section for contributors" - }, - "aboutMobileDeveloper": "Desenvolvedor da versão móvel", - "@aboutMobileDeveloper": { - "description": "Role description for mobile dev" - }, - "aboutOriginalCreator": "Criador do SpotiFLAC original", - "@aboutOriginalCreator": { - "description": "Role description for original creator" - }, - "aboutLogoArtist": "O artista talentoso que criou o nosso lindo logotipo do aplicativo!", - "@aboutLogoArtist": { - "description": "Role description for logo artist" - }, - "aboutTranslators": "Tradutores", - "@aboutTranslators": { - "description": "Section for translators" - }, - "aboutSpecialThanks": "Agradecimentos Especiais", - "@aboutSpecialThanks": { - "description": "Section for special thanks" - }, - "aboutLinks": "Links", - "@aboutLinks": { - "description": "Section for external links" - }, - "aboutMobileSource": "Código-fonte do app móvel", - "@aboutMobileSource": { - "description": "Link to mobile GitHub repo" - }, - "aboutPCSource": "Código-fonte do app desktop", - "@aboutPCSource": { - "description": "Link to PC GitHub repo" - }, - "aboutReportIssue": "Reportar um problema", - "@aboutReportIssue": { - "description": "Link to report bugs" - }, - "aboutReportIssueSubtitle": "Reporte qualquer problema que encontrar", - "@aboutReportIssueSubtitle": { - "description": "Subtitle for report issue" - }, - "aboutFeatureRequest": "Solicitação de recurso", - "@aboutFeatureRequest": { - "description": "Link to suggest features" - }, - "aboutFeatureRequestSubtitle": "Sugira novos recursos para o aplicativo", - "@aboutFeatureRequestSubtitle": { - "description": "Subtitle for feature request" - }, - "aboutTelegramChannel": "Canal do Telegram", - "@aboutTelegramChannel": { - "description": "Link to Telegram channel" - }, - "aboutTelegramChannelSubtitle": "Anúncios e atualizações", - "@aboutTelegramChannelSubtitle": { - "description": "Subtitle for Telegram channel" - }, - "aboutTelegramChat": "Comunidade do Telegram", - "@aboutTelegramChat": { - "description": "Link to Telegram chat group" - }, - "aboutTelegramChatSubtitle": "Converse com outros usuários", - "@aboutTelegramChatSubtitle": { - "description": "Subtitle for Telegram chat" - }, - "aboutSocial": "Social", - "@aboutSocial": { - "description": "Section for social links" - }, - "aboutApp": "Aplicativo", - "@aboutApp": { - "description": "Section for app info" - }, - "aboutVersion": "Versão", - "@aboutVersion": { - "description": "Version info label" - }, - "aboutBinimumDesc": "O criador da API QQDL e HiFi. Sem esta API, os downloads Tidal não existiriam!", - "@aboutBinimumDesc": { - "description": "Credit description for binimum" - }, - "aboutSachinsenalDesc": "O criador original do projeto HiFi. A base da integração do Tidal!", - "@aboutSachinsenalDesc": { - "description": "Credit description for sachinsenal0x64" - }, - "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", - "@aboutSjdonadoDesc": { - "description": "Credit description for sjdonado" - }, - "aboutDabMusic": "DAB Music", - "@aboutDabMusic": { - "description": "Name of Qobuz API service - DO NOT TRANSLATE" - }, - "aboutDabMusicDesc": "A melhor API de streaming do Qobuz. Downloads de alta resolução não seriam possíveis sem isso!", - "@aboutDabMusicDesc": { - "description": "Credit for DAB Music API" - }, - "aboutSpotiSaver": "SpotiSaver", - "@aboutSpotiSaver": { - "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" - }, - "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", - "@aboutSpotiSaverDesc": { - "description": "Credit for SpotiSaver API" - }, - "aboutAppDescription": "Baixe faixas do Spotify em qualidade sem perdas do Tidal, Qobuz e Amazon Music.", - "@aboutAppDescription": { - "description": "App description in header card" - }, - "artistAlbums": "Álbuns", - "@artistAlbums": { - "description": "Section header for artist albums" - }, - "artistSingles": "Singles e EPs", - "@artistSingles": { - "description": "Section header for singles/EPs" - }, - "artistCompilations": "Compilações", - "@artistCompilations": { - "description": "Section header for compilations" - }, - "artistPopular": "Populares", - "@artistPopular": { - "description": "Section header for popular/top tracks" - }, - "artistMonthlyListeners": "{count} ouvintes mensais", - "@artistMonthlyListeners": { - "description": "Monthly listener count display", - "placeholders": { - "count": { - "type": "String", - "description": "Formatted listener count" - } - } - }, - "trackMetadataService": "Serviço", - "@trackMetadataService": { - "description": "Metadata field - download service used" - }, - "trackMetadataPlay": "Reproduzir", - "@trackMetadataPlay": { - "description": "Action button - play track" - }, - "trackMetadataShare": "Compartilhar", - "@trackMetadataShare": { - "description": "Action button - share track" - }, - "trackMetadataDelete": "Apagar", - "@trackMetadataDelete": { - "description": "Action button - delete track" - }, - "setupGrantPermission": "Conceder Permissão", - "@setupGrantPermission": { - "description": "Button to request permission" - }, - "setupSkip": "Ignorar por enquanto", - "@setupSkip": { - "description": "Skip current step button" - }, - "setupStorageAccessRequired": "Acesso ao Armazenamento Necessário", - "@setupStorageAccessRequired": { - "description": "Title when storage access needed" - }, - "setupStorageAccessMessageAndroid11": "O Android 11+ requer a permissão \"Acesso a Todos os Arquivos\" para salvar arquivos na pasta de download escolhida.", - "@setupStorageAccessMessageAndroid11": { - "description": "Android 11+ specific explanation" - }, - "setupOpenSettings": "Abrir Configurações", - "@setupOpenSettings": { - "description": "Button to open system settings" - }, - "setupPermissionDeniedMessage": "Permissão negada. Por favor, conceda todas as permissões para continuar.", - "@setupPermissionDeniedMessage": { - "description": "Error when permission denied" - }, - "setupPermissionRequired": "Permissão {permissionType} Necessária", - "@setupPermissionRequired": { - "description": "Generic permission required title", - "placeholders": { - "permissionType": { - "type": "String", - "description": "Type of permission (Storage/Notification)" - } - } - }, - "setupPermissionRequiredMessage": "A permissão {permissionType} é necessária para a melhor experiência. Você pode alterar isso mais tarde em Configurações.", - "@setupPermissionRequiredMessage": { - "description": "Generic permission required message", - "placeholders": { - "permissionType": { - "type": "String" - } - } - }, - "setupUseDefaultFolder": "Usar Pasta Padrão?", - "@setupUseDefaultFolder": { - "description": "Dialog title for default folder" - }, - "setupNoFolderSelected": "Nenhuma pasta selecionada. Você gostaria de usar a pasta padrão de música?", - "@setupNoFolderSelected": { - "description": "Prompt when no folder selected" - }, - "setupUseDefault": "Usar Padrão", - "@setupUseDefault": { - "description": "Button to use default folder" - }, - "setupDownloadLocationTitle": "Local do Download", - "@setupDownloadLocationTitle": { - "description": "Download location dialog title" - }, - "setupDownloadLocationIosMessage": "No iOS, downloads são salvos na pasta Documentos do aplicativo. Você pode acessá-los através do app Arquivos.", - "@setupDownloadLocationIosMessage": { - "description": "iOS-specific folder info" - }, - "setupAppDocumentsFolder": "Pasta Documentos do App", - "@setupAppDocumentsFolder": { - "description": "iOS documents folder option" - }, - "setupAppDocumentsFolderSubtitle": "Recomendado - acessível através do aplicativo Arquivos", - "@setupAppDocumentsFolderSubtitle": { - "description": "Subtitle for documents folder" - }, - "setupChooseFromFiles": "Escolher dos Arquivos", - "@setupChooseFromFiles": { - "description": "iOS file picker option" - }, - "setupChooseFromFilesSubtitle": "Selecione o iCloud ou outro local", - "@setupChooseFromFilesSubtitle": { - "description": "Subtitle for file picker" - }, - "setupIosEmptyFolderWarning": "Limitação do iOS: Pastas vazias não podem ser selecionadas. Escolha uma pasta com pelo menos um arquivo.", - "@setupIosEmptyFolderWarning": { - "description": "iOS folder selection warning" - }, - "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", - "@setupIcloudNotSupported": { - "description": "Error when user selects iCloud Drive on iOS" - }, - "setupDownloadInFlac": "Baixar faixas do Spotify em FLAC", - "@setupDownloadInFlac": { - "description": "App tagline in setup" - }, - "setupStorageGranted": "Permissão de Armazenamento Concedida!", - "@setupStorageGranted": { - "description": "Success message for storage permission" - }, - "setupStorageRequired": "Permissão de Armazenamento Necessária", - "@setupStorageRequired": { - "description": "Title when storage permission needed" - }, - "setupStorageDescription": "O SpotiFLAC precisa de permissão de armazenamento para salvar os seus arquivos de música baixados.", - "@setupStorageDescription": { - "description": "Explanation for storage permission" - }, - "setupNotificationGranted": "Permissão de Notificações Concedida!", - "@setupNotificationGranted": { - "description": "Success message for notification permission" - }, - "setupNotificationEnable": "Habilitar Notificações", - "@setupNotificationEnable": { - "description": "Button to enable notifications" - }, - "setupFolderChoose": "Escolher Pasta de Download", - "@setupFolderChoose": { - "description": "Button to choose folder" - }, - "setupFolderDescription": "Selecione uma pasta onde as suas músicas baixadas serão salvas.", - "@setupFolderDescription": { - "description": "Explanation for folder selection" - }, - "setupSelectFolder": "Seleccionar Pasta", - "@setupSelectFolder": { - "description": "Button to select folder" - }, - "setupEnableNotifications": "Habilitar Notificações", - "@setupEnableNotifications": { - "description": "Button to enable notifications" - }, - "setupNotificationBackgroundDescription": "Seja notificado sobre o progresso e conclusão do download. Isso ajuda você a acompanhar os downloads quando o app estiver em segundo plano.", - "@setupNotificationBackgroundDescription": { - "description": "Detailed notification explanation" - }, - "setupSkipForNow": "Ignorar por enquanto", - "@setupSkipForNow": { - "description": "Skip button text" - }, - "setupNext": "Próximo", - "@setupNext": { - "description": "Next button text" - }, - "setupGetStarted": "Começar", - "@setupGetStarted": { - "description": "Final setup button" - }, - "setupAllowAccessToManageFiles": "Por favor, habilite \"Permitir acesso para gerenciar todos os arquivos\" na próxima tela.", - "@setupAllowAccessToManageFiles": { - "description": "Instruction for file access permission" - }, - "dialogCancel": "Cancelar", - "@dialogCancel": { - "description": "Dialog button - cancel action" - }, - "dialogSave": "Salvar", - "@dialogSave": { - "description": "Dialog button - save changes" - }, - "dialogDelete": "Apagar", - "@dialogDelete": { - "description": "Dialog button - delete item" - }, - "dialogRetry": "Tentar novamente", - "@dialogRetry": { - "description": "Dialog button - retry action" - }, - "dialogClear": "Limpar", - "@dialogClear": { - "description": "Dialog button - clear items" - }, - "dialogDone": "Concluído", - "@dialogDone": { - "description": "Dialog button - action completed" - }, - "dialogImport": "Importar", - "@dialogImport": { - "description": "Dialog button - import data" - }, - "dialogDiscard": "Descartar", - "@dialogDiscard": { - "description": "Dialog button - discard changes" - }, - "dialogRemove": "Remover", - "@dialogRemove": { - "description": "Dialog button - remove item" - }, - "dialogUninstall": "Desinstalar", - "@dialogUninstall": { - "description": "Dialog button - uninstall extension" - }, - "dialogDiscardChanges": "Descartar Alterações?", - "@dialogDiscardChanges": { - "description": "Dialog title - unsaved changes warning" - }, - "dialogUnsavedChanges": "Você tem alterações não salvas. Deseja descartá-las?", - "@dialogUnsavedChanges": { - "description": "Dialog message - unsaved changes" - }, - "dialogClearAll": "Limpar Tudo", - "@dialogClearAll": { - "description": "Dialog title - clear all items" - }, - "dialogRemoveExtension": "Remover Extensão", - "@dialogRemoveExtension": { - "description": "Dialog title - uninstall extension" - }, - "dialogRemoveExtensionMessage": "Tem certeza de que deseja remover esta extensão? Isso não pode ser desfeito.", - "@dialogRemoveExtensionMessage": { - "description": "Dialog message - uninstall confirmation" - }, - "dialogUninstallExtension": "Desinstalar Extensão?", - "@dialogUninstallExtension": { - "description": "Dialog title - uninstall extension" - }, - "dialogUninstallExtensionMessage": "Tem certeza que deseja remover {extensionName}?", - "@dialogUninstallExtensionMessage": { - "description": "Dialog message - uninstall specific extension", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "dialogClearHistoryTitle": "Limpar Histórico", - "@dialogClearHistoryTitle": { - "description": "Dialog title - clear download history" - }, - "dialogClearHistoryMessage": "Tem certeza que deseja limpar todo o histórico de downloads? Isso não pode ser desfeito.", - "@dialogClearHistoryMessage": { - "description": "Dialog message - clear history confirmation" - }, - "dialogDeleteSelectedTitle": "Apagar Selecionados", - "@dialogDeleteSelectedTitle": { - "description": "Dialog title - delete selected items" - }, - "dialogDeleteSelectedMessage": "Apagar {count} {count, plural, one {}=1{faixa} other{faixas}} do histórico?\n\nIsso também apagará os arquivos do armazenamento.", - "@dialogDeleteSelectedMessage": { - "description": "Dialog message - delete selected tracks", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dialogImportPlaylistTitle": "Importar Playlist", - "@dialogImportPlaylistTitle": { - "description": "Dialog title - import CSV playlist" - }, - "dialogImportPlaylistMessage": "{count} Faixas encontradas em CSV. Adicioná-las à lista de downloads?", - "csvImportTracks": "{count} faixas do CSV", - "@csvImportTracks": { - "description": "Label shown in quality picker for CSV import", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@dialogImportPlaylistMessage": { - "description": "Dialog message - import playlist confirmation", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarAddedToQueue": "\"{trackName}\" adicionada à fila", - "@snackbarAddedToQueue": { - "description": "Snackbar - track added to download queue", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarAddedTracksToQueue": "{count} faixas adicionadas à fila", - "@snackbarAddedTracksToQueue": { - "description": "Snackbar - multiple tracks added to queue", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarAlreadyDownloaded": "\"{trackName}\" já foi baixada", - "@snackbarAlreadyDownloaded": { - "description": "Snackbar - track already exists", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", - "@snackbarAlreadyInLibrary": { - "description": "Snackbar - track already exists in local library", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarHistoryCleared": "Histórico limpo", - "@snackbarHistoryCleared": { - "description": "Snackbar - history deleted" - }, - "snackbarCredentialsSaved": "Credenciais salvas", - "@snackbarCredentialsSaved": { - "description": "Snackbar - Spotify credentials saved" - }, - "snackbarCredentialsCleared": "Credenciais limpas", - "@snackbarCredentialsCleared": { - "description": "Snackbar - Spotify credentials removed" - }, - "snackbarDeletedTracks": "{count} {count, plural, one {}=1{faixa apagada} other{faixas apagadas}}", - "@snackbarDeletedTracks": { - "description": "Snackbar - tracks deleted", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarCannotOpenFile": "Não foi possível abrir o arquivo: {error}", - "@snackbarCannotOpenFile": { - "description": "Snackbar - file open error", - "placeholders": { - "error": { - "type": "String" - } - } - }, - "snackbarFillAllFields": "Por favor, preencha todos os campos", - "@snackbarFillAllFields": { - "description": "Snackbar - validation error" - }, - "snackbarViewQueue": "Ver Fila", - "@snackbarViewQueue": { - "description": "Snackbar action - view download queue" - }, - "snackbarUrlCopied": "URL do {platform} copiado para a área de transferência", - "@snackbarUrlCopied": { - "description": "Snackbar - URL copied", - "placeholders": { - "platform": { - "type": "String", - "description": "Platform name (Spotify/Deezer)" - } - } - }, - "snackbarFileNotFound": "Arquivo não encontrado", - "@snackbarFileNotFound": { - "description": "Snackbar - file doesn't exist" - }, - "snackbarSelectExtFile": "Por favor, selecione um arquivo .spotiflac-ext", - "@snackbarSelectExtFile": { - "description": "Snackbar - wrong file type selected" - }, - "snackbarProviderPrioritySaved": "Prioridade de provedor salva", - "@snackbarProviderPrioritySaved": { - "description": "Snackbar - provider order saved" - }, - "snackbarMetadataProviderSaved": "Prioridade do provedor de metadados salva", - "@snackbarMetadataProviderSaved": { - "description": "Snackbar - metadata provider order saved" - }, - "snackbarExtensionInstalled": "{extensionName} instalada.", - "@snackbarExtensionInstalled": { - "description": "Snackbar - extension installed successfully", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "snackbarExtensionUpdated": "{extensionName} atualizada.", - "@snackbarExtensionUpdated": { - "description": "Snackbar - extension updated successfully", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "snackbarFailedToInstall": "Falha ao instalar a extensão", - "@snackbarFailedToInstall": { - "description": "Snackbar - extension install error" - }, - "snackbarFailedToUpdate": "Falha ao atualizar a extensão", - "@snackbarFailedToUpdate": { - "description": "Snackbar - extension update error" - }, - "errorRateLimited": "Tráfico Limitado (Rate Limited)", - "@errorRateLimited": { - "description": "Error title - too many requests" - }, - "errorRateLimitedMessage": "Muitas solicitações. Por favor, aguarde um momento antes de pesquisar novamente.", - "@errorRateLimitedMessage": { - "description": "Error message - rate limit explanation" - }, - "errorNoTracksFound": "Nenhuma faixa encontrada", - "@errorNoTracksFound": { - "description": "Error - search returned no results" - }, - "errorMissingExtensionSource": "Não é possível carregar {item}: faltando a fonte da extensão", - "@errorMissingExtensionSource": { - "description": "Error - extension source not available", - "placeholders": { - "item": { - "type": "String" - } - } - }, - "actionPause": "Pausar", - "@actionPause": { - "description": "Action button - pause download" - }, - "actionResume": "Retomar", - "@actionResume": { - "description": "Action button - resume download" - }, - "actionCancel": "Cancelar", - "@actionCancel": { - "description": "Action button - cancel operation" - }, - "actionSelectAll": "Selecionar Tudo", - "@actionSelectAll": { - "description": "Action button - select all items" - }, - "actionDeselect": "Desselecionar", - "@actionDeselect": { - "description": "Action button - deselect all" - }, - "actionRemoveCredentials": "Remover Credenciais", - "@actionRemoveCredentials": { - "description": "Action button - delete Spotify credentials" - }, - "actionSaveCredentials": "Salvar Credenciais", - "@actionSaveCredentials": { - "description": "Action button - save Spotify credentials" - }, - "selectionSelected": "{count} selecionado(s)", - "@selectionSelected": { - "description": "Selection count indicator", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionAllSelected": "Todas as faixas selecionadas", - "@selectionAllSelected": { - "description": "Status - all items selected" - }, - "selectionSelectToDelete": "Selecione as faixas para apagar", - "@selectionSelectToDelete": { - "description": "Placeholder when nothing selected" - }, - "progressFetchingMetadata": "Buscando metadados... {current}/{total}", - "@progressFetchingMetadata": { - "description": "Progress indicator - loading track info", - "placeholders": { - "current": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "progressReadingCsv": "Lendo CSV...", - "@progressReadingCsv": { - "description": "Progress indicator - parsing CSV file" - }, - "searchSongs": "Músicas", - "@searchSongs": { - "description": "Search result category - songs" - }, - "searchArtists": "Artistas", - "@searchArtists": { - "description": "Search result category - artists" - }, - "searchAlbums": "Álbuns", - "@searchAlbums": { - "description": "Search result category - albums" - }, - "searchPlaylists": "Playlists", - "@searchPlaylists": { - "description": "Search result category - playlists" - }, - "tooltipPlay": "Reproduzir", - "@tooltipPlay": { - "description": "Tooltip - play button" - }, - "filenameFormat": "Formato do Nome do Arquivo", - "@filenameFormat": { - "description": "Setting title - filename pattern" - }, - "filenameShowAdvancedTags": "Show advanced tags", - "@filenameShowAdvancedTags": { - "description": "Toggle label for showing advanced filename tags" - }, - "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns", - "@filenameShowAdvancedTagsDescription": { - "description": "Description for advanced filename tag toggle" - }, - "folderOrganizationNone": "Nenhuma organização", - "@folderOrganizationNone": { - "description": "Folder option - flat structure" - }, - "folderOrganizationByArtist": "Por Artista", - "@folderOrganizationByArtist": { - "description": "Folder option - artist folders" - }, - "folderOrganizationByAlbum": "Por Album", - "@folderOrganizationByAlbum": { - "description": "Folder option - album folders" - }, - "folderOrganizationByArtistAlbum": "Artista/Álbum", - "@folderOrganizationByArtistAlbum": { - "description": "Folder option - nested folders" - }, - "folderOrganizationDescription": "Organizar arquivos baixados em pastas", - "@folderOrganizationDescription": { - "description": "Folder organization sheet description" - }, - "folderOrganizationNoneSubtitle": "Todos os arquivos na pasta de download", - "@folderOrganizationNoneSubtitle": { - "description": "Subtitle for no organization option" - }, - "folderOrganizationByArtistSubtitle": "Pasta separada para cada artista", - "@folderOrganizationByArtistSubtitle": { - "description": "Subtitle for artist folder option" - }, - "folderOrganizationByAlbumSubtitle": "Pasta separada para cada álbum", - "@folderOrganizationByAlbumSubtitle": { - "description": "Subtitle for album folder option" - }, - "folderOrganizationByArtistAlbumSubtitle": "Pastas aninhadas para artista e álbum", - "@folderOrganizationByArtistAlbumSubtitle": { - "description": "Subtitle for nested folder option" - }, - "updateAvailable": "Atualização Disponível", - "@updateAvailable": { - "description": "Update dialog title" - }, - "updateLater": "Depois", - "@updateLater": { - "description": "Update button - dismiss" - }, - "updateStartingDownload": "Iniciando download...", - "@updateStartingDownload": { - "description": "Update status - initializing" - }, - "updateDownloadFailed": "Download falhou", - "@updateDownloadFailed": { - "description": "Update error title" - }, - "updateFailedMessage": "Falha ao baixar a atualização", - "@updateFailedMessage": { - "description": "Update error message" - }, - "updateNewVersionReady": "Uma nova versão está pronta", - "@updateNewVersionReady": { - "description": "Update subtitle" - }, - "updateCurrent": "Atual", - "@updateCurrent": { - "description": "Label for current version" - }, - "updateNew": "Novo", - "@updateNew": { - "description": "Label for new version" - }, - "updateDownloading": "Baixando...", - "@updateDownloading": { - "description": "Update status - downloading" - }, - "updateWhatsNew": "Novidades", - "@updateWhatsNew": { - "description": "Changelog section title" - }, - "updateDownloadInstall": "Baixar e Instalar", - "@updateDownloadInstall": { - "description": "Update button - download and install" - }, - "updateDontRemind": "Não lembrar", - "@updateDontRemind": { - "description": "Update button - skip this version" - }, - "providerPriorityTitle": "Prioridade de Provedor", - "@providerPriorityTitle": { - "description": "Provider priority page title" - }, - "providerPriorityDescription": "Arraste para reordenar provedores de download. O aplicativo irá tentar provedores de cima para baixo ao baixar as faixas.", - "@providerPriorityDescription": { - "description": "Provider priority page description" - }, - "providerPriorityInfo": "Se uma faixa não estiver disponível no primeiro provedor, o aplicativo irá tentar automaticamente a próxima.", - "@providerPriorityInfo": { - "description": "Info tip about fallback behavior" - }, - "providerBuiltIn": "Embutido", - "@providerBuiltIn": { - "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" - }, - "providerExtension": "Extensão", - "@providerExtension": { - "description": "Label for extension-provided providers" - }, - "metadataProviderPriorityTitle": "Prioridade de Metadados", - "@metadataProviderPriorityTitle": { - "description": "Metadata priority page title" - }, - "metadataProviderPriorityDescription": "Arraste para reordenar provedores de metadados. O aplicativo tentará provedores de cima para baixo ao procurar por faixas e buscar metadados.", - "@metadataProviderPriorityDescription": { - "description": "Metadata priority page description" - }, - "metadataProviderPriorityInfo": "O Deezer não tem limites de taxa e é recomendado como principal. O Spotify pode limitar a taxa após muitas solicitações.", - "@metadataProviderPriorityInfo": { - "description": "Info tip about rate limits" - }, - "metadataNoRateLimits": "Sem limites de taxa", - "@metadataNoRateLimits": { - "description": "Deezer provider description" - }, - "metadataMayRateLimit": "Pode ter limites de taxa", - "@metadataMayRateLimit": { - "description": "Spotify provider description" - }, - "logTitle": "Registros", - "@logTitle": { - "description": "Logs screen title" - }, - "logCopied": "Registros copiados para área de transferência", - "@logCopied": { - "description": "Snackbar - logs copied" - }, - "logSearchHint": "Pesquisar registros...", - "@logSearchHint": { - "description": "Log search placeholder" - }, - "logFilterLevel": "Nível", - "@logFilterLevel": { - "description": "Filter by log level" - }, - "logFilterSection": "Filtro", - "@logFilterSection": { - "description": "Filter section title" - }, - "logShareLogs": "Compartilhar registros", - "@logShareLogs": { - "description": "Share button tooltip" - }, - "logClearLogs": "Limpar registros", - "@logClearLogs": { - "description": "Clear button tooltip" - }, - "logClearLogsTitle": "Limpar Registros", - "@logClearLogsTitle": { - "description": "Clear logs dialog title" - }, - "logClearLogsMessage": "Tem certeza de que deseja limpar todos os registros?", - "@logClearLogsMessage": { - "description": "Clear logs confirmation message" - }, - "logFilterBySeverity": "Filtrar registros por gravidade", - "@logFilterBySeverity": { - "description": "Filter dialog title" - }, - "logNoLogsYet": "Ainda não há registros", - "@logNoLogsYet": { - "description": "Empty state title" - }, - "logNoLogsYetSubtitle": "Os registros aparecerão aqui enquanto você usa o aplicativo", - "@logNoLogsYetSubtitle": { - "description": "Empty state subtitle" - }, - "logEntriesFiltered": "Entradas ({count} filtradas)", - "@logEntriesFiltered": { - "description": "Log count with filter active", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "logEntries": "Entradas ({count})", - "@logEntries": { - "description": "Total log count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "credentialsTitle": "Credenciais do Spotify", - "@credentialsTitle": { - "description": "Credentials dialog title" - }, - "credentialsDescription": "Digite a sua Client ID e Secret para usar a sua própria cota de aplicativo do Spotify.", - "@credentialsDescription": { - "description": "Credentials dialog explanation" - }, - "credentialsClientId": "Client ID", - "@credentialsClientId": { - "description": "Client ID field label - DO NOT TRANSLATE" - }, - "credentialsClientIdHint": "Colar Client ID", - "@credentialsClientIdHint": { - "description": "Client ID placeholder" - }, - "credentialsClientSecret": "Client Secret", - "@credentialsClientSecret": { - "description": "Client Secret field label - DO NOT TRANSLATE" - }, - "credentialsClientSecretHint": "Colar Client Secret", - "@credentialsClientSecretHint": { - "description": "Client Secret placeholder" - }, - "channelStable": "Estável", - "@channelStable": { - "description": "Update channel - stable releases" - }, - "channelPreview": "Prévia", - "@channelPreview": { - "description": "Update channel - beta/preview releases" - }, - "sectionSearchSource": "Origem da Pesquisa", - "@sectionSearchSource": { - "description": "Settings section header" - }, - "sectionDownload": "Download", - "@sectionDownload": { - "description": "Settings section header" - }, - "sectionPerformance": "Desempenho", - "@sectionPerformance": { - "description": "Settings section header" - }, - "sectionApp": "Aplicativo", - "@sectionApp": { - "description": "Settings section header" - }, - "sectionData": "Dados", - "@sectionData": { - "description": "Settings section header" - }, - "sectionDebug": "Depuração", - "@sectionDebug": { - "description": "Settings section header" - }, - "sectionService": "Serviço", - "@sectionService": { - "description": "Settings section header" - }, - "sectionAudioQuality": "Qualidade de Áudio", - "@sectionAudioQuality": { - "description": "Settings section header" - }, - "sectionFileSettings": "Configurações de Arquivo", - "@sectionFileSettings": { - "description": "Settings section header" - }, - "sectionLyrics": "Letras", - "@sectionLyrics": { - "description": "Settings section header" - }, - "lyricsMode": "Modo de Letras", - "@lyricsMode": { - "description": "Setting - how to save lyrics" - }, - "lyricsModeDescription": "Escolha como as letras são salvas com os seus downloads", - "@lyricsModeDescription": { - "description": "Lyrics mode picker description" - }, - "lyricsModeEmbed": "Incorporar no arquivo", - "@lyricsModeEmbed": { - "description": "Lyrics mode option - embed in audio file" - }, - "lyricsModeEmbedSubtitle": "Letra armazenada nos metadados da FLAC", - "@lyricsModeEmbedSubtitle": { - "description": "Subtitle for embed option" - }, - "lyricsModeExternal": "Arquivo .lrc externo", - "@lyricsModeExternal": { - "description": "Lyrics mode option - separate LRC file" - }, - "lyricsModeExternalSubtitle": "Arquivo .lrc separado para reprodutores como o Samsung Music", - "@lyricsModeExternalSubtitle": { - "description": "Subtitle for external option" - }, - "lyricsModeBoth": "Ambos", - "@lyricsModeBoth": { - "description": "Lyrics mode option - embed and external" - }, - "lyricsModeBothSubtitle": "Incorporar e salvar arquivo .lrc", - "@lyricsModeBothSubtitle": { - "description": "Subtitle for both option" - }, - "sectionColor": "Cor", - "@sectionColor": { - "description": "Settings section header" - }, - "sectionTheme": "Tema", - "@sectionTheme": { - "description": "Settings section header" - }, - "sectionLayout": "Layout", - "@sectionLayout": { - "description": "Settings section header" - }, - "sectionLanguage": "Idioma", - "@sectionLanguage": { - "description": "Settings section header for language" - }, - "appearanceLanguage": "Idioma do aplicativo", - "@appearanceLanguage": { - "description": "Language setting title" - }, - "settingsAppearanceSubtitle": "Tema, cores, exibição", - "@settingsAppearanceSubtitle": { - "description": "Appearance settings description" - }, - "settingsDownloadSubtitle": "Serviço, qualidade, formato de nome de arquivo", - "@settingsDownloadSubtitle": { - "description": "Download settings description" - }, - "settingsOptionsSubtitle": "Fallback, letras, arte de capa, atualizações", - "@settingsOptionsSubtitle": { - "description": "Options settings description" - }, - "settingsExtensionsSubtitle": "Gerenciar provedores de download", - "@settingsExtensionsSubtitle": { - "description": "Extensions settings description" - }, - "settingsLogsSubtitle": "Ver logs do app para depuração", - "@settingsLogsSubtitle": { - "description": "Logs settings description" - }, - "loadingSharedLink": "Carregando link compartilhado...", - "@loadingSharedLink": { - "description": "Status when opening shared URL" - }, - "pressBackAgainToExit": "Pressione voltar novamente para sair", - "@pressBackAgainToExit": { - "description": "Exit confirmation message" - }, - "downloadAllCount": "Baixar Todos ({count})", - "@downloadAllCount": { - "description": "Download all button with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "tracksCount": "{count, plural, one {}=1{1 faixa} other{{count} faixas}}", - "@tracksCount": { - "description": "Track count display", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "trackCopyFilePath": "Copiar caminho do arquivo", - "@trackCopyFilePath": { - "description": "Action - copy file path" - }, - "trackRemoveFromDevice": "Remover do dispositivo", - "@trackRemoveFromDevice": { - "description": "Action - delete downloaded file" - }, - "trackLoadLyrics": "Carregar Letras", - "@trackLoadLyrics": { - "description": "Action - fetch lyrics" - }, - "trackMetadata": "Metadados", - "@trackMetadata": { - "description": "Tab title - track metadata" - }, - "trackFileInfo": "Informações do Arquivo", - "@trackFileInfo": { - "description": "Tab title - file information" - }, - "trackLyrics": "Letras", - "@trackLyrics": { - "description": "Tab title - lyrics" - }, - "trackFileNotFound": "Arquivo não encontrado", - "@trackFileNotFound": { - "description": "Error - file doesn't exist" - }, - "trackOpenInDeezer": "Abrir no Deezer", - "@trackOpenInDeezer": { - "description": "Action - open track in Deezer app" - }, - "trackOpenInSpotify": "Abrir no Spotify", - "@trackOpenInSpotify": { - "description": "Action - open track in Spotify app" - }, - "trackTrackName": "Nome da faixa", - "@trackTrackName": { - "description": "Metadata label - track title" - }, - "trackArtist": "Artista", - "@trackArtist": { - "description": "Metadata label - artist name" - }, - "trackAlbumArtist": "Artista do álbum", - "@trackAlbumArtist": { - "description": "Metadata label - album artist" - }, - "trackAlbum": "Álbum", - "@trackAlbum": { - "description": "Metadata label - album name" - }, - "trackTrackNumber": "Número da faixa", - "@trackTrackNumber": { - "description": "Metadata label - track number" - }, - "trackDiscNumber": "Número do disco", - "@trackDiscNumber": { - "description": "Metadata label - disc number" - }, - "trackDuration": "Duração", - "@trackDuration": { - "description": "Metadata label - track length" - }, - "trackAudioQuality": "Qualidade de Áudio", - "@trackAudioQuality": { - "description": "Metadata label - audio quality" - }, - "trackReleaseDate": "Data de lançamento", - "@trackReleaseDate": { - "description": "Metadata label - release date" - }, - "trackGenre": "Género", - "@trackGenre": { - "description": "Metadata label - music genre" - }, - "trackLabel": "Gravadora", - "@trackLabel": { - "description": "Metadata label - record label" - }, - "trackCopyright": "Direitos Autorais", - "@trackCopyright": { - "description": "Metadata label - copyright information" - }, - "trackDownloaded": "Baixado", - "@trackDownloaded": { - "description": "Metadata label - download date" - }, - "trackCopyLyrics": "Copiar letra", - "@trackCopyLyrics": { - "description": "Action - copy lyrics to clipboard" - }, - "trackLyricsNotAvailable": "Letra não disponível para esta faixa", - "@trackLyricsNotAvailable": { - "description": "Message when lyrics not found" - }, - "trackLyricsTimeout": "A solicitação expirou. Tente novamente mais tarde.", - "@trackLyricsTimeout": { - "description": "Message when lyrics request times out" - }, - "trackLyricsLoadFailed": "Falha ao carregar a letra", - "@trackLyricsLoadFailed": { - "description": "Message when lyrics loading fails" - }, - "trackEmbedLyrics": "Incorporar Letras", - "@trackEmbedLyrics": { - "description": "Action - embed lyrics into audio file" - }, - "trackLyricsEmbedded": "Letras incorporadas com sucesso", - "@trackLyricsEmbedded": { - "description": "Snackbar - lyrics saved to file" - }, - "trackInstrumental": "Faixa de instrumentais", - "@trackInstrumental": { - "description": "Message when track is instrumental (no lyrics)" - }, - "trackCopiedToClipboard": "Copiado para a área de transferência", - "@trackCopiedToClipboard": { - "description": "Snackbar - content copied" - }, - "trackDeleteConfirmTitle": "Remover do dispositivo?", - "@trackDeleteConfirmTitle": { - "description": "Delete confirmation title" - }, - "trackDeleteConfirmMessage": "Isto irá excluir o arquivo baixado permanentemente e removê-lo do seu histórico.", - "@trackDeleteConfirmMessage": { - "description": "Delete confirmation message" - }, - "dateToday": "Hoje", - "@dateToday": { - "description": "Relative date - today" - }, - "dateYesterday": "Ontem", - "@dateYesterday": { - "description": "Relative date - yesterday" - }, - "dateDaysAgo": "{count} dias atrás", - "@dateDaysAgo": { - "description": "Relative date - days ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dateWeeksAgo": "{count} semanas atrás", - "@dateWeeksAgo": { - "description": "Relative date - weeks ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dateMonthsAgo": "{count} meses atrás", - "@dateMonthsAgo": { - "description": "Relative date - months ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "storeFilterAll": "Tudo", - "@storeFilterAll": { - "description": "Store filter - all extensions" - }, - "storeFilterMetadata": "Metadados", - "@storeFilterMetadata": { - "description": "Store filter - metadata providers" - }, - "storeFilterDownload": "Download", - "@storeFilterDownload": { - "description": "Store filter - download providers" - }, - "storeFilterUtility": "Utilidade", - "@storeFilterUtility": { - "description": "Store filter - utility extensions" - }, - "storeFilterLyrics": "Letras", - "@storeFilterLyrics": { - "description": "Store filter - lyrics providers" - }, - "storeFilterIntegration": "Integração", - "@storeFilterIntegration": { - "description": "Store filter - integrations" - }, - "storeClearFilters": "Limpar filtros", - "@storeClearFilters": { - "description": "Button to clear all filters" - }, - "extensionDefaultProvider": "Padrão (Deezer/Spotify)", - "@extensionDefaultProvider": { - "description": "Default search provider option" - }, - "extensionDefaultProviderSubtitle": "Usar pesquisa integrada", - "@extensionDefaultProviderSubtitle": { - "description": "Subtitle for default provider" - }, - "extensionAuthor": "Autor", - "@extensionAuthor": { - "description": "Extension detail - author" - }, - "extensionId": "ID", - "@extensionId": { - "description": "Extension detail - unique ID" - }, - "extensionError": "Erro", - "@extensionError": { - "description": "Extension detail - error message" - }, - "extensionCapabilities": "Funcionalidades", - "@extensionCapabilities": { - "description": "Section header - extension features" - }, - "extensionMetadataProvider": "Provedor de Metadados", - "@extensionMetadataProvider": { - "description": "Capability - provides metadata" - }, - "extensionDownloadProvider": "Provedor de Download", - "@extensionDownloadProvider": { - "description": "Capability - provides downloads" - }, - "extensionLyricsProvider": "Provedor de Letras", - "@extensionLyricsProvider": { - "description": "Capability - provides lyrics" - }, - "extensionUrlHandler": "Gerenciador de URL", - "@extensionUrlHandler": { - "description": "Capability - handles URLs" - }, - "extensionQualityOptions": "Opções de Qualidade", - "@extensionQualityOptions": { - "description": "Capability - quality selection" - }, - "extensionPostProcessingHooks": "Ganchos de Pós-Processamento", - "@extensionPostProcessingHooks": { - "description": "Capability - post-processing" - }, - "extensionPermissions": "Permissões", - "@extensionPermissions": { - "description": "Section header - required permissions" - }, - "extensionSettings": "Configurações", - "@extensionSettings": { - "description": "Section header - extension settings" - }, - "extensionRemoveButton": "Remover Extensão", - "@extensionRemoveButton": { - "description": "Button to uninstall extension" - }, - "extensionUpdated": "Atualizado", - "@extensionUpdated": { - "description": "Extension detail - last update" - }, - "extensionMinAppVersion": "Versão Mínima do App", - "@extensionMinAppVersion": { - "description": "Extension detail - minimum app version" - }, - "extensionCustomTrackMatching": "Correspondência de Faixa Personalizada", - "@extensionCustomTrackMatching": { - "description": "Capability - custom track matching algorithm" - }, - "extensionPostProcessing": "Pós-Processamento", - "@extensionPostProcessing": { - "description": "Capability - post-download processing" - }, - "extensionHooksAvailable": "{count} gancho(s) disponíveis", - "@extensionHooksAvailable": { - "description": "Post-processing hooks count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "extensionPatternsCount": "{count} padrão(ões)", - "@extensionPatternsCount": { - "description": "URL patterns count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "extensionStrategy": "Estratégia: {strategy}", - "@extensionStrategy": { - "description": "Track matching strategy name", - "placeholders": { - "strategy": { - "type": "String" - } - } - }, - "extensionsProviderPrioritySection": "Prioridade de Provedor", - "@extensionsProviderPrioritySection": { - "description": "Section header - provider priority" - }, - "extensionsInstalledSection": "Extensões Instaladas", - "@extensionsInstalledSection": { - "description": "Section header - installed extensions" - }, - "extensionsNoExtensions": "Nenhuma extensão instalada", - "@extensionsNoExtensions": { - "description": "Empty state - no extensions" - }, - "extensionsNoExtensionsSubtitle": "Instale arquivos .spotiflac-ext para adicionar novos provedores", - "@extensionsNoExtensionsSubtitle": { - "description": "Empty state subtitle" - }, - "extensionsInstallButton": "Instalar Extensão", - "@extensionsInstallButton": { - "description": "Button to install extension from file" - }, - "extensionsInfoTip": "Extensões podem adicionar novos metadados e baixar provedores. Somente instale extensões a partir de fontes confiáveis.", - "@extensionsInfoTip": { - "description": "Security warning about extensions" - }, - "extensionsInstalledSuccess": "Extensão instalada com sucesso", - "@extensionsInstalledSuccess": { - "description": "Success message after install" - }, - "extensionsDownloadPriority": "Prioridade de Download", - "@extensionsDownloadPriority": { - "description": "Setting - download provider order" - }, - "extensionsDownloadPrioritySubtitle": "Definir ordem do serviço de download", - "@extensionsDownloadPrioritySubtitle": { - "description": "Subtitle for download priority" - }, - "extensionsNoDownloadProvider": "Nenhuma extensão com provedor de download", - "@extensionsNoDownloadProvider": { - "description": "Empty state - no download providers" - }, - "extensionsMetadataPriority": "Prioridade de Metadados", - "@extensionsMetadataPriority": { - "description": "Setting - metadata provider order" - }, - "extensionsMetadataPrioritySubtitle": "Definir ordem de origem de pesquisa e metadados", - "@extensionsMetadataPrioritySubtitle": { - "description": "Subtitle for metadata priority" - }, - "extensionsNoMetadataProvider": "Nenhuma extensão com provedor de metadados", - "@extensionsNoMetadataProvider": { - "description": "Empty state - no metadata providers" - }, - "extensionsSearchProvider": "Provedor de Pesquisa", - "@extensionsSearchProvider": { - "description": "Setting - search provider selection" - }, - "extensionsNoCustomSearch": "Nenhuma extensão com pesquisa personalizada", - "@extensionsNoCustomSearch": { - "description": "Empty state - no search providers" - }, - "extensionsSearchProviderDescription": "Escolha qual serviço utilizar para pesquisar faixas", - "@extensionsSearchProviderDescription": { - "description": "Search provider setting description" - }, - "extensionsCustomSearch": "Busca personalizada", - "@extensionsCustomSearch": { - "description": "Label for custom search provider" - }, - "extensionsErrorLoading": "Erro ao carregar extensão", - "@extensionsErrorLoading": { - "description": "Error message when extension fails to load" - }, - "qualityFlacLossless": "FLAC Lossless", - "@qualityFlacLossless": { - "description": "Quality option - CD quality FLAC" - }, - "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", - "@qualityFlacLosslessSubtitle": { - "description": "Technical spec for lossless" - }, - "qualityHiResFlac": "Hi-Res FLAC", - "@qualityHiResFlac": { - "description": "Quality option - high resolution FLAC" - }, - "qualityHiResFlacSubtitle": "24-bit / até 96kHz", - "@qualityHiResFlacSubtitle": { - "description": "Technical spec for hi-res" - }, - "qualityHiResFlacMax": "Hi-Res FLAC Max", - "@qualityHiResFlacMax": { - "description": "Quality option - maximum resolution FLAC" - }, - "qualityHiResFlacMaxSubtitle": "24-bit / até 192kHz", - "@qualityHiResFlacMaxSubtitle": { - "description": "Technical spec for hi-res max" - }, - "qualityNote": "A qualidade real depende da faixa que estiver disponível no serviço", - "@qualityNote": { - "description": "Note about quality availability" - }, - "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", - "@youtubeQualityNote": { - "description": "Note for YouTube service explaining lossy-only quality" - }, - "youtubeOpusBitrateTitle": "YouTube Opus Bitrate", - "@youtubeOpusBitrateTitle": { - "description": "Title for YouTube Opus bitrate setting" - }, - "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate", - "@youtubeMp3BitrateTitle": { - "description": "Title for YouTube MP3 bitrate setting" - }, - "downloadAskBeforeDownload": "Perguntar qualidade antes de baixar", - "@downloadAskBeforeDownload": { - "description": "Setting - show quality picker" - }, - "downloadDirectory": "Pasta de Download", - "@downloadDirectory": { - "description": "Setting - download folder" - }, - "downloadSeparateSinglesFolder": "Pasta de Singles Separada", - "@downloadSeparateSinglesFolder": { - "description": "Setting - separate folder for singles" - }, - "downloadAlbumFolderStructure": "Estrutura da Pasta de Álbum", - "@downloadAlbumFolderStructure": { - "description": "Setting - album folder organization" - }, - "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", - "@downloadUseAlbumArtistForFolders": { - "description": "Setting - choose whether artist folders use Album Artist or Track Artist" - }, - "downloadUsePrimaryArtistOnly": "Primary artist only for folders", - "@downloadUsePrimaryArtistOnly": { - "description": "Setting - strip featured artists from folder name" - }, - "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", - "@downloadUsePrimaryArtistOnlyEnabled": { - "description": "Subtitle when primary artist only is enabled" - }, - "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", - "@downloadUsePrimaryArtistOnlyDisabled": { - "description": "Subtitle when primary artist only is disabled" - }, - "downloadSelectQuality": "Selecionar Qualidade", - "@downloadSelectQuality": { - "description": "Dialog title - choose audio quality" - }, - "downloadFrom": "Baixar De", - "@downloadFrom": { - "description": "Label - download source" - }, - "appearanceAmoledDark": "Escuro AMOLED", - "@appearanceAmoledDark": { - "description": "Theme option - pure black" - }, - "appearanceAmoledDarkSubtitle": "Fundo preto puro", - "@appearanceAmoledDarkSubtitle": { - "description": "Subtitle for AMOLED dark" - }, - "queueClearAll": "Limpar Tudo", - "@queueClearAll": { - "description": "Button - clear all queue items" - }, - "queueClearAllMessage": "Você tem certeza que deseja limpar todos os downloads?", - "@queueClearAllMessage": { - "description": "Clear queue confirmation" - }, - "settingsAutoExportFailed": "Auto-export failed downloads", - "@settingsAutoExportFailed": { - "description": "Setting toggle for auto-export" - }, - "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", - "@settingsAutoExportFailedSubtitle": { - "description": "Subtitle for auto-export setting" - }, - "settingsDownloadNetwork": "Download Network", - "@settingsDownloadNetwork": { - "description": "Setting for network type preference" - }, - "settingsDownloadNetworkAny": "WiFi + Mobile Data", - "@settingsDownloadNetworkAny": { - "description": "Network option - use any connection" - }, - "settingsDownloadNetworkWifiOnly": "WiFi Only", - "@settingsDownloadNetworkWifiOnly": { - "description": "Network option - only use WiFi" - }, - "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", - "@settingsDownloadNetworkSubtitle": { - "description": "Subtitle explaining network preference" - }, - "albumFolderArtistAlbum": "Artista / Álbum", - "@albumFolderArtistAlbum": { - "description": "Album folder option" - }, - "albumFolderArtistAlbumSubtitle": "Álbuns/Nome do Artista/Nome do Álbum/", - "@albumFolderArtistAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderArtistYearAlbum": "Artista / [Ano] Álbum", - "@albumFolderArtistYearAlbum": { - "description": "Album folder option with year" - }, - "albumFolderArtistYearAlbumSubtitle": "Álbuns/Nome do Artista/[2005] Nome do Álbum/", - "@albumFolderArtistYearAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderAlbumOnly": "Somente Álbum", - "@albumFolderAlbumOnly": { - "description": "Album folder option" - }, - "albumFolderAlbumOnlySubtitle": "Albums/Nome do Álbum/", - "@albumFolderAlbumOnlySubtitle": { - "description": "Folder structure example" - }, - "albumFolderYearAlbum": "[Ano] Álbum", - "@albumFolderYearAlbum": { - "description": "Album folder option with year" - }, - "albumFolderYearAlbumSubtitle": "Álbuns/[2005] Nome do Álbum/", - "@albumFolderYearAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderArtistAlbumSingles": "Artista / Álbum + Singles", - "@albumFolderArtistAlbumSingles": { - "description": "Album folder option with singles inside artist" - }, - "albumFolderArtistAlbumSinglesSubtitle": "Artista/Álbum/ e Artista/Singles/", - "@albumFolderArtistAlbumSinglesSubtitle": { - "description": "Folder structure example" - }, - "downloadedAlbumDeleteSelected": "Apagar Selecionados", - "@downloadedAlbumDeleteSelected": { - "description": "Button - delete selected tracks" - }, - "downloadedAlbumDeleteMessage": "Excluir {count} {count, plural, one {}=1{faixa} other{faixas}} deste álbum?\n\nIsso também excluirá os arquivos do armazenamento.", - "@downloadedAlbumDeleteMessage": { - "description": "Delete confirmation with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumSelectedCount": "{count} selecionado(s)", - "@downloadedAlbumSelectedCount": { - "description": "Selection count indicator", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumAllSelected": "Todas as faixas selecionadas", - "@downloadedAlbumAllSelected": { - "description": "Status - all items selected" - }, - "downloadedAlbumTapToSelect": "Toque nas faixas para selecionar", - "@downloadedAlbumTapToSelect": { - "description": "Selection hint" - }, - "downloadedAlbumDeleteCount": "Apagar {count} {count, plural, one {}=1{faixa} other{faixas}}", - "@downloadedAlbumDeleteCount": { - "description": "Delete button text with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumSelectToDelete": "Selecione as faixas para apagar", - "@downloadedAlbumSelectToDelete": { - "description": "Placeholder when nothing selected" - }, - "downloadedAlbumDiscHeader": "Disco {discNumber}", - "@downloadedAlbumDiscHeader": { - "description": "Header for disc separator in multi-disc albums", - "placeholders": { - "discNumber": { - "type": "int", - "example": "1" - } - } - }, - "recentTypeArtist": "Artista", - "@recentTypeArtist": { - "description": "Recent access item type - artist" - }, - "recentTypeAlbum": "Álbum", - "@recentTypeAlbum": { - "description": "Recent access item type - album" - }, - "recentTypeSong": "Música", - "@recentTypeSong": { - "description": "Recent access item type - song/track" - }, - "recentTypePlaylist": "Playlist", - "@recentTypePlaylist": { - "description": "Recent access item type - playlist" - }, - "recentEmpty": "No recent items yet", - "@recentEmpty": { - "description": "Empty state text for recent access list" - }, - "recentShowAllDownloads": "Show All Downloads", - "@recentShowAllDownloads": { - "description": "Button label to unhide hidden downloads in recent access" - }, - "recentPlaylistInfo": "Playlist: {name}", - "@recentPlaylistInfo": { - "description": "Snackbar message when tapping playlist in recent access", - "placeholders": { - "name": { - "type": "String", - "description": "Playlist name" - } - } - }, - "discographyDownload": "Baixar Discografia", - "@discographyDownload": { - "description": "Button - download artist discography" - }, - "discographyDownloadAll": "Baixar Tudo", - "@discographyDownloadAll": { - "description": "Option - download entire discography" - }, - "discographyDownloadAllSubtitle": "{count} faixas de {albumCount} lançamentos", - "@discographyDownloadAllSubtitle": { - "description": "Subtitle showing total tracks and albums", - "placeholders": { - "count": { - "type": "int" - }, - "albumCount": { - "type": "int" - } - } - }, - "discographyAlbumsOnly": "Somente Álbuns", - "@discographyAlbumsOnly": { - "description": "Option - download only albums" - }, - "discographyAlbumsOnlySubtitle": "{count} faixas de {albumCount} álbuns", - "@discographyAlbumsOnlySubtitle": { - "description": "Subtitle showing album tracks count", - "placeholders": { - "count": { - "type": "int" - }, - "albumCount": { - "type": "int" - } - } - }, - "discographySinglesOnly": "Somente Singles e EPs", - "@discographySinglesOnly": { - "description": "Option - download only singles" - }, - "discographySinglesOnlySubtitle": "{count} faixas de {albumCount} singles", - "@discographySinglesOnlySubtitle": { - "description": "Subtitle showing singles tracks count", - "placeholders": { - "count": { - "type": "int" - }, - "albumCount": { - "type": "int" - } - } - }, - "discographySelectAlbums": "Selecione Álbuns...", - "@discographySelectAlbums": { - "description": "Option - manually select albums to download" - }, - "discographySelectAlbumsSubtitle": "Escolher álbuns ou singles específicos", - "@discographySelectAlbumsSubtitle": { - "description": "Subtitle for select albums option" - }, - "discographyFetchingTracks": "Buscando faixas...", - "@discographyFetchingTracks": { - "description": "Progress - fetching album tracks" - }, - "discographyFetchingAlbum": "Buscando {current} de {total}...", - "@discographyFetchingAlbum": { - "description": "Progress - fetching specific album", - "placeholders": { - "current": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "discographySelectedCount": "{count} selecionado(s)", - "@discographySelectedCount": { - "description": "Selection count badge", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "discographyDownloadSelected": "Baixar Selecionados", - "@discographyDownloadSelected": { - "description": "Button - download selected albums" - }, - "discographyAddedToQueue": "{count} faixas adicionadas à fila", - "@discographyAddedToQueue": { - "description": "Snackbar - tracks added from discography", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "discographySkippedDownloaded": "{added} adicionada(s), {skipped} já baixada(s)", - "@discographySkippedDownloaded": { - "description": "Snackbar - with skipped tracks count", - "placeholders": { - "added": { - "type": "int" - }, - "skipped": { - "type": "int" - } - } - }, - "discographyNoAlbums": "Nenhum álbum disponível", - "@discographyNoAlbums": { - "description": "Error - no albums found for artist" - }, - "discographyFailedToFetch": "Falha ao obter alguns álbuns", - "@discographyFailedToFetch": { - "description": "Error - some albums failed to load" - }, - "sectionStorageAccess": "Storage Access", - "@sectionStorageAccess": { - "description": "Section header for storage access settings" - }, - "allFilesAccess": "All Files Access", - "@allFilesAccess": { - "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" - }, - "allFilesAccessEnabledSubtitle": "Can write to any folder", - "@allFilesAccessEnabledSubtitle": { - "description": "Subtitle when all files access is enabled" - }, - "allFilesAccessDisabledSubtitle": "Limited to media folders only", - "@allFilesAccessDisabledSubtitle": { - "description": "Subtitle when all files access is disabled" - }, - "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", - "@allFilesAccessDescription": { - "description": "Description explaining when to enable all files access" - }, - "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", - "@allFilesAccessDeniedMessage": { - "description": "Message when permission is permanently denied" - }, - "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", - "@allFilesAccessDisabledMessage": { - "description": "Snackbar message when user disables all files access" - }, - "settingsLocalLibrary": "Local Library", - "@settingsLocalLibrary": { - "description": "Settings menu item - local library" - }, - "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", - "@settingsLocalLibrarySubtitle": { - "description": "Subtitle for local library settings" - }, - "settingsCache": "Storage & Cache", - "@settingsCache": { - "description": "Settings menu item - cache management" - }, - "settingsCacheSubtitle": "View size and clear cached data", - "@settingsCacheSubtitle": { - "description": "Subtitle for cache management menu" - }, - "libraryTitle": "Local Library", - "@libraryTitle": { - "description": "Library settings page title" - }, - "libraryScanSettings": "Scan Settings", - "@libraryScanSettings": { - "description": "Section header for scan settings" - }, - "libraryEnableLocalLibrary": "Enable Local Library", - "@libraryEnableLocalLibrary": { - "description": "Toggle to enable library scanning" - }, - "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", - "@libraryEnableLocalLibrarySubtitle": { - "description": "Subtitle for enable toggle" - }, - "libraryFolder": "Library Folder", - "@libraryFolder": { - "description": "Folder selection setting" - }, - "libraryFolderHint": "Tap to select folder", - "@libraryFolderHint": { - "description": "Placeholder when no folder selected" - }, - "libraryShowDuplicateIndicator": "Show Duplicate Indicator", - "@libraryShowDuplicateIndicator": { - "description": "Toggle for duplicate indicator in search" - }, - "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", - "@libraryShowDuplicateIndicatorSubtitle": { - "description": "Subtitle for duplicate indicator toggle" - }, - "libraryActions": "Actions", - "@libraryActions": { - "description": "Section header for library actions" - }, - "libraryScan": "Scan Library", - "@libraryScan": { - "description": "Button to start library scan" - }, - "libraryScanSubtitle": "Scan for audio files", - "@libraryScanSubtitle": { - "description": "Subtitle for scan button" - }, - "libraryScanSelectFolderFirst": "Select a folder first", - "@libraryScanSelectFolderFirst": { - "description": "Message when trying to scan without folder" - }, - "libraryCleanupMissingFiles": "Cleanup Missing Files", - "@libraryCleanupMissingFiles": { - "description": "Button to remove entries for missing files" - }, - "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", - "@libraryCleanupMissingFilesSubtitle": { - "description": "Subtitle for cleanup button" - }, - "libraryClear": "Clear Library", - "@libraryClear": { - "description": "Button to clear all library entries" - }, - "libraryClearSubtitle": "Remove all scanned tracks", - "@libraryClearSubtitle": { - "description": "Subtitle for clear button" - }, - "libraryClearConfirmTitle": "Clear Library", - "@libraryClearConfirmTitle": { - "description": "Dialog title for clear confirmation" - }, - "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", - "@libraryClearConfirmMessage": { - "description": "Dialog message for clear confirmation" - }, - "libraryAbout": "About Local Library", - "@libraryAbout": { - "description": "Section header for about info" - }, - "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", - "@libraryAboutDescription": { - "description": "Description of local library feature" - }, - "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}", - "@libraryTracksUnit": { - "description": "Unit label for tracks count (without the number itself)", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "libraryLastScanned": "Last scanned: {time}", - "@libraryLastScanned": { - "description": "Last scan time display", - "placeholders": { - "time": { - "type": "String" - } - } - }, - "libraryLastScannedNever": "Never", - "@libraryLastScannedNever": { - "description": "Shown when library has never been scanned" - }, - "libraryScanning": "Scanning...", - "@libraryScanning": { - "description": "Status during scan" - }, - "libraryScanProgress": "{progress}% of {total} files", - "@libraryScanProgress": { - "description": "Scan progress display", - "placeholders": { - "progress": { - "type": "String" - }, - "total": { - "type": "int" - } - } - }, - "libraryInLibrary": "In Library", - "@libraryInLibrary": { - "description": "Badge shown on tracks that exist in local library" - }, - "libraryRemovedMissingFiles": "Removed {count} missing files from library", - "@libraryRemovedMissingFiles": { - "description": "Snackbar after cleanup", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "libraryCleared": "Library cleared", - "@libraryCleared": { - "description": "Snackbar after clearing library" - }, - "libraryStorageAccessRequired": "Storage Access Required", - "@libraryStorageAccessRequired": { - "description": "Dialog title for storage permission" - }, - "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", - "@libraryStorageAccessMessage": { - "description": "Dialog message for storage permission" - }, - "libraryFolderNotExist": "Selected folder does not exist", - "@libraryFolderNotExist": { - "description": "Error when folder doesn't exist" - }, - "librarySourceDownloaded": "Downloaded", - "@librarySourceDownloaded": { - "description": "Badge for tracks downloaded via SpotiFLAC" - }, - "librarySourceLocal": "Local", - "@librarySourceLocal": { - "description": "Badge for tracks from local library scan" - }, - "libraryFilterAll": "All", - "@libraryFilterAll": { - "description": "Filter chip - show all library items" - }, - "libraryFilterDownloaded": "Downloaded", - "@libraryFilterDownloaded": { - "description": "Filter chip - show only downloaded items" - }, - "libraryFilterLocal": "Local", - "@libraryFilterLocal": { - "description": "Filter chip - show only local library items" - }, - "libraryFilterTitle": "Filters", - "@libraryFilterTitle": { - "description": "Filter bottom sheet title" - }, - "libraryFilterReset": "Reset", - "@libraryFilterReset": { - "description": "Reset all filters button" - }, - "libraryFilterApply": "Apply", - "@libraryFilterApply": { - "description": "Apply filters button" - }, - "libraryFilterSource": "Source", - "@libraryFilterSource": { - "description": "Filter section - source type" - }, - "libraryFilterQuality": "Quality", - "@libraryFilterQuality": { - "description": "Filter section - audio quality" - }, - "libraryFilterQualityHiRes": "Hi-Res (24bit)", - "@libraryFilterQualityHiRes": { - "description": "Filter option - high resolution audio" - }, - "libraryFilterQualityCD": "CD (16bit)", - "@libraryFilterQualityCD": { - "description": "Filter option - CD quality audio" - }, - "libraryFilterQualityLossy": "Lossy", - "@libraryFilterQualityLossy": { - "description": "Filter option - lossy compressed audio" - }, - "libraryFilterFormat": "Format", - "@libraryFilterFormat": { - "description": "Filter section - file format" - }, - "libraryFilterSort": "Sort", - "@libraryFilterSort": { - "description": "Filter section - sort order" - }, - "libraryFilterSortLatest": "Latest", - "@libraryFilterSortLatest": { - "description": "Sort option - newest first" - }, - "libraryFilterSortOldest": "Oldest", - "@libraryFilterSortOldest": { - "description": "Sort option - oldest first" - }, - "timeJustNow": "Just now", - "@timeJustNow": { - "description": "Relative time - less than a minute ago" - }, - "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", - "@timeMinutesAgo": { - "description": "Relative time - minutes ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", - "@timeHoursAgo": { - "description": "Relative time - hours ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", - "@tutorialWelcomeTitle": { - "description": "Tutorial welcome page title" - }, - "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", - "@tutorialWelcomeDesc": { - "description": "Tutorial welcome page description" - }, - "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", - "@tutorialWelcomeTip1": { - "description": "Tutorial welcome tip 1" - }, - "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", - "@tutorialWelcomeTip2": { - "description": "Tutorial welcome tip 2" - }, - "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", - "@tutorialWelcomeTip3": { - "description": "Tutorial welcome tip 3" - }, - "tutorialSearchTitle": "Finding Music", - "@tutorialSearchTitle": { - "description": "Tutorial search page title" - }, - "tutorialSearchDesc": "There are two easy ways to find music you want to download.", - "@tutorialSearchDesc": { - "description": "Tutorial search page description" - }, - "tutorialDownloadTitle": "Downloading Music", - "@tutorialDownloadTitle": { - "description": "Tutorial download page title" - }, - "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", - "@tutorialDownloadDesc": { - "description": "Tutorial download page description" - }, - "tutorialLibraryTitle": "Your Library", - "@tutorialLibraryTitle": { - "description": "Tutorial library page title" - }, - "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", - "@tutorialLibraryDesc": { - "description": "Tutorial library page description" - }, - "tutorialLibraryTip1": "View download progress and queue in the Library tab", - "@tutorialLibraryTip1": { - "description": "Tutorial library tip 1" - }, - "tutorialLibraryTip2": "Tap any track to play it with your music player", - "@tutorialLibraryTip2": { - "description": "Tutorial library tip 2" - }, - "tutorialLibraryTip3": "Switch between list and grid view for better browsing", - "@tutorialLibraryTip3": { - "description": "Tutorial library tip 3" - }, - "tutorialExtensionsTitle": "Extensions", - "@tutorialExtensionsTitle": { - "description": "Tutorial extensions page title" - }, - "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", - "@tutorialExtensionsDesc": { - "description": "Tutorial extensions page description" - }, - "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", - "@tutorialExtensionsTip1": { - "description": "Tutorial extensions tip 1" - }, - "tutorialExtensionsTip2": "Add new download providers or search sources", - "@tutorialExtensionsTip2": { - "description": "Tutorial extensions tip 2" - }, - "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", - "@tutorialExtensionsTip3": { - "description": "Tutorial extensions tip 3" - }, - "tutorialSettingsTitle": "Customize Your Experience", - "@tutorialSettingsTitle": { - "description": "Tutorial settings page title" - }, - "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", - "@tutorialSettingsDesc": { - "description": "Tutorial settings page description" - }, - "tutorialSettingsTip1": "Change download location and folder organization", - "@tutorialSettingsTip1": { - "description": "Tutorial settings tip 1" - }, - "tutorialSettingsTip2": "Set default audio quality and format preferences", - "@tutorialSettingsTip2": { - "description": "Tutorial settings tip 2" - }, - "tutorialSettingsTip3": "Customize app theme and appearance", - "@tutorialSettingsTip3": { - "description": "Tutorial settings tip 3" - }, - "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", - "@tutorialReadyMessage": { - "description": "Tutorial completion message" - }, - "libraryForceFullScan": "Force Full Scan", - "@libraryForceFullScan": { - "description": "Button to force a complete rescan of library" - }, - "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", - "@libraryForceFullScanSubtitle": { - "description": "Subtitle for force full scan button" - }, - "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", - "@cleanupOrphanedDownloads": { - "description": "Button to remove history entries for deleted files" - }, - "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", - "@cleanupOrphanedDownloadsSubtitle": { - "description": "Subtitle for orphaned cleanup button" - }, - "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", - "@cleanupOrphanedDownloadsResult": { - "description": "Snackbar after orphan cleanup", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "cleanupOrphanedDownloadsNone": "No orphaned entries found", - "@cleanupOrphanedDownloadsNone": { - "description": "Snackbar when no orphans found" - }, - "cacheTitle": "Storage & Cache", - "@cacheTitle": { - "description": "Cache management page title" - }, - "cacheSummaryTitle": "Cache overview", - "@cacheSummaryTitle": { - "description": "Heading for cache summary card" - }, - "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", - "@cacheSummarySubtitle": { - "description": "Helper text for cache summary card" - }, - "cacheEstimatedTotal": "Estimated cache usage: {size}", - "@cacheEstimatedTotal": { - "description": "Total cache size shown in summary", - "placeholders": { - "size": { - "type": "String" - } - } - }, - "cacheSectionStorage": "Cached Data", - "@cacheSectionStorage": { - "description": "Section header for cache entries" - }, - "cacheSectionMaintenance": "Maintenance", - "@cacheSectionMaintenance": { - "description": "Section header for cleanup actions" - }, - "cacheAppDirectory": "App cache directory", - "@cacheAppDirectory": { - "description": "Cache item title for app cache directory" - }, - "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", - "@cacheAppDirectoryDesc": { - "description": "Description of what app cache directory contains" - }, - "cacheTempDirectory": "Temporary directory", - "@cacheTempDirectory": { - "description": "Cache item title for temporary files directory" - }, - "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", - "@cacheTempDirectoryDesc": { - "description": "Description of what temporary directory contains" - }, - "cacheCoverImage": "Cover image cache", - "@cacheCoverImage": { - "description": "Cache item title for persistent cover images" - }, - "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", - "@cacheCoverImageDesc": { - "description": "Description of what cover image cache contains" - }, - "cacheLibraryCover": "Library cover cache", - "@cacheLibraryCover": { - "description": "Cache item title for local library cover art images" - }, - "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", - "@cacheLibraryCoverDesc": { - "description": "Description of what library cover cache contains" - }, - "cacheExploreFeed": "Explore feed cache", - "@cacheExploreFeed": { - "description": "Cache item title for explore home feed cache" - }, - "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", - "@cacheExploreFeedDesc": { - "description": "Description of what explore feed cache contains" - }, - "cacheTrackLookup": "Track lookup cache", - "@cacheTrackLookup": { - "description": "Cache item title for track ID lookup cache" - }, - "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", - "@cacheTrackLookupDesc": { - "description": "Description of what track lookup cache contains" - }, - "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", - "@cacheCleanupUnusedDesc": { - "description": "Description of what cleanup unused data does" - }, - "cacheNoData": "No cached data", - "@cacheNoData": { - "description": "Label when cache category has no data" - }, - "cacheSizeWithFiles": "{size} in {count} files", - "@cacheSizeWithFiles": { - "description": "Cache size and file count", - "placeholders": { - "size": { - "type": "String" - }, - "count": { - "type": "int" - } - } - }, - "cacheSizeOnly": "{size}", - "@cacheSizeOnly": { - "description": "Cache size only", - "placeholders": { - "size": { - "type": "String" - } - } - }, - "cacheEntries": "{count} entries", - "@cacheEntries": { - "description": "Track cache entry count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "cacheClearSuccess": "Cleared: {target}", - "@cacheClearSuccess": { - "description": "Snackbar after clearing selected cache", - "placeholders": { - "target": { - "type": "String" - } - } - }, - "cacheClearConfirmTitle": "Clear cache?", - "@cacheClearConfirmTitle": { - "description": "Dialog title before clearing one cache category" - }, - "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", - "@cacheClearConfirmMessage": { - "description": "Dialog message before clearing selected cache", - "placeholders": { - "target": { - "type": "String" - } - } - }, - "cacheClearAllConfirmTitle": "Clear all cache?", - "@cacheClearAllConfirmTitle": { - "description": "Dialog title before clearing all caches" - }, - "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", - "@cacheClearAllConfirmMessage": { - "description": "Dialog message before clearing all caches" - }, - "cacheClearAll": "Clear all cache", - "@cacheClearAll": { - "description": "Button label to clear all caches" - }, - "cacheCleanupUnused": "Cleanup unused data", - "@cacheCleanupUnused": { - "description": "Action title for cleaning unused entries" - }, - "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", - "@cacheCleanupUnusedSubtitle": { - "description": "Subtitle for cleanup unused data action" - }, - "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", - "@cacheCleanupResult": { - "description": "Snackbar after unused data cleanup", - "placeholders": { - "downloadCount": { - "type": "int" - }, - "libraryCount": { - "type": "int" - } - } - }, - "cacheRefreshStats": "Refresh stats", - "@cacheRefreshStats": { - "description": "Button label to refresh cache statistics" - }, - "trackSaveCoverArt": "Save Cover Art", - "@trackSaveCoverArt": { - "description": "Menu action - save album cover art as file" - }, - "trackSaveCoverArtSubtitle": "Save album art as .jpg file", - "@trackSaveCoverArtSubtitle": { - "description": "Subtitle for save cover art action" - }, - "trackSaveLyrics": "Save Lyrics (.lrc)", - "@trackSaveLyrics": { - "description": "Menu action - save lyrics as .lrc file" - }, - "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", - "@trackSaveLyricsSubtitle": { - "description": "Subtitle for save lyrics action" - }, - "trackSaveLyricsProgress": "Saving lyrics...", - "@trackSaveLyricsProgress": { - "description": "Snackbar while saving lyrics to file" - }, - "trackReEnrich": "Re-enrich", - "@trackReEnrich": { - "description": "Menu action - re-embed metadata into audio file" - }, - "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", - "@trackReEnrichOnlineSubtitle": { - "description": "Subtitle for re-enrich metadata action for local items" - }, - "trackEditMetadata": "Edit Metadata", - "@trackEditMetadata": { - "description": "Menu action - edit embedded metadata" - }, - "trackCoverSaved": "Cover art saved to {fileName}", - "@trackCoverSaved": { - "description": "Snackbar after cover art saved", - "placeholders": { - "fileName": { - "type": "String" - } - } - }, - "trackCoverNoSource": "No cover art source available", - "@trackCoverNoSource": { - "description": "Snackbar when no cover art URL or embedded cover" - }, - "trackLyricsSaved": "Lyrics saved to {fileName}", - "@trackLyricsSaved": { - "description": "Snackbar after lyrics saved", - "placeholders": { - "fileName": { - "type": "String" - } - } - }, - "trackReEnrichProgress": "Re-enriching metadata...", - "@trackReEnrichProgress": { - "description": "Snackbar while re-enriching metadata" - }, - "trackReEnrichSearching": "Searching metadata online...", - "@trackReEnrichSearching": { - "description": "Snackbar while searching metadata from internet for local items" - }, - "trackReEnrichSuccess": "Metadata re-enriched successfully", - "@trackReEnrichSuccess": { - "description": "Snackbar after successful re-enrichment" - }, - "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", - "@trackReEnrichFfmpegFailed": { - "description": "Snackbar when FFmpeg embed fails for MP3/Opus" - }, - "trackSaveFailed": "Failed: {error}", - "@trackSaveFailed": { - "description": "Snackbar when save operation fails", - "placeholders": { - "error": { - "type": "String" - } - } - }, - "trackConvertFormat": "Convert Format", - "@trackConvertFormat": { - "description": "Menu item - convert audio format" - }, - "trackConvertFormatSubtitle": "Convert to MP3 or Opus", - "@trackConvertFormatSubtitle": { - "description": "Subtitle for convert format menu item" - }, - "trackConvertTitle": "Convert Audio", - "@trackConvertTitle": { - "description": "Title of convert bottom sheet" - }, - "trackConvertTargetFormat": "Target Format", - "@trackConvertTargetFormat": { - "description": "Label for format selection" - }, - "trackConvertBitrate": "Bitrate", - "@trackConvertBitrate": { - "description": "Label for bitrate selection" - }, - "trackConvertConfirmTitle": "Confirm Conversion", - "@trackConvertConfirmTitle": { - "description": "Confirmation dialog title" - }, - "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", - "@trackConvertConfirmMessage": { - "description": "Confirmation dialog message", - "placeholders": { - "sourceFormat": { - "type": "String" - }, - "targetFormat": { - "type": "String" - }, - "bitrate": { - "type": "String" - } - } - }, - "trackConvertConverting": "Converting audio...", - "@trackConvertConverting": { - "description": "Snackbar while converting" - }, - "trackConvertSuccess": "Converted to {format} successfully", - "@trackConvertSuccess": { - "description": "Snackbar after successful conversion", - "placeholders": { - "format": { - "type": "String" - } - } - }, - "trackConvertFailed": "Conversion failed", - "@trackConvertFailed": { - "description": "Snackbar when conversion fails" - }, - "actionCreate": "Create", - "@actionCreate": { - "description": "Generic action button - create" - }, - "collectionFoldersTitle": "My folders", - "@collectionFoldersTitle": { - "description": "Library section title for custom folders" - }, - "collectionWishlist": "Wishlist", - "@collectionWishlist": { - "description": "Custom folder for saved tracks to download later" - }, - "collectionLoved": "Loved", - "@collectionLoved": { - "description": "Custom folder for favorite tracks" - }, - "collectionPlaylists": "Playlists", - "@collectionPlaylists": { - "description": "Custom user playlists folder" - }, - "collectionPlaylist": "Playlist", - "@collectionPlaylist": { - "description": "Single playlist label" - }, - "collectionAddToPlaylist": "Add to playlist", - "@collectionAddToPlaylist": { - "description": "Action to add a track to user playlist" - }, - "collectionCreatePlaylist": "Create playlist", - "@collectionCreatePlaylist": { - "description": "Action to create a new playlist" - }, - "collectionNoPlaylistsYet": "No playlists yet", - "@collectionNoPlaylistsYet": { - "description": "Empty state title when user has no playlists" - }, - "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks", - "@collectionNoPlaylistsSubtitle": { - "description": "Empty state subtitle when user has no playlists" - }, - "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}", - "@collectionPlaylistTracks": { - "description": "Track count label for custom playlists", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "collectionAddedToPlaylist": "Added to \"{playlistName}\"", - "@collectionAddedToPlaylist": { - "description": "Snackbar after adding track to playlist", - "placeholders": { - "playlistName": { - "type": "String" - } - } - }, - "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"", - "@collectionAlreadyInPlaylist": { - "description": "Snackbar when track already exists in playlist", - "placeholders": { - "playlistName": { - "type": "String" - } - } - }, - "collectionPlaylistCreated": "Playlist created", - "@collectionPlaylistCreated": { - "description": "Snackbar after creating playlist" - }, - "collectionPlaylistNameHint": "Playlist name", - "@collectionPlaylistNameHint": { - "description": "Hint text for playlist name input" - }, - "collectionPlaylistNameRequired": "Playlist name is required", - "@collectionPlaylistNameRequired": { - "description": "Validation error for empty playlist name" - }, - "collectionRenamePlaylist": "Rename playlist", - "@collectionRenamePlaylist": { - "description": "Action to rename playlist" - }, - "collectionDeletePlaylist": "Delete playlist", - "@collectionDeletePlaylist": { - "description": "Action to delete playlist" - }, - "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?", - "@collectionDeletePlaylistMessage": { - "description": "Confirmation message for deleting playlist", - "placeholders": { - "playlistName": { - "type": "String" - } - } - }, - "collectionPlaylistDeleted": "Playlist deleted", - "@collectionPlaylistDeleted": { - "description": "Snackbar after deleting playlist" - }, - "collectionPlaylistRenamed": "Playlist renamed", - "@collectionPlaylistRenamed": { - "description": "Snackbar after renaming playlist" - }, - "collectionWishlistEmptyTitle": "Wishlist is empty", - "@collectionWishlistEmptyTitle": { - "description": "Wishlist empty state title" - }, - "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later", - "@collectionWishlistEmptySubtitle": { - "description": "Wishlist empty state subtitle" - }, - "collectionLovedEmptyTitle": "Loved folder is empty", - "@collectionLovedEmptyTitle": { - "description": "Loved empty state title" - }, - "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites", - "@collectionLovedEmptySubtitle": { - "description": "Loved empty state subtitle" - }, - "collectionPlaylistEmptyTitle": "Playlist is empty", - "@collectionPlaylistEmptyTitle": { - "description": "Playlist empty state title" - }, - "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here", - "@collectionPlaylistEmptySubtitle": { - "description": "Playlist empty state subtitle" - }, - "collectionRemoveFromPlaylist": "Remove from playlist", - "@collectionRemoveFromPlaylist": { - "description": "Tooltip for removing track from playlist" - }, - "collectionRemoveFromFolder": "Remove from folder", - "@collectionRemoveFromFolder": { - "description": "Tooltip for removing track from wishlist/loved folder" - }, - "collectionRemoved": "\"{trackName}\" removed", - "@collectionRemoved": { - "description": "Snackbar after removing a track from a collection", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionAddedToLoved": "\"{trackName}\" added to Loved", - "@collectionAddedToLoved": { - "description": "Snackbar after adding track to loved folder", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved", - "@collectionRemovedFromLoved": { - "description": "Snackbar after removing track from loved folder", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist", - "@collectionAddedToWishlist": { - "description": "Snackbar after adding track to wishlist", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist", - "@collectionRemovedFromWishlist": { - "description": "Snackbar after removing track from wishlist", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "trackOptionAddToLoved": "Add to Loved", - "@trackOptionAddToLoved": { - "description": "Bottom sheet action label - add track to loved folder" - }, - "trackOptionRemoveFromLoved": "Remove from Loved", - "@trackOptionRemoveFromLoved": { - "description": "Bottom sheet action label - remove track from loved folder" - }, - "trackOptionAddToWishlist": "Add to Wishlist", - "@trackOptionAddToWishlist": { - "description": "Bottom sheet action label - add track to wishlist" - }, - "trackOptionRemoveFromWishlist": "Remove from Wishlist", - "@trackOptionRemoveFromWishlist": { - "description": "Bottom sheet action label - remove track from wishlist" - }, - "collectionPlaylistChangeCover": "Change cover image", - "@collectionPlaylistChangeCover": { - "description": "Bottom sheet action to pick a custom cover image for a playlist" - }, - "collectionPlaylistRemoveCover": "Remove cover image", - "@collectionPlaylistRemoveCover": { - "description": "Bottom sheet action to remove custom cover image from a playlist" - }, - "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}", - "@selectionShareCount": { - "description": "Share button text with count in selection mode", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionShareNoFiles": "No shareable files found", - "@selectionShareNoFiles": { - "description": "Snackbar when no selected files exist on disk" - }, - "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}", - "@selectionConvertCount": { - "description": "Convert button text with count in selection mode", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionConvertNoConvertible": "No convertible tracks selected", - "@selectionConvertNoConvertible": { - "description": "Snackbar when no selected tracks support conversion" - }, - "selectionBatchConvertConfirmTitle": "Batch Convert", - "@selectionBatchConvertConfirmTitle": { - "description": "Confirmation dialog title for batch conversion" - }, - "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.", - "@selectionBatchConvertConfirmMessage": { - "description": "Confirmation dialog message for batch conversion", - "placeholders": { - "count": { - "type": "int" - }, - "format": { - "type": "String" - }, - "bitrate": { - "type": "String" - } - } - }, - "selectionBatchConvertProgress": "Converting {current} of {total}...", - "@selectionBatchConvertProgress": { - "description": "Snackbar during batch conversion progress", - "placeholders": { - "current": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}", - "@selectionBatchConvertSuccess": { - "description": "Snackbar after batch conversion completes", - "placeholders": { - "success": { - "type": "int" - }, - "total": { - "type": "int" - }, - "format": { - "type": "String" - } - } - }, - "downloadedAlbumDownloadedCount": "{count} baixado(s)", - "@downloadedAlbumDownloadedCount": { - "description": "Downloaded tracks count badge", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", - "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { - "description": "Subtitle when Album Artist is used for folder naming" - }, - "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", - "@downloadUseAlbumArtistForFoldersTrackSubtitle": { - "description": "Subtitle when Track Artist is used for folder naming" - } -} \ No newline at end of file diff --git a/lib/l10n/arb/app_ru.arb b/lib/l10n/arb/app_ru.arb index 877c925e..10f1fce3 100644 --- a/lib/l10n/arb/app_ru.arb +++ b/lib/l10n/arb/app_ru.arb @@ -728,7 +728,7 @@ "@dialogDeleteSelectedTitle": { "description": "Dialog title - delete selected items" }, - "dialogDeleteSelectedMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}} из истории?\n\nЭто также удалит файлы из хранилища.", + "dialogDeleteSelectedMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}} из истории?\n\nЭто также удалит файлы из хранилища.", "@dialogDeleteSelectedMessage": { "description": "Dialog message - delete selected tracks", "placeholders": { @@ -807,7 +807,7 @@ "@snackbarCredentialsCleared": { "description": "Snackbar - Spotify credentials removed" }, - "snackbarDeletedTracks": "Удалено {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}", + "snackbarDeletedTracks": "Удалено {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}", "@snackbarDeletedTracks": { "description": "Snackbar - tracks deleted", "placeholders": { @@ -1350,7 +1350,7 @@ } } }, - "tracksCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} =1 {1 трек} other {{count} треков}}", + "tracksCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}}", "@tracksCount": { "description": "Track count display", "placeholders": { @@ -1889,7 +1889,7 @@ "@downloadedAlbumDeleteSelected": { "description": "Button - delete selected tracks" }, - "downloadedAlbumDeleteMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}} из этого альбома?\n\nЭто также удалит файлы из хранилища.", + "downloadedAlbumDeleteMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}} из этого альбома?\n\nЭто также удалит файлы из хранилища.", "@downloadedAlbumDeleteMessage": { "description": "Delete confirmation with count", "placeholders": { @@ -1915,7 +1915,7 @@ "@downloadedAlbumTapToSelect": { "description": "Selection hint" }, - "downloadedAlbumDeleteCount": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}", + "downloadedAlbumDeleteCount": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}", "@downloadedAlbumDeleteCount": { "description": "Delete button text with count", "placeholders": { @@ -2214,7 +2214,7 @@ "@libraryAboutDescription": { "description": "Description of local library feature" }, - "libraryTracksUnit": "{count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}", + "libraryTracksUnit": "{count, plural, one {трек} few {трека} many {треков} other{треков}}", "@libraryTracksUnit": { "description": "Unit label for tracks count (without the number itself)", "placeholders": { @@ -2353,7 +2353,7 @@ "@timeJustNow": { "description": "Relative time - less than a minute ago" }, - "timeMinutesAgo": "{count, plural, one {{count} минуту} few {{count} минуты} many {{count} минут} =1 {1 минуту} other {{count} минут}} назад", + "timeMinutesAgo": "{count, plural, one {{count} минуту} few {{count} минуты} many {{count} минут} other {{count} минут}} назад", "@timeMinutesAgo": { "description": "Relative time - minutes ago", "placeholders": { @@ -2362,7 +2362,7 @@ } } }, - "timeHoursAgo": "{count, plural, one {{count} час} few {{count} часа} many {{count} часов} =1 {1 час} other {{count} часов}} назад", + "timeHoursAgo": "{count, plural, one {{count} час} few {{count} часа} many {{count} часов} other {{count} часов}} назад", "@timeHoursAgo": { "description": "Relative time - hours ago", "placeholders": { @@ -2848,7 +2848,7 @@ "@collectionNoPlaylistsSubtitle": { "description": "Empty state subtitle when user has no playlists" }, - "collectionPlaylistTracks": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} =1 {1 трек} other {{count} треков}}", + "collectionPlaylistTracks": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}}", "@collectionPlaylistTracks": { "description": "Track count label for custom playlists", "placeholders": { @@ -3013,7 +3013,7 @@ "@collectionPlaylistRemoveCover": { "description": "Bottom sheet action to remove custom cover image from a playlist" }, - "selectionShareCount": "Отправить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}", + "selectionShareCount": "Отправить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}", "@selectionShareCount": { "description": "Share button text with count in selection mode", "placeholders": { @@ -3026,7 +3026,7 @@ "@selectionShareNoFiles": { "description": "Snackbar when no selected files exist on disk" }, - "selectionConvertCount": "Конвертировать {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}", + "selectionConvertCount": "Конвертировать {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}", "@selectionConvertCount": { "description": "Convert button text with count in selection mode", "placeholders": { diff --git a/lib/l10n/arb/app_tr-TR.arb b/lib/l10n/arb/app_tr-TR.arb deleted file mode 100644 index 3e87884e..00000000 --- a/lib/l10n/arb/app_tr-TR.arb +++ /dev/null @@ -1,3105 +0,0 @@ -{ - "@@locale": "tr", - "@@last_modified": "2026-01-16", - "appName": "SpotiFLAC", - "@appName": { - "description": "App name - DO NOT TRANSLATE" - }, - "navHome": "Ara", - "@navHome": { - "description": "Bottom navigation - Home tab" - }, - "navLibrary": "Library", - "@navLibrary": { - "description": "Bottom navigation - Library tab" - }, - "navSettings": "Ayarlar", - "@navSettings": { - "description": "Bottom navigation - Settings tab" - }, - "navStore": "Dükkan", - "@navStore": { - "description": "Bottom navigation - Extension store tab" - }, - "homeTitle": "Ara", - "@homeTitle": { - "description": "Home screen title" - }, - "homeSubtitle": "Spotify linki yapıştır veya isimle arat", - "@homeSubtitle": { - "description": "Subtitle shown below search box" - }, - "homeSupports": "Desteklenen linkler: Şarkı, Albüm, Çalma Listesi, Sanatçı linkleri", - "@homeSupports": { - "description": "Info text about supported URL types" - }, - "homeRecent": "En son", - "@homeRecent": { - "description": "Section header for recent searches" - }, - "historyFilterAll": "Tümü", - "@historyFilterAll": { - "description": "Filter chip - show all items" - }, - "historyFilterAlbums": "Albümler", - "@historyFilterAlbums": { - "description": "Filter chip - show albums only" - }, - "historyFilterSingles": "Single'lar", - "@historyFilterSingles": { - "description": "Filter chip - show singles only" - }, - "historySearchHint": "Arama geçmişi...", - "@historySearchHint": { - "description": "Search bar placeholder in history" - }, - "settingsTitle": "Ayarlar", - "@settingsTitle": { - "description": "Settings screen title" - }, - "settingsDownload": "İndirme", - "@settingsDownload": { - "description": "Settings section - download options" - }, - "settingsAppearance": "Görünüm", - "@settingsAppearance": { - "description": "Settings section - visual customization" - }, - "settingsOptions": "Seçenekler", - "@settingsOptions": { - "description": "Settings section - app options" - }, - "settingsExtensions": "Eklentiler", - "@settingsExtensions": { - "description": "Settings section - extension management" - }, - "settingsAbout": "Hakkında", - "@settingsAbout": { - "description": "Settings section - app info" - }, - "downloadTitle": "İndirme", - "@downloadTitle": { - "description": "Download settings page title" - }, - "downloadAskQualitySubtitle": "Her indirmeden önce kalite seçim ekranını göster", - "@downloadAskQualitySubtitle": { - "description": "Subtitle for ask quality toggle" - }, - "downloadFilenameFormat": "Dosya adı formatı", - "@downloadFilenameFormat": { - "description": "Setting for output filename pattern" - }, - "downloadFolderOrganization": "Dosya Organizasyonu", - "@downloadFolderOrganization": { - "description": "Setting for folder structure" - }, - "appearanceTitle": "Görünüm", - "@appearanceTitle": { - "description": "Appearance settings page title" - }, - "appearanceThemeSystem": "Sistem", - "@appearanceThemeSystem": { - "description": "Follow system theme" - }, - "appearanceThemeLight": "Açık", - "@appearanceThemeLight": { - "description": "Light theme" - }, - "appearanceThemeDark": "Koyu", - "@appearanceThemeDark": { - "description": "Dark theme" - }, - "appearanceDynamicColor": "Dinamik Renk", - "@appearanceDynamicColor": { - "description": "Material You dynamic colors" - }, - "appearanceDynamicColorSubtitle": "Duvar kağıdının renklerini kullan", - "@appearanceDynamicColorSubtitle": { - "description": "Subtitle for dynamic color" - }, - "appearanceHistoryView": "Geçmiş Düzeni", - "@appearanceHistoryView": { - "description": "Layout style for history" - }, - "appearanceHistoryViewList": "Liste", - "@appearanceHistoryViewList": { - "description": "List layout option" - }, - "appearanceHistoryViewGrid": "Izgara", - "@appearanceHistoryViewGrid": { - "description": "Grid layout option" - }, - "optionsTitle": "Seçenekler", - "@optionsTitle": { - "description": "Options settings page title" - }, - "optionsPrimaryProvider": "Ana Kaynek", - "@optionsPrimaryProvider": { - "description": "Main search provider setting" - }, - "optionsPrimaryProviderSubtitle": "Şarkı ismi aratılırken kullanılan kaynak.", - "@optionsPrimaryProviderSubtitle": { - "description": "Subtitle for primary provider" - }, - "optionsUsingExtension": "Kullanılan eklenti: {extensionName}", - "@optionsUsingExtension": { - "description": "Shows active extension name", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "optionsSwitchBack": "Dahili kaynaklara dönmek için Deezer veya Spotify'a tıkla", - "@optionsSwitchBack": { - "description": "Hint to switch back to built-in providers" - }, - "optionsAutoFallback": "Diğerlerini dene", - "@optionsAutoFallback": { - "description": "Auto-retry with other services" - }, - "optionsAutoFallbackSubtitle": "İndirme başarısız olursa diğer hizmetleri dene", - "@optionsAutoFallbackSubtitle": { - "description": "Subtitle for auto fallback" - }, - "optionsUseExtensionProviders": "Eklenti sağlayıcılarını kullan", - "@optionsUseExtensionProviders": { - "description": "Enable extension download providers" - }, - "optionsUseExtensionProvidersOn": "Eklentiler ilk denenecek", - "@optionsUseExtensionProvidersOn": { - "description": "Status when extension providers enabled" - }, - "optionsUseExtensionProvidersOff": "Sadece dahili sağlayıcıları kullan", - "@optionsUseExtensionProvidersOff": { - "description": "Status when extension providers disabled" - }, - "optionsEmbedLyrics": "Şarkı Sözlerini Göm", - "@optionsEmbedLyrics": { - "description": "Embed lyrics in audio files" - }, - "optionsEmbedLyricsSubtitle": "Senkronize şarkı sözlerini FLAC dosyalarına göm", - "@optionsEmbedLyricsSubtitle": { - "description": "Subtitle for embed lyrics" - }, - "optionsMaxQualityCover": "En Yüksek Kapak Kalitesi", - "@optionsMaxQualityCover": { - "description": "Download highest quality album art" - }, - "optionsMaxQualityCoverSubtitle": "En yüksek kalitedeki albüm kapaklarını indir", - "@optionsMaxQualityCoverSubtitle": { - "description": "Subtitle for max quality cover" - }, - "optionsConcurrentDownloads": "Eş Zamanlı İndirmeler", - "@optionsConcurrentDownloads": { - "description": "Number of parallel downloads" - }, - "optionsConcurrentSequential": "Sıralı (Birer birer)", - "@optionsConcurrentSequential": { - "description": "Download one at a time" - }, - "optionsConcurrentParallel": "Aynı anda {count} indirme", - "@optionsConcurrentParallel": { - "description": "Multiple parallel downloads", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "optionsConcurrentWarning": "Aynı anda birden fazla indirme sınırlamaya takılabilir", - "@optionsConcurrentWarning": { - "description": "Warning about rate limits" - }, - "optionsExtensionStore": "Eklenti Dükkanı", - "@optionsExtensionStore": { - "description": "Show/hide store tab" - }, - "optionsExtensionStoreSubtitle": "Dükkan sekmesini altta göster", - "@optionsExtensionStoreSubtitle": { - "description": "Subtitle for extension store toggle" - }, - "optionsCheckUpdates": "Güncelleştirmeleri Denetle", - "@optionsCheckUpdates": { - "description": "Auto update check toggle" - }, - "optionsCheckUpdatesSubtitle": "Yeni sürüm çıktığında bildir", - "@optionsCheckUpdatesSubtitle": { - "description": "Subtitle for update check" - }, - "optionsUpdateChannel": "Güncelleme Kanalı", - "@optionsUpdateChannel": { - "description": "Stable vs preview releases" - }, - "optionsUpdateChannelStable": "Sadece stabil sürümler", - "@optionsUpdateChannelStable": { - "description": "Only stable updates" - }, - "optionsUpdateChannelPreview": "Önizleme sürümlerini al", - "@optionsUpdateChannelPreview": { - "description": "Include beta/preview updates" - }, - "optionsUpdateChannelWarning": "Önizleme sürümleri hatalar veya tamamlanmamış özellikler içerebilir", - "@optionsUpdateChannelWarning": { - "description": "Warning about preview channel" - }, - "optionsClearHistory": "İndirme Geçmişini Temizle", - "@optionsClearHistory": { - "description": "Delete all download history" - }, - "optionsClearHistorySubtitle": "İndirilen bütün şarkıları geçmişten temizle", - "@optionsClearHistorySubtitle": { - "description": "Subtitle for clear history" - }, - "optionsDetailedLogging": "Detaylı Günlükleme", - "@optionsDetailedLogging": { - "description": "Enable verbose logs for debugging" - }, - "optionsDetailedLoggingOn": "Detaylı günlük kayıt ediliyor", - "@optionsDetailedLoggingOn": { - "description": "Status when logging enabled" - }, - "optionsDetailedLoggingOff": "Hata bildirmek için aç", - "@optionsDetailedLoggingOff": { - "description": "Status when logging disabled" - }, - "optionsSpotifyCredentials": "Spotify Kimlik Bilgileri", - "@optionsSpotifyCredentials": { - "description": "Spotify API credentials setting" - }, - "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", - "@optionsSpotifyCredentialsConfigured": { - "description": "Shows configured client ID preview", - "placeholders": { - "clientId": { - "type": "String" - } - } - }, - "optionsSpotifyCredentialsRequired": "Zorunlu - değiştirmek için tıkla", - "@optionsSpotifyCredentialsRequired": { - "description": "Prompt to set up credentials" - }, - "optionsSpotifyWarning": "Spotify'ın senin API kimlik bilgilerine ihtiyacı var. Onları developer.spotify.com'dan alabilirsin", - "@optionsSpotifyWarning": { - "description": "Info about Spotify API requirement" - }, - "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", - "@optionsSpotifyDeprecationWarning": { - "description": "Warning about Spotify API deprecation" - }, - "extensionsTitle": "Eklentiler", - "@extensionsTitle": { - "description": "Extensions page title" - }, - "extensionsDisabled": "Devre Dışı", - "@extensionsDisabled": { - "description": "Extension status - inactive" - }, - "extensionsVersion": "Versiyon {version}", - "@extensionsVersion": { - "description": "Extension version display", - "placeholders": { - "version": { - "type": "String" - } - } - }, - "extensionsAuthor": "{author} tarafından", - "@extensionsAuthor": { - "description": "Extension author credit", - "placeholders": { - "author": { - "type": "String" - } - } - }, - "extensionsUninstall": "Kaldır", - "@extensionsUninstall": { - "description": "Uninstall extension button" - }, - "storeTitle": "Eklenti Dükkanı", - "@storeTitle": { - "description": "Store screen title" - }, - "storeSearch": "Eklenti ara...", - "@storeSearch": { - "description": "Store search placeholder" - }, - "storeInstall": "Kur", - "@storeInstall": { - "description": "Install extension button" - }, - "storeInstalled": "Kuruldu", - "@storeInstalled": { - "description": "Already installed badge" - }, - "storeUpdate": "Güncelle", - "@storeUpdate": { - "description": "Update available button" - }, - "aboutTitle": "Hakkında", - "@aboutTitle": { - "description": "About page title" - }, - "aboutContributors": "Katkıda Bulunanlar", - "@aboutContributors": { - "description": "Section for contributors" - }, - "aboutMobileDeveloper": "Mobil versiyon geliştiricisi", - "@aboutMobileDeveloper": { - "description": "Role description for mobile dev" - }, - "aboutOriginalCreator": "Orijinal SpotiFLAC'ın kurucusu", - "@aboutOriginalCreator": { - "description": "Role description for original creator" - }, - "aboutLogoArtist": "Uygulama logomuzu yaratmış yetenekli sanatçımız!", - "@aboutLogoArtist": { - "description": "Role description for logo artist" - }, - "aboutTranslators": "Çevirmenler", - "@aboutTranslators": { - "description": "Section for translators" - }, - "aboutSpecialThanks": "Özel teşekkür", - "@aboutSpecialThanks": { - "description": "Section for special thanks" - }, - "aboutLinks": "Linkler", - "@aboutLinks": { - "description": "Section for external links" - }, - "aboutMobileSource": "Mobil kaynak kodu", - "@aboutMobileSource": { - "description": "Link to mobile GitHub repo" - }, - "aboutPCSource": "PC kaynak kodu", - "@aboutPCSource": { - "description": "Link to PC GitHub repo" - }, - "aboutReportIssue": "Sorun bildir", - "@aboutReportIssue": { - "description": "Link to report bugs" - }, - "aboutReportIssueSubtitle": "Karşılaştığın herhangi bir problemi bildir", - "@aboutReportIssueSubtitle": { - "description": "Subtitle for report issue" - }, - "aboutFeatureRequest": "Özellik isteği", - "@aboutFeatureRequest": { - "description": "Link to suggest features" - }, - "aboutFeatureRequestSubtitle": "Uygulama için yeni özellikler isteyin", - "@aboutFeatureRequestSubtitle": { - "description": "Subtitle for feature request" - }, - "aboutTelegramChannel": "Telegram Kanalı", - "@aboutTelegramChannel": { - "description": "Link to Telegram channel" - }, - "aboutTelegramChannelSubtitle": "Duyurular ve güncellemeler", - "@aboutTelegramChannelSubtitle": { - "description": "Subtitle for Telegram channel" - }, - "aboutTelegramChat": "Telegram Grubu", - "@aboutTelegramChat": { - "description": "Link to Telegram chat group" - }, - "aboutTelegramChatSubtitle": "Diğer kullanıcılarla sohbet et", - "@aboutTelegramChatSubtitle": { - "description": "Subtitle for Telegram chat" - }, - "aboutSocial": "Sosyal ağlar", - "@aboutSocial": { - "description": "Section for social links" - }, - "aboutApp": "Uygulama", - "@aboutApp": { - "description": "Section for app info" - }, - "aboutVersion": "Versiyon", - "@aboutVersion": { - "description": "Version info label" - }, - "aboutBinimumDesc": "QQDL ve HiFi API'ın kurucusu. Bu API olmadan, Tidal indirmeleri olmazdı!", - "@aboutBinimumDesc": { - "description": "Credit description for binimum" - }, - "aboutSachinsenalDesc": "Orijinal HiFi projesi kurucusu. Tidal entegrasyonun temeli!", - "@aboutSachinsenalDesc": { - "description": "Credit description for sachinsenal0x64" - }, - "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", - "@aboutSjdonadoDesc": { - "description": "Credit description for sjdonado" - }, - "aboutDabMusic": "DAB Music", - "@aboutDabMusic": { - "description": "Name of Qobuz API service - DO NOT TRANSLATE" - }, - "aboutDabMusicDesc": "En iyi Qobuz streaming API'ı. Yüksek kalite indirmeler bunun sayesinde!", - "@aboutDabMusicDesc": { - "description": "Credit for DAB Music API" - }, - "aboutSpotiSaver": "SpotiSaver", - "@aboutSpotiSaver": { - "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" - }, - "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", - "@aboutSpotiSaverDesc": { - "description": "Credit for SpotiSaver API" - }, - "aboutAppDescription": "Spotify şarkılarını Tidal, Qobuz ve Amazon Music'den yüksek kalitede indir.", - "@aboutAppDescription": { - "description": "App description in header card" - }, - "artistAlbums": "Albümler", - "@artistAlbums": { - "description": "Section header for artist albums" - }, - "artistSingles": "Single'lar ve EP'ler", - "@artistSingles": { - "description": "Section header for singles/EPs" - }, - "artistCompilations": "Derlemeler", - "@artistCompilations": { - "description": "Section header for compilations" - }, - "artistPopular": "Popüler", - "@artistPopular": { - "description": "Section header for popular/top tracks" - }, - "artistMonthlyListeners": "Aylık {count} dinleyici", - "@artistMonthlyListeners": { - "description": "Monthly listener count display", - "placeholders": { - "count": { - "type": "String", - "description": "Formatted listener count" - } - } - }, - "trackMetadataService": "Hizmet", - "@trackMetadataService": { - "description": "Metadata field - download service used" - }, - "trackMetadataPlay": "Oynat", - "@trackMetadataPlay": { - "description": "Action button - play track" - }, - "trackMetadataShare": "Paylaş", - "@trackMetadataShare": { - "description": "Action button - share track" - }, - "trackMetadataDelete": "Sil", - "@trackMetadataDelete": { - "description": "Action button - delete track" - }, - "setupGrantPermission": "İzin Ver", - "@setupGrantPermission": { - "description": "Button to request permission" - }, - "setupSkip": "Şimdilik atla", - "@setupSkip": { - "description": "Skip current step button" - }, - "setupStorageAccessRequired": "Depolama Erişimi Gerekli", - "@setupStorageAccessRequired": { - "description": "Title when storage access needed" - }, - "setupStorageAccessMessageAndroid11": "Android 11 ve sonrasında şarkıların seçili klasörünüze kaydedilebilmesi için \"Bütün dosyalara eriş\" iznine ihtiyaç var.", - "@setupStorageAccessMessageAndroid11": { - "description": "Android 11+ specific explanation" - }, - "setupOpenSettings": "Ayarları Aç", - "@setupOpenSettings": { - "description": "Button to open system settings" - }, - "setupPermissionDeniedMessage": "İzin reddedildi. Devam etmek için lütfen bütün izinleri verin.", - "@setupPermissionDeniedMessage": { - "description": "Error when permission denied" - }, - "setupPermissionRequired": "{permissionType} İzni Zorunlu", - "@setupPermissionRequired": { - "description": "Generic permission required title", - "placeholders": { - "permissionType": { - "type": "String", - "description": "Type of permission (Storage/Notification)" - } - } - }, - "setupPermissionRequiredMessage": "En iyi deneyim için {permissionType} izni zorunludur. Bunu ayarlardan daha sonra değiştirebilirsiniz.", - "@setupPermissionRequiredMessage": { - "description": "Generic permission required message", - "placeholders": { - "permissionType": { - "type": "String" - } - } - }, - "setupUseDefaultFolder": "Varsayılan Klasörü Kullan?", - "@setupUseDefaultFolder": { - "description": "Dialog title for default folder" - }, - "setupNoFolderSelected": "Klasör seçilmedi. Varsayılan \"Music\" klasörünü kullanmak ister misiniz?", - "@setupNoFolderSelected": { - "description": "Prompt when no folder selected" - }, - "setupUseDefault": "Varsayılanı Kullan", - "@setupUseDefault": { - "description": "Button to use default folder" - }, - "setupDownloadLocationTitle": "İndirme Konumu", - "@setupDownloadLocationTitle": { - "description": "Download location dialog title" - }, - "setupDownloadLocationIosMessage": "iOS'ta indirilenler uygulamanın \"Documents\" dosyasına kaydedilir. Onlara Dosyalar uygulamasından erişebilirsiniz.", - "@setupDownloadLocationIosMessage": { - "description": "iOS-specific folder info" - }, - "setupAppDocumentsFolder": "App Documents Folder", - "@setupAppDocumentsFolder": { - "description": "iOS documents folder option" - }, - "setupAppDocumentsFolderSubtitle": "Tavsiye edilen - Dosyalar uygulamasından erişilebilir", - "@setupAppDocumentsFolderSubtitle": { - "description": "Subtitle for documents folder" - }, - "setupChooseFromFiles": "Dosyalar'dan Seç", - "@setupChooseFromFiles": { - "description": "iOS file picker option" - }, - "setupChooseFromFilesSubtitle": "iCloud veya başka konum seç", - "@setupChooseFromFilesSubtitle": { - "description": "Subtitle for file picker" - }, - "setupIosEmptyFolderWarning": "iOS'un sınırlaması: Boş klasörler seçilemiyor. İçinde en az bir dosya bulunan bir klasör seçin.", - "@setupIosEmptyFolderWarning": { - "description": "iOS folder selection warning" - }, - "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", - "@setupIcloudNotSupported": { - "description": "Error when user selects iCloud Drive on iOS" - }, - "setupDownloadInFlac": "Spotify şarkılarını FLAC olarak indirin", - "@setupDownloadInFlac": { - "description": "App tagline in setup" - }, - "setupStorageGranted": "Depolama İzni Verildi!", - "@setupStorageGranted": { - "description": "Success message for storage permission" - }, - "setupStorageRequired": "Depolama İzni Gerekli", - "@setupStorageRequired": { - "description": "Title when storage permission needed" - }, - "setupStorageDescription": "SpotiFLAC'ın şarkılarınızı kaydetmek için depolama iznine ihtiyacı var.", - "@setupStorageDescription": { - "description": "Explanation for storage permission" - }, - "setupNotificationGranted": "Bildirim İzni Verildi!", - "@setupNotificationGranted": { - "description": "Success message for notification permission" - }, - "setupNotificationEnable": "Bildirimleri Etkinleştir", - "@setupNotificationEnable": { - "description": "Button to enable notifications" - }, - "setupFolderChoose": "İndirilecek Klasörü Seç", - "@setupFolderChoose": { - "description": "Button to choose folder" - }, - "setupFolderDescription": "İndirdiğin şarkıların kaydedileceği klasörü seç.", - "@setupFolderDescription": { - "description": "Explanation for folder selection" - }, - "setupSelectFolder": "Klasör Seç", - "@setupSelectFolder": { - "description": "Button to select folder" - }, - "setupEnableNotifications": "Bildirimleri Etkinleştir", - "@setupEnableNotifications": { - "description": "Button to enable notifications" - }, - "setupNotificationBackgroundDescription": "İndirmelerin durumu hakkında bildirim al. Bunu açmak uygulama arka plandayken indirmelerinizi takip etmenizi sağlar.", - "@setupNotificationBackgroundDescription": { - "description": "Detailed notification explanation" - }, - "setupSkipForNow": "Şimdilik atla", - "@setupSkipForNow": { - "description": "Skip button text" - }, - "setupNext": "Sıradaki", - "@setupNext": { - "description": "Next button text" - }, - "setupGetStarted": "Başla", - "@setupGetStarted": { - "description": "Final setup button" - }, - "setupAllowAccessToManageFiles": "Lütfen bir sonraki ekranda \"Bütün dosyalara eriş\" iznini sağlayın.", - "@setupAllowAccessToManageFiles": { - "description": "Instruction for file access permission" - }, - "dialogCancel": "İptal", - "@dialogCancel": { - "description": "Dialog button - cancel action" - }, - "dialogSave": "Kaydet", - "@dialogSave": { - "description": "Dialog button - save changes" - }, - "dialogDelete": "Sil", - "@dialogDelete": { - "description": "Dialog button - delete item" - }, - "dialogRetry": "Yeniden dene", - "@dialogRetry": { - "description": "Dialog button - retry action" - }, - "dialogClear": "Temizle", - "@dialogClear": { - "description": "Dialog button - clear items" - }, - "dialogDone": "Tamamlandı", - "@dialogDone": { - "description": "Dialog button - action completed" - }, - "dialogImport": "İçe aktar", - "@dialogImport": { - "description": "Dialog button - import data" - }, - "dialogDiscard": "Vazgeç", - "@dialogDiscard": { - "description": "Dialog button - discard changes" - }, - "dialogRemove": "Kaldır", - "@dialogRemove": { - "description": "Dialog button - remove item" - }, - "dialogUninstall": "Kaldır", - "@dialogUninstall": { - "description": "Dialog button - uninstall extension" - }, - "dialogDiscardChanges": "Değişiklikleri İptal Et?", - "@dialogDiscardChanges": { - "description": "Dialog title - unsaved changes warning" - }, - "dialogUnsavedChanges": "Kaydedilmeyen değişiklikler mevcut. Bu değişiklikleri iptal etmek istiyor musunuz?", - "@dialogUnsavedChanges": { - "description": "Dialog message - unsaved changes" - }, - "dialogClearAll": "Tümünü Temizle", - "@dialogClearAll": { - "description": "Dialog title - clear all items" - }, - "dialogRemoveExtension": "Eklentiyi Kaldır", - "@dialogRemoveExtension": { - "description": "Dialog title - uninstall extension" - }, - "dialogRemoveExtensionMessage": "Bu eklentiyi kaldırmak istediğine emin misin? Bu işlem geri alınamaz.", - "@dialogRemoveExtensionMessage": { - "description": "Dialog message - uninstall confirmation" - }, - "dialogUninstallExtension": "Eklentiyi Kaldır?", - "@dialogUninstallExtension": { - "description": "Dialog title - uninstall extension" - }, - "dialogUninstallExtensionMessage": "{extensionName} eklentisini kaldırmak istediğine emin misin?", - "@dialogUninstallExtensionMessage": { - "description": "Dialog message - uninstall specific extension", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "dialogClearHistoryTitle": "Geçmişi Temizle", - "@dialogClearHistoryTitle": { - "description": "Dialog title - clear download history" - }, - "dialogClearHistoryMessage": "Tüm indirme geçmişini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.", - "@dialogClearHistoryMessage": { - "description": "Dialog message - clear history confirmation" - }, - "dialogDeleteSelectedTitle": "Seçileni Sil", - "@dialogDeleteSelectedTitle": { - "description": "Dialog title - delete selected items" - }, - "dialogDeleteSelectedMessage": "{count} {count, plural, one {}=1{şarkıyı} other{şarkıyı}} geçmişten silmeye emin misiniz?\n\nBu işlem seçilenleri cihazınızdan da silecektir.", - "@dialogDeleteSelectedMessage": { - "description": "Dialog message - delete selected tracks", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dialogImportPlaylistTitle": "Çalma listesini içe aktar", - "@dialogImportPlaylistTitle": { - "description": "Dialog title - import CSV playlist" - }, - "dialogImportPlaylistMessage": "CSV'de {count} şarkı bulundu. İndirme kuyruğuna ekle?", - "csvImportTracks": "CSV'den {count} şarkı", - "@csvImportTracks": { - "description": "Label shown in quality picker for CSV import", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@dialogImportPlaylistMessage": { - "description": "Dialog message - import playlist confirmation", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarAddedToQueue": "\"{trackName}\" kuyruğa eklendi", - "@snackbarAddedToQueue": { - "description": "Snackbar - track added to download queue", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarAddedTracksToQueue": "{count} şarkı kuyruğa eklendi", - "@snackbarAddedTracksToQueue": { - "description": "Snackbar - multiple tracks added to queue", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarAlreadyDownloaded": "\"{trackName}\" zaten indirilmiş", - "@snackbarAlreadyDownloaded": { - "description": "Snackbar - track already exists", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", - "@snackbarAlreadyInLibrary": { - "description": "Snackbar - track already exists in local library", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarHistoryCleared": "Geçmiş temizlendi", - "@snackbarHistoryCleared": { - "description": "Snackbar - history deleted" - }, - "snackbarCredentialsSaved": "Kimlik bilgileri kaydedildi", - "@snackbarCredentialsSaved": { - "description": "Snackbar - Spotify credentials saved" - }, - "snackbarCredentialsCleared": "Kimlik bilgileri temizlendi", - "@snackbarCredentialsCleared": { - "description": "Snackbar - Spotify credentials removed" - }, - "snackbarDeletedTracks": "{count} {count, plural, one {}=1{şarkı} other{şarkı}} silindi", - "@snackbarDeletedTracks": { - "description": "Snackbar - tracks deleted", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarCannotOpenFile": "Dosya açılamadı: {error}", - "@snackbarCannotOpenFile": { - "description": "Snackbar - file open error", - "placeholders": { - "error": { - "type": "String" - } - } - }, - "snackbarFillAllFields": "Lütfen tüm alanları doldurun", - "@snackbarFillAllFields": { - "description": "Snackbar - validation error" - }, - "snackbarViewQueue": "Kuyruğu Görüntüle", - "@snackbarViewQueue": { - "description": "Snackbar action - view download queue" - }, - "snackbarUrlCopied": "{platform} Bağlantı panoya kopyalandı", - "@snackbarUrlCopied": { - "description": "Snackbar - URL copied", - "placeholders": { - "platform": { - "type": "String", - "description": "Platform name (Spotify/Deezer)" - } - } - }, - "snackbarFileNotFound": "Dosya bulunamadı", - "@snackbarFileNotFound": { - "description": "Snackbar - file doesn't exist" - }, - "snackbarSelectExtFile": "Lütfen .spotiflac-ext dosyasını seçin", - "@snackbarSelectExtFile": { - "description": "Snackbar - wrong file type selected" - }, - "snackbarProviderPrioritySaved": "Provider priority saved", - "@snackbarProviderPrioritySaved": { - "description": "Snackbar - provider order saved" - }, - "snackbarMetadataProviderSaved": "Metadata provider priority saved", - "@snackbarMetadataProviderSaved": { - "description": "Snackbar - metadata provider order saved" - }, - "snackbarExtensionInstalled": "{extensionName} yüklendi.", - "@snackbarExtensionInstalled": { - "description": "Snackbar - extension installed successfully", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "snackbarExtensionUpdated": "{extensionName} güncellendi.", - "@snackbarExtensionUpdated": { - "description": "Snackbar - extension updated successfully", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "snackbarFailedToInstall": "Eklenti yüklenirken hata oluştu", - "@snackbarFailedToInstall": { - "description": "Snackbar - extension install error" - }, - "snackbarFailedToUpdate": "Eklenti güncellenirken hata oluştu", - "@snackbarFailedToUpdate": { - "description": "Snackbar - extension update error" - }, - "errorRateLimited": "Aşırı istek gönderildi", - "@errorRateLimited": { - "description": "Error title - too many requests" - }, - "errorRateLimitedMessage": "Çok fazla istek. Lütfen arama yapmadan önce biraz bekleyin.", - "@errorRateLimitedMessage": { - "description": "Error message - rate limit explanation" - }, - "errorNoTracksFound": "Parça bulunamadı", - "@errorNoTracksFound": { - "description": "Error - search returned no results" - }, - "errorMissingExtensionSource": "{item} yüklenemedi: Eksik eklenti kaynağı", - "@errorMissingExtensionSource": { - "description": "Error - extension source not available", - "placeholders": { - "item": { - "type": "String" - } - } - }, - "actionPause": "Duraklat", - "@actionPause": { - "description": "Action button - pause download" - }, - "actionResume": "Devam et", - "@actionResume": { - "description": "Action button - resume download" - }, - "actionCancel": "Vazgeç", - "@actionCancel": { - "description": "Action button - cancel operation" - }, - "actionSelectAll": "Tümünü Seç", - "@actionSelectAll": { - "description": "Action button - select all items" - }, - "actionDeselect": "Seçimi kaldır", - "@actionDeselect": { - "description": "Action button - deselect all" - }, - "actionRemoveCredentials": "Özellikleri kaldır", - "@actionRemoveCredentials": { - "description": "Action button - delete Spotify credentials" - }, - "actionSaveCredentials": "Özellikleri kaydet", - "@actionSaveCredentials": { - "description": "Action button - save Spotify credentials" - }, - "selectionSelected": "{count} seçildi", - "@selectionSelected": { - "description": "Selection count indicator", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionAllSelected": "Tüm parçalar seçildi", - "@selectionAllSelected": { - "description": "Status - all items selected" - }, - "selectionSelectToDelete": "Silinecek parçaları seçin", - "@selectionSelectToDelete": { - "description": "Placeholder when nothing selected" - }, - "progressFetchingMetadata": "Meta verileri alınıyor... {current}/{total}", - "@progressFetchingMetadata": { - "description": "Progress indicator - loading track info", - "placeholders": { - "current": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "progressReadingCsv": "CSV okunuyor...", - "@progressReadingCsv": { - "description": "Progress indicator - parsing CSV file" - }, - "searchSongs": "Şarkılar", - "@searchSongs": { - "description": "Search result category - songs" - }, - "searchArtists": "Sanatçılar", - "@searchArtists": { - "description": "Search result category - artists" - }, - "searchAlbums": "Albümler", - "@searchAlbums": { - "description": "Search result category - albums" - }, - "searchPlaylists": "Çalma Listeleri", - "@searchPlaylists": { - "description": "Search result category - playlists" - }, - "tooltipPlay": "Oynat", - "@tooltipPlay": { - "description": "Tooltip - play button" - }, - "filenameFormat": "Dosya adı formatı", - "@filenameFormat": { - "description": "Setting title - filename pattern" - }, - "filenameShowAdvancedTags": "Show advanced tags", - "@filenameShowAdvancedTags": { - "description": "Toggle label for showing advanced filename tags" - }, - "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns", - "@filenameShowAdvancedTagsDescription": { - "description": "Description for advanced filename tag toggle" - }, - "folderOrganizationNone": "Organizasyon yok", - "@folderOrganizationNone": { - "description": "Folder option - flat structure" - }, - "folderOrganizationByArtist": "Sanatçıya Göre", - "@folderOrganizationByArtist": { - "description": "Folder option - artist folders" - }, - "folderOrganizationByAlbum": "Albüme Göre", - "@folderOrganizationByAlbum": { - "description": "Folder option - album folders" - }, - "folderOrganizationByArtistAlbum": "Sanatçı/Albüm", - "@folderOrganizationByArtistAlbum": { - "description": "Folder option - nested folders" - }, - "folderOrganizationDescription": "İndirilenleri klasörlerle organize et", - "@folderOrganizationDescription": { - "description": "Folder organization sheet description" - }, - "folderOrganizationNoneSubtitle": "Her şey indirilen dosyasına kaydedilecek", - "@folderOrganizationNoneSubtitle": { - "description": "Subtitle for no organization option" - }, - "folderOrganizationByArtistSubtitle": "Her sanatçı için ayrı klasör", - "@folderOrganizationByArtistSubtitle": { - "description": "Subtitle for artist folder option" - }, - "folderOrganizationByAlbumSubtitle": "Her albüm için ayrı klasör", - "@folderOrganizationByAlbumSubtitle": { - "description": "Subtitle for album folder option" - }, - "folderOrganizationByArtistAlbumSubtitle": "Sanatçı klasörlerinin içinde Albüm klasörleri", - "@folderOrganizationByArtistAlbumSubtitle": { - "description": "Subtitle for nested folder option" - }, - "updateAvailable": "Güncelleme Mevcut", - "@updateAvailable": { - "description": "Update dialog title" - }, - "updateLater": "Daha Sonra", - "@updateLater": { - "description": "Update button - dismiss" - }, - "updateStartingDownload": "İndirme başlıyor...", - "@updateStartingDownload": { - "description": "Update status - initializing" - }, - "updateDownloadFailed": "İndirme başarısız", - "@updateDownloadFailed": { - "description": "Update error title" - }, - "updateFailedMessage": "Güncelleme indirilemedi", - "@updateFailedMessage": { - "description": "Update error message" - }, - "updateNewVersionReady": "Yeni bir sürüm hazır", - "@updateNewVersionReady": { - "description": "Update subtitle" - }, - "updateCurrent": "Şimdiki", - "@updateCurrent": { - "description": "Label for current version" - }, - "updateNew": "Yeni", - "@updateNew": { - "description": "Label for new version" - }, - "updateDownloading": "İndiriliyor...", - "@updateDownloading": { - "description": "Update status - downloading" - }, - "updateWhatsNew": "Yenilikler", - "@updateWhatsNew": { - "description": "Changelog section title" - }, - "updateDownloadInstall": "İndir & Yükle", - "@updateDownloadInstall": { - "description": "Update button - download and install" - }, - "updateDontRemind": "Bir daha sorma", - "@updateDontRemind": { - "description": "Update button - skip this version" - }, - "providerPriorityTitle": "İndirme hizmetleri öncelik sırası", - "@providerPriorityTitle": { - "description": "Provider priority page title" - }, - "providerPriorityDescription": "İndirme hizmetlerini sıralamak için kaydır. Uygulama şarkı indirirken hizmetleri yukarıdan aşağıya doğru deneyecektir.", - "@providerPriorityDescription": { - "description": "Provider priority page description" - }, - "providerPriorityInfo": "Eğer bir şarkı ilk hizmette mevcut değilse uygulama otomatik olarak bir sonrakini deneyecektir.", - "@providerPriorityInfo": { - "description": "Info tip about fallback behavior" - }, - "providerBuiltIn": "Dahili", - "@providerBuiltIn": { - "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" - }, - "providerExtension": "Eklenti", - "@providerExtension": { - "description": "Label for extension-provided providers" - }, - "metadataProviderPriorityTitle": "Metadata Priority", - "@metadataProviderPriorityTitle": { - "description": "Metadata priority page title" - }, - "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", - "@metadataProviderPriorityDescription": { - "description": "Metadata priority page description" - }, - "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", - "@metadataProviderPriorityInfo": { - "description": "Info tip about rate limits" - }, - "metadataNoRateLimits": "No rate limits", - "@metadataNoRateLimits": { - "description": "Deezer provider description" - }, - "metadataMayRateLimit": "May rate limit", - "@metadataMayRateLimit": { - "description": "Spotify provider description" - }, - "logTitle": "Kayıtlar", - "@logTitle": { - "description": "Logs screen title" - }, - "logCopied": "Kayıtlar panoya kopyalandı", - "@logCopied": { - "description": "Snackbar - logs copied" - }, - "logSearchHint": "Kayıtları Ara...", - "@logSearchHint": { - "description": "Log search placeholder" - }, - "logFilterLevel": "Seviye", - "@logFilterLevel": { - "description": "Filter by log level" - }, - "logFilterSection": "Filtre", - "@logFilterSection": { - "description": "Filter section title" - }, - "logShareLogs": "Kayıtları paylaş", - "@logShareLogs": { - "description": "Share button tooltip" - }, - "logClearLogs": "Kayıtları temizle", - "@logClearLogs": { - "description": "Clear button tooltip" - }, - "logClearLogsTitle": "Kayıtları temizle", - "@logClearLogsTitle": { - "description": "Clear logs dialog title" - }, - "logClearLogsMessage": "Tüm kayıtları temizlemek istediğinize emin misiniz?", - "@logClearLogsMessage": { - "description": "Clear logs confirmation message" - }, - "logFilterBySeverity": "Filter logs by severity", - "@logFilterBySeverity": { - "description": "Filter dialog title" - }, - "logNoLogsYet": "No logs yet", - "@logNoLogsYet": { - "description": "Empty state title" - }, - "logNoLogsYetSubtitle": "Logs will appear here as you use the app", - "@logNoLogsYetSubtitle": { - "description": "Empty state subtitle" - }, - "logEntriesFiltered": "Entries ({count} filtered)", - "@logEntriesFiltered": { - "description": "Log count with filter active", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "logEntries": "Entries ({count})", - "@logEntries": { - "description": "Total log count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "credentialsTitle": "Spotify Credentials", - "@credentialsTitle": { - "description": "Credentials dialog title" - }, - "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", - "@credentialsDescription": { - "description": "Credentials dialog explanation" - }, - "credentialsClientId": "Client ID", - "@credentialsClientId": { - "description": "Client ID field label - DO NOT TRANSLATE" - }, - "credentialsClientIdHint": "Paste Client ID", - "@credentialsClientIdHint": { - "description": "Client ID placeholder" - }, - "credentialsClientSecret": "Client Secret", - "@credentialsClientSecret": { - "description": "Client Secret field label - DO NOT TRANSLATE" - }, - "credentialsClientSecretHint": "Paste Client Secret", - "@credentialsClientSecretHint": { - "description": "Client Secret placeholder" - }, - "channelStable": "Stable", - "@channelStable": { - "description": "Update channel - stable releases" - }, - "channelPreview": "Preview", - "@channelPreview": { - "description": "Update channel - beta/preview releases" - }, - "sectionSearchSource": "Search Source", - "@sectionSearchSource": { - "description": "Settings section header" - }, - "sectionDownload": "Download", - "@sectionDownload": { - "description": "Settings section header" - }, - "sectionPerformance": "Performance", - "@sectionPerformance": { - "description": "Settings section header" - }, - "sectionApp": "App", - "@sectionApp": { - "description": "Settings section header" - }, - "sectionData": "Data", - "@sectionData": { - "description": "Settings section header" - }, - "sectionDebug": "Debug", - "@sectionDebug": { - "description": "Settings section header" - }, - "sectionService": "Service", - "@sectionService": { - "description": "Settings section header" - }, - "sectionAudioQuality": "Audio Quality", - "@sectionAudioQuality": { - "description": "Settings section header" - }, - "sectionFileSettings": "File Settings", - "@sectionFileSettings": { - "description": "Settings section header" - }, - "sectionLyrics": "Lyrics", - "@sectionLyrics": { - "description": "Settings section header" - }, - "lyricsMode": "Lyrics Mode", - "@lyricsMode": { - "description": "Setting - how to save lyrics" - }, - "lyricsModeDescription": "Choose how lyrics are saved with your downloads", - "@lyricsModeDescription": { - "description": "Lyrics mode picker description" - }, - "lyricsModeEmbed": "Embed in file", - "@lyricsModeEmbed": { - "description": "Lyrics mode option - embed in audio file" - }, - "lyricsModeEmbedSubtitle": "Lyrics stored inside FLAC metadata", - "@lyricsModeEmbedSubtitle": { - "description": "Subtitle for embed option" - }, - "lyricsModeExternal": "External .lrc file", - "@lyricsModeExternal": { - "description": "Lyrics mode option - separate LRC file" - }, - "lyricsModeExternalSubtitle": "Separate .lrc file for players like Samsung Music", - "@lyricsModeExternalSubtitle": { - "description": "Subtitle for external option" - }, - "lyricsModeBoth": "Both", - "@lyricsModeBoth": { - "description": "Lyrics mode option - embed and external" - }, - "lyricsModeBothSubtitle": "Embed and save .lrc file", - "@lyricsModeBothSubtitle": { - "description": "Subtitle for both option" - }, - "sectionColor": "Color", - "@sectionColor": { - "description": "Settings section header" - }, - "sectionTheme": "Theme", - "@sectionTheme": { - "description": "Settings section header" - }, - "sectionLayout": "Layout", - "@sectionLayout": { - "description": "Settings section header" - }, - "sectionLanguage": "Language", - "@sectionLanguage": { - "description": "Settings section header for language" - }, - "appearanceLanguage": "App Language", - "@appearanceLanguage": { - "description": "Language setting title" - }, - "settingsAppearanceSubtitle": "Theme, colors, display", - "@settingsAppearanceSubtitle": { - "description": "Appearance settings description" - }, - "settingsDownloadSubtitle": "Service, quality, filename format", - "@settingsDownloadSubtitle": { - "description": "Download settings description" - }, - "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", - "@settingsOptionsSubtitle": { - "description": "Options settings description" - }, - "settingsExtensionsSubtitle": "Manage download providers", - "@settingsExtensionsSubtitle": { - "description": "Extensions settings description" - }, - "settingsLogsSubtitle": "View app logs for debugging", - "@settingsLogsSubtitle": { - "description": "Logs settings description" - }, - "loadingSharedLink": "Loading shared link...", - "@loadingSharedLink": { - "description": "Status when opening shared URL" - }, - "pressBackAgainToExit": "Press back again to exit", - "@pressBackAgainToExit": { - "description": "Exit confirmation message" - }, - "downloadAllCount": "Download All ({count})", - "@downloadAllCount": { - "description": "Download all button with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", - "@tracksCount": { - "description": "Track count display", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "trackCopyFilePath": "Copy file path", - "@trackCopyFilePath": { - "description": "Action - copy file path" - }, - "trackRemoveFromDevice": "Remove from device", - "@trackRemoveFromDevice": { - "description": "Action - delete downloaded file" - }, - "trackLoadLyrics": "Load Lyrics", - "@trackLoadLyrics": { - "description": "Action - fetch lyrics" - }, - "trackMetadata": "Metadata", - "@trackMetadata": { - "description": "Tab title - track metadata" - }, - "trackFileInfo": "File Info", - "@trackFileInfo": { - "description": "Tab title - file information" - }, - "trackLyrics": "Lyrics", - "@trackLyrics": { - "description": "Tab title - lyrics" - }, - "trackFileNotFound": "File not found", - "@trackFileNotFound": { - "description": "Error - file doesn't exist" - }, - "trackOpenInDeezer": "Open in Deezer", - "@trackOpenInDeezer": { - "description": "Action - open track in Deezer app" - }, - "trackOpenInSpotify": "Open in Spotify", - "@trackOpenInSpotify": { - "description": "Action - open track in Spotify app" - }, - "trackTrackName": "Track name", - "@trackTrackName": { - "description": "Metadata label - track title" - }, - "trackArtist": "Artist", - "@trackArtist": { - "description": "Metadata label - artist name" - }, - "trackAlbumArtist": "Album artist", - "@trackAlbumArtist": { - "description": "Metadata label - album artist" - }, - "trackAlbum": "Album", - "@trackAlbum": { - "description": "Metadata label - album name" - }, - "trackTrackNumber": "Track number", - "@trackTrackNumber": { - "description": "Metadata label - track number" - }, - "trackDiscNumber": "Disc number", - "@trackDiscNumber": { - "description": "Metadata label - disc number" - }, - "trackDuration": "Duration", - "@trackDuration": { - "description": "Metadata label - track length" - }, - "trackAudioQuality": "Audio quality", - "@trackAudioQuality": { - "description": "Metadata label - audio quality" - }, - "trackReleaseDate": "Release date", - "@trackReleaseDate": { - "description": "Metadata label - release date" - }, - "trackGenre": "Genre", - "@trackGenre": { - "description": "Metadata label - music genre" - }, - "trackLabel": "Label", - "@trackLabel": { - "description": "Metadata label - record label" - }, - "trackCopyright": "Copyright", - "@trackCopyright": { - "description": "Metadata label - copyright information" - }, - "trackDownloaded": "Downloaded", - "@trackDownloaded": { - "description": "Metadata label - download date" - }, - "trackCopyLyrics": "Copy lyrics", - "@trackCopyLyrics": { - "description": "Action - copy lyrics to clipboard" - }, - "trackLyricsNotAvailable": "Lyrics not available for this track", - "@trackLyricsNotAvailable": { - "description": "Message when lyrics not found" - }, - "trackLyricsTimeout": "Request timed out. Try again later.", - "@trackLyricsTimeout": { - "description": "Message when lyrics request times out" - }, - "trackLyricsLoadFailed": "Failed to load lyrics", - "@trackLyricsLoadFailed": { - "description": "Message when lyrics loading fails" - }, - "trackEmbedLyrics": "Embed Lyrics", - "@trackEmbedLyrics": { - "description": "Action - embed lyrics into audio file" - }, - "trackLyricsEmbedded": "Lyrics embedded successfully", - "@trackLyricsEmbedded": { - "description": "Snackbar - lyrics saved to file" - }, - "trackInstrumental": "Instrumental track", - "@trackInstrumental": { - "description": "Message when track is instrumental (no lyrics)" - }, - "trackCopiedToClipboard": "Copied to clipboard", - "@trackCopiedToClipboard": { - "description": "Snackbar - content copied" - }, - "trackDeleteConfirmTitle": "Remove from device?", - "@trackDeleteConfirmTitle": { - "description": "Delete confirmation title" - }, - "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", - "@trackDeleteConfirmMessage": { - "description": "Delete confirmation message" - }, - "dateToday": "Today", - "@dateToday": { - "description": "Relative date - today" - }, - "dateYesterday": "Yesterday", - "@dateYesterday": { - "description": "Relative date - yesterday" - }, - "dateDaysAgo": "{count} days ago", - "@dateDaysAgo": { - "description": "Relative date - days ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dateWeeksAgo": "{count} weeks ago", - "@dateWeeksAgo": { - "description": "Relative date - weeks ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dateMonthsAgo": "{count} months ago", - "@dateMonthsAgo": { - "description": "Relative date - months ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "storeFilterAll": "All", - "@storeFilterAll": { - "description": "Store filter - all extensions" - }, - "storeFilterMetadata": "Metadata", - "@storeFilterMetadata": { - "description": "Store filter - metadata providers" - }, - "storeFilterDownload": "Download", - "@storeFilterDownload": { - "description": "Store filter - download providers" - }, - "storeFilterUtility": "Utility", - "@storeFilterUtility": { - "description": "Store filter - utility extensions" - }, - "storeFilterLyrics": "Lyrics", - "@storeFilterLyrics": { - "description": "Store filter - lyrics providers" - }, - "storeFilterIntegration": "Integration", - "@storeFilterIntegration": { - "description": "Store filter - integrations" - }, - "storeClearFilters": "Clear filters", - "@storeClearFilters": { - "description": "Button to clear all filters" - }, - "extensionDefaultProvider": "Default (Deezer/Spotify)", - "@extensionDefaultProvider": { - "description": "Default search provider option" - }, - "extensionDefaultProviderSubtitle": "Use built-in search", - "@extensionDefaultProviderSubtitle": { - "description": "Subtitle for default provider" - }, - "extensionAuthor": "Author", - "@extensionAuthor": { - "description": "Extension detail - author" - }, - "extensionId": "ID", - "@extensionId": { - "description": "Extension detail - unique ID" - }, - "extensionError": "Error", - "@extensionError": { - "description": "Extension detail - error message" - }, - "extensionCapabilities": "Capabilities", - "@extensionCapabilities": { - "description": "Section header - extension features" - }, - "extensionMetadataProvider": "Metadata Provider", - "@extensionMetadataProvider": { - "description": "Capability - provides metadata" - }, - "extensionDownloadProvider": "Download Provider", - "@extensionDownloadProvider": { - "description": "Capability - provides downloads" - }, - "extensionLyricsProvider": "Lyrics Provider", - "@extensionLyricsProvider": { - "description": "Capability - provides lyrics" - }, - "extensionUrlHandler": "URL Handler", - "@extensionUrlHandler": { - "description": "Capability - handles URLs" - }, - "extensionQualityOptions": "Quality Options", - "@extensionQualityOptions": { - "description": "Capability - quality selection" - }, - "extensionPostProcessingHooks": "Post-Processing Hooks", - "@extensionPostProcessingHooks": { - "description": "Capability - post-processing" - }, - "extensionPermissions": "Permissions", - "@extensionPermissions": { - "description": "Section header - required permissions" - }, - "extensionSettings": "Settings", - "@extensionSettings": { - "description": "Section header - extension settings" - }, - "extensionRemoveButton": "Remove Extension", - "@extensionRemoveButton": { - "description": "Button to uninstall extension" - }, - "extensionUpdated": "Updated", - "@extensionUpdated": { - "description": "Extension detail - last update" - }, - "extensionMinAppVersion": "Min App Version", - "@extensionMinAppVersion": { - "description": "Extension detail - minimum app version" - }, - "extensionCustomTrackMatching": "Custom Track Matching", - "@extensionCustomTrackMatching": { - "description": "Capability - custom track matching algorithm" - }, - "extensionPostProcessing": "Post-Processing", - "@extensionPostProcessing": { - "description": "Capability - post-download processing" - }, - "extensionHooksAvailable": "{count} hook(s) available", - "@extensionHooksAvailable": { - "description": "Post-processing hooks count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "extensionPatternsCount": "{count} pattern(s)", - "@extensionPatternsCount": { - "description": "URL patterns count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "extensionStrategy": "Strategy: {strategy}", - "@extensionStrategy": { - "description": "Track matching strategy name", - "placeholders": { - "strategy": { - "type": "String" - } - } - }, - "extensionsProviderPrioritySection": "Provider Priority", - "@extensionsProviderPrioritySection": { - "description": "Section header - provider priority" - }, - "extensionsInstalledSection": "Installed Extensions", - "@extensionsInstalledSection": { - "description": "Section header - installed extensions" - }, - "extensionsNoExtensions": "No extensions installed", - "@extensionsNoExtensions": { - "description": "Empty state - no extensions" - }, - "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", - "@extensionsNoExtensionsSubtitle": { - "description": "Empty state subtitle" - }, - "extensionsInstallButton": "Install Extension", - "@extensionsInstallButton": { - "description": "Button to install extension from file" - }, - "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", - "@extensionsInfoTip": { - "description": "Security warning about extensions" - }, - "extensionsInstalledSuccess": "Extension installed successfully", - "@extensionsInstalledSuccess": { - "description": "Success message after install" - }, - "extensionsDownloadPriority": "Download Priority", - "@extensionsDownloadPriority": { - "description": "Setting - download provider order" - }, - "extensionsDownloadPrioritySubtitle": "Set download service order", - "@extensionsDownloadPrioritySubtitle": { - "description": "Subtitle for download priority" - }, - "extensionsNoDownloadProvider": "No extensions with download provider", - "@extensionsNoDownloadProvider": { - "description": "Empty state - no download providers" - }, - "extensionsMetadataPriority": "Metadata Priority", - "@extensionsMetadataPriority": { - "description": "Setting - metadata provider order" - }, - "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", - "@extensionsMetadataPrioritySubtitle": { - "description": "Subtitle for metadata priority" - }, - "extensionsNoMetadataProvider": "No extensions with metadata provider", - "@extensionsNoMetadataProvider": { - "description": "Empty state - no metadata providers" - }, - "extensionsSearchProvider": "Search Provider", - "@extensionsSearchProvider": { - "description": "Setting - search provider selection" - }, - "extensionsNoCustomSearch": "No extensions with custom search", - "@extensionsNoCustomSearch": { - "description": "Empty state - no search providers" - }, - "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", - "@extensionsSearchProviderDescription": { - "description": "Search provider setting description" - }, - "extensionsCustomSearch": "Custom search", - "@extensionsCustomSearch": { - "description": "Label for custom search provider" - }, - "extensionsErrorLoading": "Error loading extension", - "@extensionsErrorLoading": { - "description": "Error message when extension fails to load" - }, - "qualityFlacLossless": "FLAC Lossless", - "@qualityFlacLossless": { - "description": "Quality option - CD quality FLAC" - }, - "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", - "@qualityFlacLosslessSubtitle": { - "description": "Technical spec for lossless" - }, - "qualityHiResFlac": "Hi-Res FLAC", - "@qualityHiResFlac": { - "description": "Quality option - high resolution FLAC" - }, - "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", - "@qualityHiResFlacSubtitle": { - "description": "Technical spec for hi-res" - }, - "qualityHiResFlacMax": "Hi-Res FLAC Max", - "@qualityHiResFlacMax": { - "description": "Quality option - maximum resolution FLAC" - }, - "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", - "@qualityHiResFlacMaxSubtitle": { - "description": "Technical spec for hi-res max" - }, - "qualityNote": "Actual quality depends on track availability from the service", - "@qualityNote": { - "description": "Note about quality availability" - }, - "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", - "@youtubeQualityNote": { - "description": "Note for YouTube service explaining lossy-only quality" - }, - "youtubeOpusBitrateTitle": "YouTube Opus Bitrate", - "@youtubeOpusBitrateTitle": { - "description": "Title for YouTube Opus bitrate setting" - }, - "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate", - "@youtubeMp3BitrateTitle": { - "description": "Title for YouTube MP3 bitrate setting" - }, - "downloadAskBeforeDownload": "Ask Before Download", - "@downloadAskBeforeDownload": { - "description": "Setting - show quality picker" - }, - "downloadDirectory": "Download Directory", - "@downloadDirectory": { - "description": "Setting - download folder" - }, - "downloadSeparateSinglesFolder": "Separate Singles Folder", - "@downloadSeparateSinglesFolder": { - "description": "Setting - separate folder for singles" - }, - "downloadAlbumFolderStructure": "Album Folder Structure", - "@downloadAlbumFolderStructure": { - "description": "Setting - album folder organization" - }, - "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", - "@downloadUseAlbumArtistForFolders": { - "description": "Setting - choose whether artist folders use Album Artist or Track Artist" - }, - "downloadUsePrimaryArtistOnly": "Primary artist only for folders", - "@downloadUsePrimaryArtistOnly": { - "description": "Setting - strip featured artists from folder name" - }, - "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", - "@downloadUsePrimaryArtistOnlyEnabled": { - "description": "Subtitle when primary artist only is enabled" - }, - "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", - "@downloadUsePrimaryArtistOnlyDisabled": { - "description": "Subtitle when primary artist only is disabled" - }, - "downloadSelectQuality": "Select Quality", - "@downloadSelectQuality": { - "description": "Dialog title - choose audio quality" - }, - "downloadFrom": "Download From", - "@downloadFrom": { - "description": "Label - download source" - }, - "appearanceAmoledDark": "AMOLED Dark", - "@appearanceAmoledDark": { - "description": "Theme option - pure black" - }, - "appearanceAmoledDarkSubtitle": "Pure black background", - "@appearanceAmoledDarkSubtitle": { - "description": "Subtitle for AMOLED dark" - }, - "queueClearAll": "Clear All", - "@queueClearAll": { - "description": "Button - clear all queue items" - }, - "queueClearAllMessage": "Are you sure you want to clear all downloads?", - "@queueClearAllMessage": { - "description": "Clear queue confirmation" - }, - "settingsAutoExportFailed": "Auto-export failed downloads", - "@settingsAutoExportFailed": { - "description": "Setting toggle for auto-export" - }, - "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", - "@settingsAutoExportFailedSubtitle": { - "description": "Subtitle for auto-export setting" - }, - "settingsDownloadNetwork": "Download Network", - "@settingsDownloadNetwork": { - "description": "Setting for network type preference" - }, - "settingsDownloadNetworkAny": "WiFi + Mobile Data", - "@settingsDownloadNetworkAny": { - "description": "Network option - use any connection" - }, - "settingsDownloadNetworkWifiOnly": "WiFi Only", - "@settingsDownloadNetworkWifiOnly": { - "description": "Network option - only use WiFi" - }, - "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", - "@settingsDownloadNetworkSubtitle": { - "description": "Subtitle explaining network preference" - }, - "albumFolderArtistAlbum": "Artist / Album", - "@albumFolderArtistAlbum": { - "description": "Album folder option" - }, - "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", - "@albumFolderArtistAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderArtistYearAlbum": "Artist / [Year] Album", - "@albumFolderArtistYearAlbum": { - "description": "Album folder option with year" - }, - "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", - "@albumFolderArtistYearAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderAlbumOnly": "Album Only", - "@albumFolderAlbumOnly": { - "description": "Album folder option" - }, - "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", - "@albumFolderAlbumOnlySubtitle": { - "description": "Folder structure example" - }, - "albumFolderYearAlbum": "[Year] Album", - "@albumFolderYearAlbum": { - "description": "Album folder option with year" - }, - "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", - "@albumFolderYearAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderArtistAlbumSingles": "Artist / Album + Singles", - "@albumFolderArtistAlbumSingles": { - "description": "Album folder option with singles inside artist" - }, - "albumFolderArtistAlbumSinglesSubtitle": "Artist/Album/ and Artist/Singles/", - "@albumFolderArtistAlbumSinglesSubtitle": { - "description": "Folder structure example" - }, - "downloadedAlbumDeleteSelected": "Delete Selected", - "@downloadedAlbumDeleteSelected": { - "description": "Button - delete selected tracks" - }, - "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", - "@downloadedAlbumDeleteMessage": { - "description": "Delete confirmation with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumSelectedCount": "{count} selected", - "@downloadedAlbumSelectedCount": { - "description": "Selection count indicator", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumAllSelected": "All tracks selected", - "@downloadedAlbumAllSelected": { - "description": "Status - all items selected" - }, - "downloadedAlbumTapToSelect": "Tap tracks to select", - "@downloadedAlbumTapToSelect": { - "description": "Selection hint" - }, - "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", - "@downloadedAlbumDeleteCount": { - "description": "Delete button text with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumSelectToDelete": "Select tracks to delete", - "@downloadedAlbumSelectToDelete": { - "description": "Placeholder when nothing selected" - }, - "downloadedAlbumDiscHeader": "Disc {discNumber}", - "@downloadedAlbumDiscHeader": { - "description": "Header for disc separator in multi-disc albums", - "placeholders": { - "discNumber": { - "type": "int", - "example": "1" - } - } - }, - "recentTypeArtist": "Artist", - "@recentTypeArtist": { - "description": "Recent access item type - artist" - }, - "recentTypeAlbum": "Album", - "@recentTypeAlbum": { - "description": "Recent access item type - album" - }, - "recentTypeSong": "Song", - "@recentTypeSong": { - "description": "Recent access item type - song/track" - }, - "recentTypePlaylist": "Playlist", - "@recentTypePlaylist": { - "description": "Recent access item type - playlist" - }, - "recentEmpty": "No recent items yet", - "@recentEmpty": { - "description": "Empty state text for recent access list" - }, - "recentShowAllDownloads": "Show All Downloads", - "@recentShowAllDownloads": { - "description": "Button label to unhide hidden downloads in recent access" - }, - "recentPlaylistInfo": "Playlist: {name}", - "@recentPlaylistInfo": { - "description": "Snackbar message when tapping playlist in recent access", - "placeholders": { - "name": { - "type": "String", - "description": "Playlist name" - } - } - }, - "discographyDownload": "Download Discography", - "@discographyDownload": { - "description": "Button - download artist discography" - }, - "discographyDownloadAll": "Download All", - "@discographyDownloadAll": { - "description": "Option - download entire discography" - }, - "discographyDownloadAllSubtitle": "{count} tracks from {albumCount} releases", - "@discographyDownloadAllSubtitle": { - "description": "Subtitle showing total tracks and albums", - "placeholders": { - "count": { - "type": "int" - }, - "albumCount": { - "type": "int" - } - } - }, - "discographyAlbumsOnly": "Albums Only", - "@discographyAlbumsOnly": { - "description": "Option - download only albums" - }, - "discographyAlbumsOnlySubtitle": "{count} tracks from {albumCount} albums", - "@discographyAlbumsOnlySubtitle": { - "description": "Subtitle showing album tracks count", - "placeholders": { - "count": { - "type": "int" - }, - "albumCount": { - "type": "int" - } - } - }, - "discographySinglesOnly": "Singles & EPs Only", - "@discographySinglesOnly": { - "description": "Option - download only singles" - }, - "discographySinglesOnlySubtitle": "{count} tracks from {albumCount} singles", - "@discographySinglesOnlySubtitle": { - "description": "Subtitle showing singles tracks count", - "placeholders": { - "count": { - "type": "int" - }, - "albumCount": { - "type": "int" - } - } - }, - "discographySelectAlbums": "Select Albums...", - "@discographySelectAlbums": { - "description": "Option - manually select albums to download" - }, - "discographySelectAlbumsSubtitle": "Choose specific albums or singles", - "@discographySelectAlbumsSubtitle": { - "description": "Subtitle for select albums option" - }, - "discographyFetchingTracks": "Fetching tracks...", - "@discographyFetchingTracks": { - "description": "Progress - fetching album tracks" - }, - "discographyFetchingAlbum": "Fetching {current} of {total}...", - "@discographyFetchingAlbum": { - "description": "Progress - fetching specific album", - "placeholders": { - "current": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "discographySelectedCount": "{count} selected", - "@discographySelectedCount": { - "description": "Selection count badge", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "discographyDownloadSelected": "Download Selected", - "@discographyDownloadSelected": { - "description": "Button - download selected albums" - }, - "discographyAddedToQueue": "Added {count} tracks to queue", - "@discographyAddedToQueue": { - "description": "Snackbar - tracks added from discography", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "discographySkippedDownloaded": "{added} added, {skipped} already downloaded", - "@discographySkippedDownloaded": { - "description": "Snackbar - with skipped tracks count", - "placeholders": { - "added": { - "type": "int" - }, - "skipped": { - "type": "int" - } - } - }, - "discographyNoAlbums": "No albums available", - "@discographyNoAlbums": { - "description": "Error - no albums found for artist" - }, - "discographyFailedToFetch": "Failed to fetch some albums", - "@discographyFailedToFetch": { - "description": "Error - some albums failed to load" - }, - "sectionStorageAccess": "Storage Access", - "@sectionStorageAccess": { - "description": "Section header for storage access settings" - }, - "allFilesAccess": "All Files Access", - "@allFilesAccess": { - "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" - }, - "allFilesAccessEnabledSubtitle": "Can write to any folder", - "@allFilesAccessEnabledSubtitle": { - "description": "Subtitle when all files access is enabled" - }, - "allFilesAccessDisabledSubtitle": "Limited to media folders only", - "@allFilesAccessDisabledSubtitle": { - "description": "Subtitle when all files access is disabled" - }, - "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", - "@allFilesAccessDescription": { - "description": "Description explaining when to enable all files access" - }, - "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", - "@allFilesAccessDeniedMessage": { - "description": "Message when permission is permanently denied" - }, - "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", - "@allFilesAccessDisabledMessage": { - "description": "Snackbar message when user disables all files access" - }, - "settingsLocalLibrary": "Local Library", - "@settingsLocalLibrary": { - "description": "Settings menu item - local library" - }, - "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", - "@settingsLocalLibrarySubtitle": { - "description": "Subtitle for local library settings" - }, - "settingsCache": "Storage & Cache", - "@settingsCache": { - "description": "Settings menu item - cache management" - }, - "settingsCacheSubtitle": "View size and clear cached data", - "@settingsCacheSubtitle": { - "description": "Subtitle for cache management menu" - }, - "libraryTitle": "Local Library", - "@libraryTitle": { - "description": "Library settings page title" - }, - "libraryScanSettings": "Scan Settings", - "@libraryScanSettings": { - "description": "Section header for scan settings" - }, - "libraryEnableLocalLibrary": "Enable Local Library", - "@libraryEnableLocalLibrary": { - "description": "Toggle to enable library scanning" - }, - "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", - "@libraryEnableLocalLibrarySubtitle": { - "description": "Subtitle for enable toggle" - }, - "libraryFolder": "Library Folder", - "@libraryFolder": { - "description": "Folder selection setting" - }, - "libraryFolderHint": "Tap to select folder", - "@libraryFolderHint": { - "description": "Placeholder when no folder selected" - }, - "libraryShowDuplicateIndicator": "Show Duplicate Indicator", - "@libraryShowDuplicateIndicator": { - "description": "Toggle for duplicate indicator in search" - }, - "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", - "@libraryShowDuplicateIndicatorSubtitle": { - "description": "Subtitle for duplicate indicator toggle" - }, - "libraryActions": "Actions", - "@libraryActions": { - "description": "Section header for library actions" - }, - "libraryScan": "Scan Library", - "@libraryScan": { - "description": "Button to start library scan" - }, - "libraryScanSubtitle": "Scan for audio files", - "@libraryScanSubtitle": { - "description": "Subtitle for scan button" - }, - "libraryScanSelectFolderFirst": "Select a folder first", - "@libraryScanSelectFolderFirst": { - "description": "Message when trying to scan without folder" - }, - "libraryCleanupMissingFiles": "Cleanup Missing Files", - "@libraryCleanupMissingFiles": { - "description": "Button to remove entries for missing files" - }, - "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", - "@libraryCleanupMissingFilesSubtitle": { - "description": "Subtitle for cleanup button" - }, - "libraryClear": "Clear Library", - "@libraryClear": { - "description": "Button to clear all library entries" - }, - "libraryClearSubtitle": "Remove all scanned tracks", - "@libraryClearSubtitle": { - "description": "Subtitle for clear button" - }, - "libraryClearConfirmTitle": "Clear Library", - "@libraryClearConfirmTitle": { - "description": "Dialog title for clear confirmation" - }, - "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", - "@libraryClearConfirmMessage": { - "description": "Dialog message for clear confirmation" - }, - "libraryAbout": "About Local Library", - "@libraryAbout": { - "description": "Section header for about info" - }, - "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", - "@libraryAboutDescription": { - "description": "Description of local library feature" - }, - "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}", - "@libraryTracksUnit": { - "description": "Unit label for tracks count (without the number itself)", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "libraryLastScanned": "Last scanned: {time}", - "@libraryLastScanned": { - "description": "Last scan time display", - "placeholders": { - "time": { - "type": "String" - } - } - }, - "libraryLastScannedNever": "Never", - "@libraryLastScannedNever": { - "description": "Shown when library has never been scanned" - }, - "libraryScanning": "Scanning...", - "@libraryScanning": { - "description": "Status during scan" - }, - "libraryScanProgress": "{progress}% of {total} files", - "@libraryScanProgress": { - "description": "Scan progress display", - "placeholders": { - "progress": { - "type": "String" - }, - "total": { - "type": "int" - } - } - }, - "libraryInLibrary": "In Library", - "@libraryInLibrary": { - "description": "Badge shown on tracks that exist in local library" - }, - "libraryRemovedMissingFiles": "Removed {count} missing files from library", - "@libraryRemovedMissingFiles": { - "description": "Snackbar after cleanup", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "libraryCleared": "Library cleared", - "@libraryCleared": { - "description": "Snackbar after clearing library" - }, - "libraryStorageAccessRequired": "Storage Access Required", - "@libraryStorageAccessRequired": { - "description": "Dialog title for storage permission" - }, - "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", - "@libraryStorageAccessMessage": { - "description": "Dialog message for storage permission" - }, - "libraryFolderNotExist": "Selected folder does not exist", - "@libraryFolderNotExist": { - "description": "Error when folder doesn't exist" - }, - "librarySourceDownloaded": "Downloaded", - "@librarySourceDownloaded": { - "description": "Badge for tracks downloaded via SpotiFLAC" - }, - "librarySourceLocal": "Local", - "@librarySourceLocal": { - "description": "Badge for tracks from local library scan" - }, - "libraryFilterAll": "All", - "@libraryFilterAll": { - "description": "Filter chip - show all library items" - }, - "libraryFilterDownloaded": "Downloaded", - "@libraryFilterDownloaded": { - "description": "Filter chip - show only downloaded items" - }, - "libraryFilterLocal": "Local", - "@libraryFilterLocal": { - "description": "Filter chip - show only local library items" - }, - "libraryFilterTitle": "Filters", - "@libraryFilterTitle": { - "description": "Filter bottom sheet title" - }, - "libraryFilterReset": "Reset", - "@libraryFilterReset": { - "description": "Reset all filters button" - }, - "libraryFilterApply": "Apply", - "@libraryFilterApply": { - "description": "Apply filters button" - }, - "libraryFilterSource": "Source", - "@libraryFilterSource": { - "description": "Filter section - source type" - }, - "libraryFilterQuality": "Quality", - "@libraryFilterQuality": { - "description": "Filter section - audio quality" - }, - "libraryFilterQualityHiRes": "Hi-Res (24bit)", - "@libraryFilterQualityHiRes": { - "description": "Filter option - high resolution audio" - }, - "libraryFilterQualityCD": "CD (16bit)", - "@libraryFilterQualityCD": { - "description": "Filter option - CD quality audio" - }, - "libraryFilterQualityLossy": "Lossy", - "@libraryFilterQualityLossy": { - "description": "Filter option - lossy compressed audio" - }, - "libraryFilterFormat": "Format", - "@libraryFilterFormat": { - "description": "Filter section - file format" - }, - "libraryFilterSort": "Sort", - "@libraryFilterSort": { - "description": "Filter section - sort order" - }, - "libraryFilterSortLatest": "Latest", - "@libraryFilterSortLatest": { - "description": "Sort option - newest first" - }, - "libraryFilterSortOldest": "Oldest", - "@libraryFilterSortOldest": { - "description": "Sort option - oldest first" - }, - "timeJustNow": "Just now", - "@timeJustNow": { - "description": "Relative time - less than a minute ago" - }, - "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", - "@timeMinutesAgo": { - "description": "Relative time - minutes ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", - "@timeHoursAgo": { - "description": "Relative time - hours ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", - "@tutorialWelcomeTitle": { - "description": "Tutorial welcome page title" - }, - "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", - "@tutorialWelcomeDesc": { - "description": "Tutorial welcome page description" - }, - "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", - "@tutorialWelcomeTip1": { - "description": "Tutorial welcome tip 1" - }, - "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", - "@tutorialWelcomeTip2": { - "description": "Tutorial welcome tip 2" - }, - "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", - "@tutorialWelcomeTip3": { - "description": "Tutorial welcome tip 3" - }, - "tutorialSearchTitle": "Finding Music", - "@tutorialSearchTitle": { - "description": "Tutorial search page title" - }, - "tutorialSearchDesc": "There are two easy ways to find music you want to download.", - "@tutorialSearchDesc": { - "description": "Tutorial search page description" - }, - "tutorialDownloadTitle": "Downloading Music", - "@tutorialDownloadTitle": { - "description": "Tutorial download page title" - }, - "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", - "@tutorialDownloadDesc": { - "description": "Tutorial download page description" - }, - "tutorialLibraryTitle": "Your Library", - "@tutorialLibraryTitle": { - "description": "Tutorial library page title" - }, - "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", - "@tutorialLibraryDesc": { - "description": "Tutorial library page description" - }, - "tutorialLibraryTip1": "View download progress and queue in the Library tab", - "@tutorialLibraryTip1": { - "description": "Tutorial library tip 1" - }, - "tutorialLibraryTip2": "Tap any track to play it with your music player", - "@tutorialLibraryTip2": { - "description": "Tutorial library tip 2" - }, - "tutorialLibraryTip3": "Switch between list and grid view for better browsing", - "@tutorialLibraryTip3": { - "description": "Tutorial library tip 3" - }, - "tutorialExtensionsTitle": "Extensions", - "@tutorialExtensionsTitle": { - "description": "Tutorial extensions page title" - }, - "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", - "@tutorialExtensionsDesc": { - "description": "Tutorial extensions page description" - }, - "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", - "@tutorialExtensionsTip1": { - "description": "Tutorial extensions tip 1" - }, - "tutorialExtensionsTip2": "Add new download providers or search sources", - "@tutorialExtensionsTip2": { - "description": "Tutorial extensions tip 2" - }, - "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", - "@tutorialExtensionsTip3": { - "description": "Tutorial extensions tip 3" - }, - "tutorialSettingsTitle": "Customize Your Experience", - "@tutorialSettingsTitle": { - "description": "Tutorial settings page title" - }, - "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", - "@tutorialSettingsDesc": { - "description": "Tutorial settings page description" - }, - "tutorialSettingsTip1": "Change download location and folder organization", - "@tutorialSettingsTip1": { - "description": "Tutorial settings tip 1" - }, - "tutorialSettingsTip2": "Set default audio quality and format preferences", - "@tutorialSettingsTip2": { - "description": "Tutorial settings tip 2" - }, - "tutorialSettingsTip3": "Customize app theme and appearance", - "@tutorialSettingsTip3": { - "description": "Tutorial settings tip 3" - }, - "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", - "@tutorialReadyMessage": { - "description": "Tutorial completion message" - }, - "libraryForceFullScan": "Force Full Scan", - "@libraryForceFullScan": { - "description": "Button to force a complete rescan of library" - }, - "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", - "@libraryForceFullScanSubtitle": { - "description": "Subtitle for force full scan button" - }, - "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", - "@cleanupOrphanedDownloads": { - "description": "Button to remove history entries for deleted files" - }, - "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", - "@cleanupOrphanedDownloadsSubtitle": { - "description": "Subtitle for orphaned cleanup button" - }, - "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", - "@cleanupOrphanedDownloadsResult": { - "description": "Snackbar after orphan cleanup", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "cleanupOrphanedDownloadsNone": "No orphaned entries found", - "@cleanupOrphanedDownloadsNone": { - "description": "Snackbar when no orphans found" - }, - "cacheTitle": "Storage & Cache", - "@cacheTitle": { - "description": "Cache management page title" - }, - "cacheSummaryTitle": "Cache overview", - "@cacheSummaryTitle": { - "description": "Heading for cache summary card" - }, - "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", - "@cacheSummarySubtitle": { - "description": "Helper text for cache summary card" - }, - "cacheEstimatedTotal": "Estimated cache usage: {size}", - "@cacheEstimatedTotal": { - "description": "Total cache size shown in summary", - "placeholders": { - "size": { - "type": "String" - } - } - }, - "cacheSectionStorage": "Cached Data", - "@cacheSectionStorage": { - "description": "Section header for cache entries" - }, - "cacheSectionMaintenance": "Maintenance", - "@cacheSectionMaintenance": { - "description": "Section header for cleanup actions" - }, - "cacheAppDirectory": "App cache directory", - "@cacheAppDirectory": { - "description": "Cache item title for app cache directory" - }, - "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", - "@cacheAppDirectoryDesc": { - "description": "Description of what app cache directory contains" - }, - "cacheTempDirectory": "Temporary directory", - "@cacheTempDirectory": { - "description": "Cache item title for temporary files directory" - }, - "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", - "@cacheTempDirectoryDesc": { - "description": "Description of what temporary directory contains" - }, - "cacheCoverImage": "Cover image cache", - "@cacheCoverImage": { - "description": "Cache item title for persistent cover images" - }, - "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", - "@cacheCoverImageDesc": { - "description": "Description of what cover image cache contains" - }, - "cacheLibraryCover": "Library cover cache", - "@cacheLibraryCover": { - "description": "Cache item title for local library cover art images" - }, - "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", - "@cacheLibraryCoverDesc": { - "description": "Description of what library cover cache contains" - }, - "cacheExploreFeed": "Explore feed cache", - "@cacheExploreFeed": { - "description": "Cache item title for explore home feed cache" - }, - "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", - "@cacheExploreFeedDesc": { - "description": "Description of what explore feed cache contains" - }, - "cacheTrackLookup": "Track lookup cache", - "@cacheTrackLookup": { - "description": "Cache item title for track ID lookup cache" - }, - "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", - "@cacheTrackLookupDesc": { - "description": "Description of what track lookup cache contains" - }, - "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", - "@cacheCleanupUnusedDesc": { - "description": "Description of what cleanup unused data does" - }, - "cacheNoData": "No cached data", - "@cacheNoData": { - "description": "Label when cache category has no data" - }, - "cacheSizeWithFiles": "{size} in {count} files", - "@cacheSizeWithFiles": { - "description": "Cache size and file count", - "placeholders": { - "size": { - "type": "String" - }, - "count": { - "type": "int" - } - } - }, - "cacheSizeOnly": "{size}", - "@cacheSizeOnly": { - "description": "Cache size only", - "placeholders": { - "size": { - "type": "String" - } - } - }, - "cacheEntries": "{count} entries", - "@cacheEntries": { - "description": "Track cache entry count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "cacheClearSuccess": "Cleared: {target}", - "@cacheClearSuccess": { - "description": "Snackbar after clearing selected cache", - "placeholders": { - "target": { - "type": "String" - } - } - }, - "cacheClearConfirmTitle": "Clear cache?", - "@cacheClearConfirmTitle": { - "description": "Dialog title before clearing one cache category" - }, - "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", - "@cacheClearConfirmMessage": { - "description": "Dialog message before clearing selected cache", - "placeholders": { - "target": { - "type": "String" - } - } - }, - "cacheClearAllConfirmTitle": "Clear all cache?", - "@cacheClearAllConfirmTitle": { - "description": "Dialog title before clearing all caches" - }, - "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", - "@cacheClearAllConfirmMessage": { - "description": "Dialog message before clearing all caches" - }, - "cacheClearAll": "Clear all cache", - "@cacheClearAll": { - "description": "Button label to clear all caches" - }, - "cacheCleanupUnused": "Cleanup unused data", - "@cacheCleanupUnused": { - "description": "Action title for cleaning unused entries" - }, - "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", - "@cacheCleanupUnusedSubtitle": { - "description": "Subtitle for cleanup unused data action" - }, - "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", - "@cacheCleanupResult": { - "description": "Snackbar after unused data cleanup", - "placeholders": { - "downloadCount": { - "type": "int" - }, - "libraryCount": { - "type": "int" - } - } - }, - "cacheRefreshStats": "Refresh stats", - "@cacheRefreshStats": { - "description": "Button label to refresh cache statistics" - }, - "trackSaveCoverArt": "Save Cover Art", - "@trackSaveCoverArt": { - "description": "Menu action - save album cover art as file" - }, - "trackSaveCoverArtSubtitle": "Save album art as .jpg file", - "@trackSaveCoverArtSubtitle": { - "description": "Subtitle for save cover art action" - }, - "trackSaveLyrics": "Save Lyrics (.lrc)", - "@trackSaveLyrics": { - "description": "Menu action - save lyrics as .lrc file" - }, - "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", - "@trackSaveLyricsSubtitle": { - "description": "Subtitle for save lyrics action" - }, - "trackSaveLyricsProgress": "Saving lyrics...", - "@trackSaveLyricsProgress": { - "description": "Snackbar while saving lyrics to file" - }, - "trackReEnrich": "Re-enrich", - "@trackReEnrich": { - "description": "Menu action - re-embed metadata into audio file" - }, - "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", - "@trackReEnrichOnlineSubtitle": { - "description": "Subtitle for re-enrich metadata action for local items" - }, - "trackEditMetadata": "Edit Metadata", - "@trackEditMetadata": { - "description": "Menu action - edit embedded metadata" - }, - "trackCoverSaved": "Cover art saved to {fileName}", - "@trackCoverSaved": { - "description": "Snackbar after cover art saved", - "placeholders": { - "fileName": { - "type": "String" - } - } - }, - "trackCoverNoSource": "No cover art source available", - "@trackCoverNoSource": { - "description": "Snackbar when no cover art URL or embedded cover" - }, - "trackLyricsSaved": "Lyrics saved to {fileName}", - "@trackLyricsSaved": { - "description": "Snackbar after lyrics saved", - "placeholders": { - "fileName": { - "type": "String" - } - } - }, - "trackReEnrichProgress": "Re-enriching metadata...", - "@trackReEnrichProgress": { - "description": "Snackbar while re-enriching metadata" - }, - "trackReEnrichSearching": "Searching metadata online...", - "@trackReEnrichSearching": { - "description": "Snackbar while searching metadata from internet for local items" - }, - "trackReEnrichSuccess": "Metadata re-enriched successfully", - "@trackReEnrichSuccess": { - "description": "Snackbar after successful re-enrichment" - }, - "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", - "@trackReEnrichFfmpegFailed": { - "description": "Snackbar when FFmpeg embed fails for MP3/Opus" - }, - "trackSaveFailed": "Failed: {error}", - "@trackSaveFailed": { - "description": "Snackbar when save operation fails", - "placeholders": { - "error": { - "type": "String" - } - } - }, - "trackConvertFormat": "Convert Format", - "@trackConvertFormat": { - "description": "Menu item - convert audio format" - }, - "trackConvertFormatSubtitle": "Convert to MP3 or Opus", - "@trackConvertFormatSubtitle": { - "description": "Subtitle for convert format menu item" - }, - "trackConvertTitle": "Convert Audio", - "@trackConvertTitle": { - "description": "Title of convert bottom sheet" - }, - "trackConvertTargetFormat": "Target Format", - "@trackConvertTargetFormat": { - "description": "Label for format selection" - }, - "trackConvertBitrate": "Bitrate", - "@trackConvertBitrate": { - "description": "Label for bitrate selection" - }, - "trackConvertConfirmTitle": "Confirm Conversion", - "@trackConvertConfirmTitle": { - "description": "Confirmation dialog title" - }, - "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", - "@trackConvertConfirmMessage": { - "description": "Confirmation dialog message", - "placeholders": { - "sourceFormat": { - "type": "String" - }, - "targetFormat": { - "type": "String" - }, - "bitrate": { - "type": "String" - } - } - }, - "trackConvertConverting": "Converting audio...", - "@trackConvertConverting": { - "description": "Snackbar while converting" - }, - "trackConvertSuccess": "Converted to {format} successfully", - "@trackConvertSuccess": { - "description": "Snackbar after successful conversion", - "placeholders": { - "format": { - "type": "String" - } - } - }, - "trackConvertFailed": "Conversion failed", - "@trackConvertFailed": { - "description": "Snackbar when conversion fails" - }, - "actionCreate": "Create", - "@actionCreate": { - "description": "Generic action button - create" - }, - "collectionFoldersTitle": "My folders", - "@collectionFoldersTitle": { - "description": "Library section title for custom folders" - }, - "collectionWishlist": "Wishlist", - "@collectionWishlist": { - "description": "Custom folder for saved tracks to download later" - }, - "collectionLoved": "Loved", - "@collectionLoved": { - "description": "Custom folder for favorite tracks" - }, - "collectionPlaylists": "Playlists", - "@collectionPlaylists": { - "description": "Custom user playlists folder" - }, - "collectionPlaylist": "Playlist", - "@collectionPlaylist": { - "description": "Single playlist label" - }, - "collectionAddToPlaylist": "Add to playlist", - "@collectionAddToPlaylist": { - "description": "Action to add a track to user playlist" - }, - "collectionCreatePlaylist": "Create playlist", - "@collectionCreatePlaylist": { - "description": "Action to create a new playlist" - }, - "collectionNoPlaylistsYet": "No playlists yet", - "@collectionNoPlaylistsYet": { - "description": "Empty state title when user has no playlists" - }, - "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks", - "@collectionNoPlaylistsSubtitle": { - "description": "Empty state subtitle when user has no playlists" - }, - "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}", - "@collectionPlaylistTracks": { - "description": "Track count label for custom playlists", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "collectionAddedToPlaylist": "Added to \"{playlistName}\"", - "@collectionAddedToPlaylist": { - "description": "Snackbar after adding track to playlist", - "placeholders": { - "playlistName": { - "type": "String" - } - } - }, - "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"", - "@collectionAlreadyInPlaylist": { - "description": "Snackbar when track already exists in playlist", - "placeholders": { - "playlistName": { - "type": "String" - } - } - }, - "collectionPlaylistCreated": "Playlist created", - "@collectionPlaylistCreated": { - "description": "Snackbar after creating playlist" - }, - "collectionPlaylistNameHint": "Playlist name", - "@collectionPlaylistNameHint": { - "description": "Hint text for playlist name input" - }, - "collectionPlaylistNameRequired": "Playlist name is required", - "@collectionPlaylistNameRequired": { - "description": "Validation error for empty playlist name" - }, - "collectionRenamePlaylist": "Rename playlist", - "@collectionRenamePlaylist": { - "description": "Action to rename playlist" - }, - "collectionDeletePlaylist": "Delete playlist", - "@collectionDeletePlaylist": { - "description": "Action to delete playlist" - }, - "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?", - "@collectionDeletePlaylistMessage": { - "description": "Confirmation message for deleting playlist", - "placeholders": { - "playlistName": { - "type": "String" - } - } - }, - "collectionPlaylistDeleted": "Playlist deleted", - "@collectionPlaylistDeleted": { - "description": "Snackbar after deleting playlist" - }, - "collectionPlaylistRenamed": "Playlist renamed", - "@collectionPlaylistRenamed": { - "description": "Snackbar after renaming playlist" - }, - "collectionWishlistEmptyTitle": "Wishlist is empty", - "@collectionWishlistEmptyTitle": { - "description": "Wishlist empty state title" - }, - "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later", - "@collectionWishlistEmptySubtitle": { - "description": "Wishlist empty state subtitle" - }, - "collectionLovedEmptyTitle": "Loved folder is empty", - "@collectionLovedEmptyTitle": { - "description": "Loved empty state title" - }, - "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites", - "@collectionLovedEmptySubtitle": { - "description": "Loved empty state subtitle" - }, - "collectionPlaylistEmptyTitle": "Playlist is empty", - "@collectionPlaylistEmptyTitle": { - "description": "Playlist empty state title" - }, - "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here", - "@collectionPlaylistEmptySubtitle": { - "description": "Playlist empty state subtitle" - }, - "collectionRemoveFromPlaylist": "Remove from playlist", - "@collectionRemoveFromPlaylist": { - "description": "Tooltip for removing track from playlist" - }, - "collectionRemoveFromFolder": "Remove from folder", - "@collectionRemoveFromFolder": { - "description": "Tooltip for removing track from wishlist/loved folder" - }, - "collectionRemoved": "\"{trackName}\" removed", - "@collectionRemoved": { - "description": "Snackbar after removing a track from a collection", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionAddedToLoved": "\"{trackName}\" added to Loved", - "@collectionAddedToLoved": { - "description": "Snackbar after adding track to loved folder", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved", - "@collectionRemovedFromLoved": { - "description": "Snackbar after removing track from loved folder", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist", - "@collectionAddedToWishlist": { - "description": "Snackbar after adding track to wishlist", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist", - "@collectionRemovedFromWishlist": { - "description": "Snackbar after removing track from wishlist", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "trackOptionAddToLoved": "Add to Loved", - "@trackOptionAddToLoved": { - "description": "Bottom sheet action label - add track to loved folder" - }, - "trackOptionRemoveFromLoved": "Remove from Loved", - "@trackOptionRemoveFromLoved": { - "description": "Bottom sheet action label - remove track from loved folder" - }, - "trackOptionAddToWishlist": "Add to Wishlist", - "@trackOptionAddToWishlist": { - "description": "Bottom sheet action label - add track to wishlist" - }, - "trackOptionRemoveFromWishlist": "Remove from Wishlist", - "@trackOptionRemoveFromWishlist": { - "description": "Bottom sheet action label - remove track from wishlist" - }, - "collectionPlaylistChangeCover": "Change cover image", - "@collectionPlaylistChangeCover": { - "description": "Bottom sheet action to pick a custom cover image for a playlist" - }, - "collectionPlaylistRemoveCover": "Remove cover image", - "@collectionPlaylistRemoveCover": { - "description": "Bottom sheet action to remove custom cover image from a playlist" - }, - "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}", - "@selectionShareCount": { - "description": "Share button text with count in selection mode", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionShareNoFiles": "No shareable files found", - "@selectionShareNoFiles": { - "description": "Snackbar when no selected files exist on disk" - }, - "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}", - "@selectionConvertCount": { - "description": "Convert button text with count in selection mode", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionConvertNoConvertible": "No convertible tracks selected", - "@selectionConvertNoConvertible": { - "description": "Snackbar when no selected tracks support conversion" - }, - "selectionBatchConvertConfirmTitle": "Batch Convert", - "@selectionBatchConvertConfirmTitle": { - "description": "Confirmation dialog title for batch conversion" - }, - "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.", - "@selectionBatchConvertConfirmMessage": { - "description": "Confirmation dialog message for batch conversion", - "placeholders": { - "count": { - "type": "int" - }, - "format": { - "type": "String" - }, - "bitrate": { - "type": "String" - } - } - }, - "selectionBatchConvertProgress": "Converting {current} of {total}...", - "@selectionBatchConvertProgress": { - "description": "Snackbar during batch conversion progress", - "placeholders": { - "current": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}", - "@selectionBatchConvertSuccess": { - "description": "Snackbar after batch conversion completes", - "placeholders": { - "success": { - "type": "int" - }, - "total": { - "type": "int" - }, - "format": { - "type": "String" - } - } - }, - "downloadedAlbumDownloadedCount": "{count} downloaded", - "@downloadedAlbumDownloadedCount": { - "description": "Downloaded tracks count badge", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", - "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { - "description": "Subtitle when Album Artist is used for folder naming" - }, - "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", - "@downloadUseAlbumArtistForFoldersTrackSubtitle": { - "description": "Subtitle when Track Artist is used for folder naming" - } -} \ No newline at end of file diff --git a/lib/l10n/arb/app_zh_CN.arb b/lib/l10n/arb/app_zh_CN.arb index 5b4dd487..d7a9c1aa 100644 --- a/lib/l10n/arb/app_zh_CN.arb +++ b/lib/l10n/arb/app_zh_CN.arb @@ -1,5 +1,5 @@ { - "@@locale": "zh-CN", + "@@locale": "zh_CN", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { diff --git a/lib/l10n/arb/app_zh_TW.arb b/lib/l10n/arb/app_zh_TW.arb index d33658af..b51686c0 100644 --- a/lib/l10n/arb/app_zh_TW.arb +++ b/lib/l10n/arb/app_zh_TW.arb @@ -1,5 +1,5 @@ { - "@@locale": "zh-TW", + "@@locale": "zh_TW", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 271f22f7..8ba3f33b 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -457,6 +457,19 @@ class TrackNotifier extends Notifier { return; } + // If URL doesn't match any known service, it's unrecognized + final isSpotifyUrl = url.contains('open.spotify.com') || + url.contains('spotify.link') || + url.startsWith('spotify:'); + if (!isSpotifyUrl) { + state = TrackState( + isLoading: false, + error: 'url_not_recognized', + hasSearchText: state.hasSearchText, + ); + return; + } + final parsed = await PlatformBridge.parseSpotifyUrl(url); if (!_isRequestValid(requestId)) return; diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index f1c3f807..65e8fcec 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -583,7 +583,25 @@ class _HomeTabState extends ConsumerState if (url.isEmpty) return; if (url.startsWith('http') || url.startsWith('spotify:')) { await ref.read(trackProvider.notifier).fetchFromUrl(url); - _navigateToDetailIfNeeded(); + final trackState = ref.read(trackProvider); + if (trackState.error != null && mounted) { + final l10n = context.l10n; + final errorMsg = trackState.error!; + final isRateLimit = errorMsg.contains('429') || + errorMsg.toLowerCase().contains('rate limit') || + errorMsg.toLowerCase().contains('too many requests'); + final displayMessage = errorMsg == 'url_not_recognized' + ? l10n.errorUrlNotRecognizedMessage + : isRateLimit + ? l10n.errorRateLimitedMessage + : l10n.errorUrlFetchFailed; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(displayMessage)), + ); + ref.read(trackProvider.notifier).clear(); + } else { + _navigateToDetailIfNeeded(); + } } else { final settings = ref.read(settingsProvider); await ref @@ -1262,7 +1280,8 @@ class _HomeTabState extends ConsumerState (searchArtists != null && searchArtists.isNotEmpty) || (searchAlbums != null && searchAlbums.isNotEmpty) || (searchPlaylists != null && searchPlaylists.isNotEmpty) || - isLoading; + isLoading || + error != null; return SliverMainAxisGroup( slivers: _buildSearchResults( @@ -2224,11 +2243,14 @@ class _HomeTabState extends ConsumerState } Widget _buildErrorWidget(String error, ColorScheme colorScheme) { + final l10n = context.l10n; final isRateLimit = error.contains('429') || error.toLowerCase().contains('rate limit') || error.toLowerCase().contains('too many requests'); + final isUrlNotRecognized = error == 'url_not_recognized'; + if (isRateLimit) { return Card( elevation: 0, @@ -2245,7 +2267,7 @@ class _HomeTabState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Rate Limited', + l10n.errorRateLimited, style: TextStyle( color: colorScheme.onErrorContainer, fontWeight: FontWeight.bold, @@ -2253,7 +2275,7 @@ class _HomeTabState extends ConsumerState ), const SizedBox(height: 4), Text( - 'Too many requests. Please wait a moment before searching again.', + l10n.errorRateLimitedMessage, style: TextStyle( color: colorScheme.onErrorContainer, fontSize: 12, @@ -2268,6 +2290,45 @@ class _HomeTabState extends ConsumerState ); } + if (isUrlNotRecognized) { + return Card( + elevation: 0, + color: colorScheme.errorContainer.withValues(alpha: 0.5), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(Icons.link_off, color: colorScheme.error), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.errorUrlNotRecognized, + style: TextStyle( + color: colorScheme.error, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + l10n.errorUrlNotRecognizedMessage, + style: TextStyle( + color: colorScheme.error, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + return Card( elevation: 0, color: colorScheme.errorContainer.withValues(alpha: 0.5), @@ -2279,7 +2340,10 @@ class _HomeTabState extends ConsumerState Icon(Icons.error_outline, color: colorScheme.error), const SizedBox(width: 12), Expanded( - child: Text(error, style: TextStyle(color: colorScheme.error)), + child: Text( + l10n.errorUrlFetchFailed, + style: TextStyle(color: colorScheme.error), + ), ), ], ), diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index e103c6a1..b77fee99 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -102,13 +102,29 @@ class _MainShellState extends ConsumerState { if (_currentIndex != 0) { _onNavTap(0); } - ref.read(trackProvider.notifier).fetchFromUrl(url); ref.read(settingsProvider.notifier).setHasSearchedBefore(); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(context.l10n.loadingSharedLink))); } + await ref.read(trackProvider.notifier).fetchFromUrl(url); + final trackState = ref.read(trackProvider); + if (trackState.error != null && mounted) { + final l10n = context.l10n; + final errorMsg = trackState.error!; + final isRateLimit = errorMsg.contains('429') || + errorMsg.toLowerCase().contains('rate limit') || + errorMsg.toLowerCase().contains('too many requests'); + final displayMessage = errorMsg == 'url_not_recognized' + ? l10n.errorUrlNotRecognizedMessage + : isRateLimit + ? l10n.errorRateLimitedMessage + : l10n.errorUrlFetchFailed; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(displayMessage)), + ); + } } Future _checkForUpdates() async { From 9c6856b584430291040ba3c560d8e7e3b6d46e10 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 10 Mar 2026 15:26:19 +0700 Subject: [PATCH 024/145] refactor: migrate to git-cliff for changelog and deduplicate library items - Replace manual CHANGELOG.md parsing with git-cliff action in release workflow - Add cliff.toml config for conventional commit grouping and GitHub integration - Extract buildPathMatchKeys into shared utility with Android storage alias support - Deduplicate local library items overlapping with downloaded items in queue tab --- .github/workflows/release.yml | 106 +++++++++------------- cliff.toml | 105 +++++++++++++++++++++ lib/providers/local_library_provider.dart | 78 ++-------------- lib/screens/queue_tab.dart | 35 ++++++- lib/utils/path_match_keys.dart | 104 +++++++++++++++++++++ 5 files changed, 292 insertions(+), 136 deletions(-) create mode 100644 cliff.toml create mode 100644 lib/utils/path_match_keys.dart diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04ade01e..1129349f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -309,32 +309,22 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 + with: + fetch-depth: 0 # Full history needed for git-cliff - - name: Extract changelog for version + - name: Generate changelog with git-cliff id: changelog + uses: orhun/git-cliff-action@v4 + with: + config: cliff.toml + args: --latest --strip header + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OUTPUT: /tmp/changelog.txt + + - name: Show generated changelog run: | - VERSION=${{ needs.get-version.outputs.version }} - VERSION_NUM=${VERSION#v} # Remove 'v' prefix - - echo "Looking for version: $VERSION_NUM" - - # Extract changelog section for this version using sed - # Find the line with version, then print until next version header or end - CHANGELOG=$(sed -n "/^## \[$VERSION_NUM\]/,/^## \[/{ /^## \[$VERSION_NUM\]/d; /^## \[/d; p; }" CHANGELOG.md) - - # If no changelog found, use default message - if [ -z "$CHANGELOG" ]; then - echo "No changelog found for version $VERSION_NUM" - CHANGELOG="See CHANGELOG.md for details." - else - echo "Found changelog content" - # Remove trailing --- separator if present (CHANGELOG uses --- between versions) - CHANGELOG=$(echo "$CHANGELOG" | sed '/^---$/d') - fi - - # Save to file for multiline support - echo "$CHANGELOG" > /tmp/changelog.txt - echo "Extracted changelog:" + echo "Generated changelog:" cat /tmp/changelog.txt - name: Download Android APK @@ -352,15 +342,13 @@ jobs: - name: Prepare release body run: | VERSION=${{ needs.get-version.outputs.version }} - cat > /tmp/release_body.txt << 'HEADER' - ### What's New - HEADER - - cat /tmp/changelog.txt >> /tmp/release_body.txt - REPO_OWNER="${{ github.repository_owner }}" REPO_NAME="${{ github.event.repository.name }}" - + + # Start with git-cliff changelog + cp /tmp/changelog.txt /tmp/release_body.txt + + # Append download section cat >> /tmp/release_body.txt << FOOTER --- @@ -404,6 +392,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Download Android APK uses: actions/download-artifact@v7 @@ -417,52 +407,40 @@ jobs: name: ios-ipa path: ./release - - name: Extract changelog for version + - name: Generate changelog with git-cliff for Telegram + uses: orhun/git-cliff-action@v4 + with: + config: cliff.toml + args: --latest --strip all + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OUTPUT: /tmp/cliff_tg.txt + + - name: Convert changelog for Telegram id: changelog run: | - VERSION=${{ needs.get-version.outputs.version }} - VERSION_NUM=${VERSION#v} - - # Extract changelog, limit to ~2500 chars for Telegram (4096 limit minus message overhead) - # Use tr -d '\r' to handle CRLF line endings from Windows - FULL_CHANGELOG=$(cat CHANGELOG.md | tr -d '\r' | sed -n "/^## \[$VERSION_NUM\]/,/^## \[/{ /^## \[$VERSION_NUM\]/d; /^## \[/d; p; }" | sed '/^---$/d') - - echo "DEBUG: Extracted changelog length: ${#FULL_CHANGELOG}" - echo "DEBUG: First 200 chars: ${FULL_CHANGELOG:0:200}" - - if [ -z "$FULL_CHANGELOG" ]; then - CHANGELOG="See release notes on GitHub for details." + if [ ! -s /tmp/cliff_tg.txt ]; then + echo "See release notes on GitHub for details." > /tmp/changelog.txt else - # Convert GitHub Markdown to Telegram HTML: - # - **text** → text - # - `code` → code - # - ### Header → Header - # - Escape HTML special chars first - # - Remove > blockquote prefix - CHANGELOG=$(echo "$FULL_CHANGELOG" | \ - sed 's/^> //' | \ + # Convert Markdown to Telegram HTML + CHANGELOG=$(cat /tmp/cliff_tg.txt | \ + sed '/^\*\*Full Changelog\*\*/d' | \ + sed 's/\[#\([0-9]*\)\]([^)]*)/#\1/g' | \ + sed 's/\[@\([^]]*\)\]([^)]*)/@\1/g' | \ sed 's/&/\&/g' | \ sed 's//\>/g' | \ - sed 's/`\([^`]*\)`/\1<\/code>/g' | \ sed 's/\*\*\([^*]*\)\*\*/\1<\/b>/g' | \ sed 's/^### \(.*\)$/\1<\/b>/g' | \ sed 's/^## \(.*\)$/\1<\/b>/g' | \ - sed 's/^- /• /g' | \ - sed 's/^ - / ◦ /g') - - # Take first 2500 characters, then cut at last complete line + sed 's/^- /• /g') + + # Truncate for Telegram 4096 char limit CHANGELOG=$(echo "$CHANGELOG" | head -c 2500 | sed '$d') - - # Check if truncated - FULL_LEN=${#FULL_CHANGELOG} - if [ $FULL_LEN -gt 2500 ]; then - CHANGELOG="${CHANGELOG}"$'\n\n... (see full changelog on GitHub)' - fi + echo "$CHANGELOG" > /tmp/changelog.txt fi - echo "$CHANGELOG" > /tmp/changelog.txt - echo "DEBUG: Final changelog:" + echo "Telegram changelog:" cat /tmp/changelog.txt - name: Send to Telegram Channel diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..0509818b --- /dev/null +++ b/cliff.toml @@ -0,0 +1,105 @@ +# git-cliff configuration for SpotiFLAC Mobile +# https://git-cliff.org/docs/configuration + +[changelog] +# Template for the changelog body +body = """ +{%- macro remote_url() -%} + https://github.com/zarzet/SpotiFLAC-Mobile +{%- endmacro -%} + +{% if version %}\ + ## {{ version | trim_start_matches(pat="v") }} +{% else %}\ + ## Unreleased +{% endif %}\ + +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}**{{ commit.scope }}**: {% endif %}\ + {{ commit.message | upper_first }}\ + {% if commit.github.pr_number %} \ + ([#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}))\ + {% endif %}\ + {%- if commit.github.username %} by [@{{ commit.github.username }}](https://github.com/{{ commit.github.username }}){%- endif %} + {%- endfor %} +{% endfor %} + +{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} + + ### New Contributors +{%- for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} + * @{{ contributor.username }} made their first contribution + {%- if contributor.pr_number %} in \ + [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ + {%- endif %} +{%- endfor %} +{%- endif -%} + +{% if version %} + {% if previous.version %} + **Full Changelog**: [{{ previous.version }}...{{ version }}]({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}) + {% endif %} +{% else -%} + {% raw %}\n{% endraw %} +{% endif %} +""" +# Remove leading and trailing whitespace +trim = true + +[git] +# Parse conventional commits +conventional_commits = true +filter_unconventional = true + +# Process each line of a commit as an individual commit +split_commits = false + +# Regex for preprocessing the commit messages +commit_preprocessors = [ + # Remove PR number from message (we add it back via GitHub integration) + { pattern = '\(#(\d+)\)', replace = '' }, + # Strip conventional commit prefix for cleaner messages + # (group header already shows the type) +] + +# Regex for parsing and grouping commits +commit_parsers = [ + # Skip noise: translation commits from Crowdin + { message = "^New translations", skip = true }, + { message = "^Update source file", skip = true }, + # Skip merge commits + { message = "^Merge", skip = true }, + # Skip version bump commits + { message = "^v\\d+", skip = true }, + { message = "^chore: update VirusTotal", skip = true }, + + # Group by conventional commit type + { message = "^feat", group = "New Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^doc", group = "Documentation" }, + { message = "^style", group = "Styling" }, + { message = "^test", group = "Testing" }, + { message = "^chore\\(deps\\)", group = "Dependencies" }, + { message = "^chore\\(l10n\\)", skip = true }, + { message = "^chore|^ci", group = "Chores" }, +] + +# Protect breaking changes from being skipped +protect_breaking_commits = true + +# Filter out commits by matching patterns +filter_commits = false + +# Tag pattern for version detection +tag_pattern = "v[0-9].*" + +# Sort commits by newest first +sort_commits = "newest" + +[remote.github] +owner = "zarzet" +repo = "SpotiFLAC-Mobile" diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index fe9f21ad..1dd10dd7 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -9,6 +9,7 @@ import 'package:spotiflac_android/services/library_database.dart'; import 'package:spotiflac_android/services/notification_service.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/logger.dart'; +import 'package:spotiflac_android/utils/path_match_keys.dart'; final _log = AppLogger('LocalLibrary'); @@ -193,74 +194,11 @@ class LocalLibraryNotifier extends Notifier { await _loadFromDatabase(); } - Set _buildPathMatchKeys(String? filePath) { - final raw = filePath?.trim() ?? ''; - if (raw.isEmpty) return const {}; - - final cleaned = raw.startsWith('EXISTS:') ? raw.substring(7) : raw; - final keys = {}; - - void addNormalized(String value) { - final trimmed = value.trim(); - if (trimmed.isEmpty) return; - keys.add(trimmed); - keys.add(trimmed.toLowerCase()); - if (trimmed.contains('\\')) { - final slash = trimmed.replaceAll('\\', '/'); - keys.add(slash); - keys.add(slash.toLowerCase()); - } - if (trimmed.contains('%')) { - try { - final decoded = Uri.decodeFull(trimmed); - keys.add(decoded); - keys.add(decoded.toLowerCase()); - } catch (_) {} - } - - Uri? parsed; - try { - parsed = Uri.parse(trimmed); - } catch (_) {} - - if (parsed != null && parsed.hasScheme) { - final noQueryOrFragment = parsed.replace(query: null, fragment: null); - keys.add(noQueryOrFragment.toString()); - keys.add(noQueryOrFragment.toString().toLowerCase()); - - if (parsed.scheme == 'file') { - try { - final fileOnly = parsed.toFilePath(); - if (fileOnly.isNotEmpty) { - keys.add(fileOnly); - keys.add(fileOnly.toLowerCase()); - if (fileOnly.contains('\\')) { - final slash = fileOnly.replaceAll('\\', '/'); - keys.add(slash); - keys.add(slash.toLowerCase()); - } - } - } catch (_) {} - } - } else if (trimmed.startsWith('/')) { - try { - final asFileUri = Uri.file(trimmed).toString(); - keys.add(asFileUri); - keys.add(asFileUri.toLowerCase()); - } catch (_) {} - } - } - - addNormalized(cleaned); - - return keys; - } - bool _isDownloadedPath(String? filePath, Set downloadedPathKeys) { if (filePath == null || filePath.isEmpty || downloadedPathKeys.isEmpty) { return false; } - final candidateKeys = _buildPathMatchKeys(filePath); + final candidateKeys = buildPathMatchKeys(filePath); for (final key in candidateKeys) { if (downloadedPathKeys.contains(key)) { return true; @@ -322,8 +260,9 @@ class LocalLibraryNotifier extends Notifier { String? resolvedPath; bool didStartSecurityAccess = false; if (Platform.isIOS && iosBookmark != null && iosBookmark.isNotEmpty) { - resolvedPath = - await PlatformBridge.startAccessingIosBookmark(iosBookmark); + resolvedPath = await PlatformBridge.startAccessingIosBookmark( + iosBookmark, + ); if (resolvedPath != null) { didStartSecurityAccess = true; _log.i('Started iOS security-scoped access: $resolvedPath'); @@ -354,7 +293,7 @@ class LocalLibraryNotifier extends Notifier { }; final downloadedPathKeys = {}; for (final path in allHistoryPaths) { - downloadedPathKeys.addAll(_buildPathMatchKeys(path)); + downloadedPathKeys.addAll(buildPathMatchKeys(path)); } _log.i( 'Excluding ${allHistoryPaths.length} downloaded files from library scan ' @@ -834,8 +773,9 @@ class LocalLibraryNotifier extends Notifier { Future cleanupMissingFiles({String? iosBookmark}) async { bool didStartSecurityAccess = false; if (Platform.isIOS && iosBookmark != null && iosBookmark.isNotEmpty) { - final resolved = - await PlatformBridge.startAccessingIosBookmark(iosBookmark); + final resolved = await PlatformBridge.startAccessingIosBookmark( + iosBookmark, + ); if (resolved != null) { didStartSecurityAccess = true; } diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 1aa3bad3..e05429c9 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -29,6 +29,7 @@ import 'package:spotiflac_android/screens/downloaded_album_screen.dart'; import 'package:spotiflac_android/screens/library_tracks_folder_screen.dart'; import 'package:spotiflac_android/screens/local_album_screen.dart'; import 'package:spotiflac_android/utils/clickable_metadata.dart'; +import 'package:spotiflac_android/utils/path_match_keys.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; enum LibraryItemSource { downloaded, local } @@ -2112,10 +2113,22 @@ class _QueueTabState extends ConsumerState { if (count > 1) albumCount++; } + final downloadedPathKeys = {}; + for (final item in items) { + downloadedPathKeys.addAll(buildPathMatchKeys(item.filePath)); + } + + final dedupedLocalItems = localItems + .where((item) { + final localPathKeys = buildPathMatchKeys(item.filePath); + return !localPathKeys.any(downloadedPathKeys.contains); + }) + .toList(growable: false); + // Calculate local library stats final localAlbumCounts = {}; final localAlbumMap = >{}; - for (final item in localItems) { + for (final item in dedupedLocalItems) { final key = '${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}'; localAlbumCounts[key] = (localAlbumCounts[key] ?? 0) + 1; @@ -2841,8 +2854,24 @@ class _QueueTabState extends ConsumerState { .map((item) => UnifiedLibraryItem.fromLocalLibrary(item)) .toList(growable: false); - final merged = [...unifiedDownloaded, ...unifiedLocal] - ..sort((a, b) => b.addedAt.compareTo(a.addedAt)); + final downloadedPathKeys = {}; + for (final item in unifiedDownloaded) { + downloadedPathKeys.addAll(buildPathMatchKeys(item.filePath)); + } + + final dedupedUnifiedLocal = []; + for (final item in unifiedLocal) { + final localPathKeys = buildPathMatchKeys(item.filePath); + final overlapsDownloaded = localPathKeys.any(downloadedPathKeys.contains); + if (!overlapsDownloaded) { + dedupedUnifiedLocal.add(item); + } + } + + final merged = [ + ...unifiedDownloaded, + ...dedupedUnifiedLocal, + ]..sort((a, b) => b.addedAt.compareTo(a.addedAt)); _unifiedItemsCache[filterMode] = _UnifiedCacheEntry( historyItems: historyItems, diff --git a/lib/utils/path_match_keys.dart b/lib/utils/path_match_keys.dart new file mode 100644 index 00000000..ace140dd --- /dev/null +++ b/lib/utils/path_match_keys.dart @@ -0,0 +1,104 @@ +import 'dart:io'; + +const _androidStoragePathAliases = [ + '/storage/emulated/0', + '/storage/emulated/legacy', + '/storage/self/primary', + '/sdcard', + '/mnt/sdcard', +]; + +Set buildPathMatchKeys(String? filePath) { + final raw = filePath?.trim() ?? ''; + if (raw.isEmpty) return const {}; + + final cleaned = raw.startsWith('EXISTS:') ? raw.substring(7).trim() : raw; + if (cleaned.isEmpty) return const {}; + + final keys = {}; + final visited = {}; + + void addNormalized(String value) { + final trimmed = value.trim(); + if (trimmed.isEmpty) return; + if (!visited.add(trimmed)) return; + + keys.add(trimmed); + keys.add(trimmed.toLowerCase()); + + if (trimmed.contains('\\')) { + final slash = trimmed.replaceAll('\\', '/'); + if (slash != trimmed) { + addNormalized(slash); + } + } + + if (trimmed.contains('%')) { + try { + final decoded = Uri.decodeFull(trimmed); + if (decoded != trimmed) { + addNormalized(decoded); + } + } catch (_) {} + } + + Uri? parsed; + try { + parsed = Uri.parse(trimmed); + } catch (_) {} + + if (parsed != null && parsed.hasScheme) { + final withoutQueryOrFragment = parsed.replace( + query: null, + fragment: null, + ); + final uriString = withoutQueryOrFragment.toString(); + keys.add(uriString); + keys.add(uriString.toLowerCase()); + + if (parsed.scheme == 'file') { + try { + addNormalized(parsed.toFilePath()); + } catch (_) {} + } + } else if (trimmed.startsWith('/')) { + try { + final asFileUri = Uri.file(trimmed).toString(); + keys.add(asFileUri); + keys.add(asFileUri.toLowerCase()); + } catch (_) {} + } + + if (Platform.isAndroid) { + for (final alias in _androidEquivalentPaths(trimmed)) { + if (alias != trimmed) { + addNormalized(alias); + } + } + } + } + + addNormalized(cleaned); + return keys; +} + +Iterable _androidEquivalentPaths(String path) { + final normalized = path.replaceAll('\\', '/'); + final lower = normalized.toLowerCase(); + String? suffix; + + for (final prefix in _androidStoragePathAliases) { + if (lower == prefix) { + suffix = ''; + break; + } + final withSlash = '$prefix/'; + if (lower.startsWith(withSlash)) { + suffix = normalized.substring(prefix.length); + break; + } + } + + if (suffix == null) return const []; + return _androidStoragePathAliases.map((prefix) => '$prefix$suffix'); +} From db55bb4693f88e7f96c4cb7564a155948bcbcba7 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 10 Mar 2026 23:12:44 +0700 Subject: [PATCH 025/145] fix(deezer): update quality label to FLAC Best Quality since API may deliver above CD quality --- lib/widgets/download_service_picker.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index 69eb4f1d..41c8c330 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -73,8 +73,8 @@ const _builtInServices = [ qualityOptions: [ QualityOption( id: 'FLAC', - label: 'FLAC Lossless', - description: '16-bit / 44.1kHz (CD Quality)', + label: 'FLAC Best Quality', + description: 'Up to 24-bit / 48kHz+', ), ], ), From 64408c8d8b32258d9a9ac51f172de2a90b8e7898 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 10 Mar 2026 23:25:45 +0700 Subject: [PATCH 026/145] feat: add Single and EP badge on artist page and fix EP bucket filtering (#203) --- lib/screens/artist_screen.dart | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index a1c37333..8ad6bf03 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -343,7 +343,7 @@ class _ArtistScreenState extends ConsumerState { .where((a) => a.albumType == 'album') .toList(growable: false); _singlesBucket = albums - .where((a) => a.albumType == 'single') + .where((a) => a.albumType == 'single' || a.albumType == 'ep') .toList(growable: false); _compilationsBucket = albums .where((a) => a.albumType == 'compilation') @@ -414,6 +414,7 @@ class _ArtistScreenState extends ConsumerState { context.l10n.artistSingles, singles, colorScheme, + showTypeBadge: true, ), ), if (compilations.isNotEmpty) @@ -1523,8 +1524,9 @@ class _ArtistScreenState extends ConsumerState { Widget _buildAlbumSection( String title, List albums, - ColorScheme colorScheme, - ) { + ColorScheme colorScheme, { + bool showTypeBadge = false, + }) { final sectionHeight = _artistAlbumSectionHeight(); final tileSize = _artistAlbumTileSize(); @@ -1555,6 +1557,7 @@ class _ArtistScreenState extends ConsumerState { colorScheme, tileSize: tileSize, sectionHeight: sectionHeight, + showTypeBadge: showTypeBadge, ), ); }, @@ -1569,6 +1572,7 @@ class _ArtistScreenState extends ConsumerState { ColorScheme colorScheme, { required double tileSize, required double sectionHeight, + bool showTypeBadge = false, }) { final isSelected = _selectedAlbumIds.contains(album.id); @@ -1681,6 +1685,29 @@ class _ArtistScreenState extends ConsumerState { : null, ), ), + if (showTypeBadge) + Positioned( + left: 6, + bottom: 6, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.7), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + album.albumType == 'ep' ? 'EP' : 'Single', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + ), ], ), const SizedBox(height: 8), From 76fe8dbc69cc5618b346373953dd1d56a5092da8 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Mar 2026 00:31:20 +0700 Subject: [PATCH 027/145] feat: add CUE sheet support for local library scanning and splitting (#201) Parse .cue files in library scanner (Go + SAF) to display individual tracks instead of one large audio file. Add FFmpeg-based CUE splitting to extract tracks into separate FLAC files with embedded metadata and cover art. - Go: CUE parser, two-pass scan (CUE first, skip referenced audio), virtual paths (cue#trackNN) for DB UNIQUE constraint, audioDir override for SAF temp-file scenarios - Android: SAF scanner recognizes .cue in both full and incremental scan, copies .cue+audio to temp for Go parsing, unchanged-CUE audio sibling dedup, parseCueSheet handler resolves SAF audio siblings - Dart: FFmpegService.splitCueToTracks, CUE split UI in track metadata screen, persistent output dir for SAF splits with write-back - CUE virtual path normalization across fileExists/fileStat/deleteFile/ openFile; play/share/open blocked for virtual tracks with guidance to split first; delete only removes DB entry, not shared .cue file - iOS: parseCueSheet handler - Localization: 12 new CUE-related strings Requested by @Seerafimm Closes #201 --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 527 +++++++++++++++- go_backend/cue_parser.go | 577 +++++++++++++++++ go_backend/exports.go | 26 + go_backend/library_scan.go | 120 +++- ios/Runner/AppDelegate.swift | 9 + lib/l10n/app_localizations.dart | 72 +++ lib/l10n/app_localizations_de.dart | 48 ++ lib/l10n/app_localizations_en.dart | 48 ++ lib/l10n/app_localizations_es.dart | 48 ++ lib/l10n/app_localizations_fr.dart | 48 ++ lib/l10n/app_localizations_hi.dart | 48 ++ lib/l10n/app_localizations_id.dart | 48 ++ lib/l10n/app_localizations_ja.dart | 48 ++ lib/l10n/app_localizations_ko.dart | 48 ++ lib/l10n/app_localizations_nl.dart | 48 ++ lib/l10n/app_localizations_pt.dart | 48 ++ lib/l10n/app_localizations_ru.dart | 48 ++ lib/l10n/app_localizations_tr.dart | 48 ++ lib/l10n/app_localizations_zh.dart | 48 ++ lib/l10n/arb/app_en.arb | 84 +++ lib/providers/playback_provider.dart | 12 + lib/screens/local_album_screen.dart | 20 +- lib/screens/track_metadata_screen.dart | 593 +++++++++++++++++- lib/services/ffmpeg_service.dart | 154 +++++ lib/services/platform_bridge.dart | 16 + lib/utils/file_access.dart | 50 +- 26 files changed, 2844 insertions(+), 40 deletions(-) create mode 100644 go_backend/cue_parser.go diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index da56220d..8fe38bfc 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -807,6 +807,72 @@ class MainActivity: FlutterFragmentActivity() { } } + /** + * Get the parent DocumentFile directory for a SAF document URI. + * The child URI must be a tree-based document URI (e.g. from SAF tree scan). + * Returns a DocumentFile that supports findFile() for sibling lookup. + */ + private fun safParentDir(childUri: Uri): DocumentFile? { + try { + val docId = android.provider.DocumentsContract.getDocumentId(childUri) + if (docId.isNullOrEmpty()) return null + + // Document IDs typically look like "primary:Music/Album/file.cue" + // Parent would be "primary:Music/Album" + val lastSlash = docId.lastIndexOf('/') + if (lastSlash <= 0) return null + + val parentDocId = docId.substring(0, lastSlash) + + // Build a tree document URI for the parent so it supports listing/findFile + val treeDocId = android.provider.DocumentsContract.getTreeDocumentId(childUri) + if (treeDocId.isNullOrEmpty()) return null + + val parentUri = android.provider.DocumentsContract.buildDocumentUriUsingTree( + childUri, parentDocId + ) + return DocumentFile.fromTreeUri(this, parentUri) + ?: DocumentFile.fromSingleUri(this, parentUri) + } catch (e: Exception) { + android.util.Log.w("SpotiFLAC", "Failed to get SAF parent dir: ${e.message}") + return null + } + } + + /** + * Extract the audio filename referenced by a CUE sheet file. + * Reads the FILE "name" TYPE line from the .cue text. + * Returns just the filename (no path), or null if not found. + */ + private fun extractCueAudioFileName(cueTempPath: String): String? { + try { + val lines = File(cueTempPath).readLines() + for (line in lines) { + val trimmed = line.trim().let { l -> + // Strip BOM + if (l.startsWith("\uFEFF")) l.removePrefix("\uFEFF").trim() else l + } + if (trimmed.uppercase(Locale.ROOT).startsWith("FILE ")) { + val rest = trimmed.substring(5).trim() + // Parse: "filename" TYPE or filename TYPE + val filename = if (rest.startsWith("\"")) { + val endQuote = rest.indexOf('"', 1) + if (endQuote > 0) rest.substring(1, endQuote) else rest + } else { + // Last word is the type, everything else is the filename + val parts = rest.split("\\s+".toRegex()) + if (parts.size >= 2) parts.dropLast(1).joinToString(" ") else rest + } + // Return just the filename (strip any path separators) + return filename.substringAfterLast("/").substringAfterLast("\\") + } + } + } catch (e: Exception) { + android.util.Log.w("SpotiFLAC", "Failed to extract audio filename from CUE: ${e.message}") + } + return null + } + private fun scanSafTree(treeUriStr: String): String { if (treeUriStr.isBlank()) return "[]" @@ -820,8 +886,10 @@ class MainActivity: FlutterFragmentActivity() { it.currentFile = "Scanning folders..." } - val supportedExt = setOf(".flac", ".m4a", ".mp3", ".opus", ".ogg") + val supportedAudioExt = setOf(".flac", ".m4a", ".mp3", ".opus", ".ogg") val audioFiles = mutableListOf>() + // CUE files: (cueDoc, parentDir) — we need the parent to find sibling audio + val cueFiles = mutableListOf>() val visitedDirUris = mutableSetOf() var traversalErrors = 0 @@ -870,7 +938,9 @@ class MainActivity: FlutterFragmentActivity() { } else if (child.isFile) { val name = child.name ?: continue val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT) - if (ext.isNotBlank() && supportedExt.contains(".$ext")) { + if (ext == "cue") { + cueFiles.add(child to dir) + } else if (ext.isNotBlank() && supportedAudioExt.contains(".$ext")) { audioFiles.add(child to path) } } @@ -885,11 +955,12 @@ class MainActivity: FlutterFragmentActivity() { } } + val totalItems = audioFiles.size + cueFiles.size updateSafScanProgress { - it.totalFiles = audioFiles.size + it.totalFiles = totalItems } - if (audioFiles.isEmpty()) { + if (audioFiles.isEmpty() && cueFiles.isEmpty()) { updateSafScanProgress { it.isComplete = true it.progressPct = 100.0 @@ -901,12 +972,138 @@ class MainActivity: FlutterFragmentActivity() { var scanned = 0 var errors = traversalErrors + // --- CUE first pass: parse CUE sheets, expand to tracks, track referenced audio --- + val cueReferencedAudioUris = mutableSetOf() + + for ((cueDoc, parentDir) in cueFiles) { + if (safScanCancel) { + updateSafScanProgress { it.isComplete = true } + return "[]" + } + + val cueName = try { cueDoc.name ?: "" } catch (_: Exception) { "" } + updateSafScanProgress { it.currentFile = cueName } + + var tempCuePath: String? = null + var tempAudioPath: String? = null + try { + // Copy CUE to temp + tempCuePath = copyUriToTemp(cueDoc.uri, ".cue") + if (tempCuePath == null) { + errors++ + android.util.Log.w("SpotiFLAC", "SAF scan: failed to copy CUE ${cueDoc.uri}") + scanned++ + continue + } + + // Extract the audio filename from the CUE sheet text + val audioFileName = extractCueAudioFileName(tempCuePath) + + // Find the referenced audio file as a sibling in the same SAF directory + var audioDoc: DocumentFile? = null + if (!audioFileName.isNullOrBlank()) { + audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null } + } + + // Fallback: try common audio extensions with the CUE base name + if (audioDoc == null) { + val cueBaseName = cueName.substringBeforeLast('.') + val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a") + for (ext in commonExts) { + audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null } + if (audioDoc != null) break + // Try uppercase + audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null } + if (audioDoc != null) break + } + } + + if (audioDoc == null) { + android.util.Log.w("SpotiFLAC", "SAF scan: no audio file found for CUE $cueName") + errors++ + scanned++ + continue + } + + // Mark this audio file so we skip it in the regular audio pass + cueReferencedAudioUris.add(audioDoc.uri.toString()) + + // Copy audio to same temp dir so Go can resolve it + val tempDir = File(tempCuePath).parent ?: cacheDir.absolutePath + val audioName = try { audioDoc.name ?: "audio.flac" } catch (_: Exception) { "audio.flac" } + val audioExt = audioName.substringAfterLast('.', "").lowercase(Locale.ROOT) + val fallbackAudioExt = if (audioExt.isNotBlank()) ".$audioExt" else null + + tempAudioPath = copyUriToTemp(audioDoc.uri, fallbackAudioExt) + if (tempAudioPath == null) { + android.util.Log.w("SpotiFLAC", "SAF scan: failed to copy audio for CUE $cueName") + errors++ + scanned++ + continue + } + + // Rename temp audio to its original name so Go can find it by name + val renamedAudio = File(tempDir, audioName) + val tempAudioFile = File(tempAudioPath) + if (renamedAudio.absolutePath != tempAudioFile.absolutePath) { + tempAudioFile.renameTo(renamedAudio) + tempAudioPath = renamedAudio.absolutePath + } + + val cueLastModified = try { cueDoc.lastModified() } catch (_: Exception) { 0L } + + // Call Go to produce library scan entries for each CUE track + val cueResultsJson = Gobackend.scanCueSheetForLibrary( + tempCuePath, + tempDir, + cueDoc.uri.toString(), + cueLastModified + ) + + val cueArray = JSONArray(cueResultsJson) + for (j in 0 until cueArray.length()) { + results.put(cueArray.getJSONObject(j)) + } + + android.util.Log.d( + "SpotiFLAC", + "SAF scan: CUE $cueName -> ${cueArray.length()} tracks" + ) + } catch (e: Exception) { + errors++ + android.util.Log.w("SpotiFLAC", "SAF scan: error processing CUE $cueName: ${e.message}") + } finally { + try { tempCuePath?.let { File(it).delete() } } catch (_: Exception) {} + try { tempAudioPath?.let { File(it).delete() } } catch (_: Exception) {} + } + + scanned++ + val pct = scanned.toDouble() / totalItems.toDouble() * 100.0 + updateSafScanProgress { + it.scannedFiles = scanned + it.errorCount = errors + it.progressPct = pct + } + } + + // --- Regular audio file pass: skip files referenced by CUE sheets --- for ((doc, _) in audioFiles) { if (safScanCancel) { updateSafScanProgress { it.isComplete = true } return "[]" } + // Skip audio files that are represented by CUE track entries + if (cueReferencedAudioUris.contains(doc.uri.toString())) { + scanned++ + val pct = scanned.toDouble() / totalItems.toDouble() * 100.0 + updateSafScanProgress { + it.scannedFiles = scanned + it.progressPct = pct + } + continue + } + val name = try { doc.name ?: "" } catch (_: Exception) { "" } updateSafScanProgress { it.currentFile = name @@ -947,7 +1144,7 @@ class MainActivity: FlutterFragmentActivity() { } scanned++ - val pct = scanned.toDouble() / audioFiles.size.toDouble() * 100.0 + val pct = scanned.toDouble() / totalItems.toDouble() * 100.0 updateSafScanProgress { it.scannedFiles = scanned it.errorCount = errors @@ -965,6 +1162,8 @@ class MainActivity: FlutterFragmentActivity() { /** * Incremental SAF tree scan - only scans new or modified files. + * Supports .cue sheets: expands them into virtual track entries and + * deduplicates audio files referenced by CUE sheets. * @param treeUriStr The SAF tree URI to scan * @param existingFilesJson JSON object mapping file URI -> lastModified timestamp * @return JSON object with new/changed files and removed URIs @@ -1007,13 +1206,29 @@ class MainActivity: FlutterFragmentActivity() { it.currentFile = "Scanning folders..." } - val supportedExt = setOf(".flac", ".m4a", ".mp3", ".opus", ".ogg") + val supportedAudioExt = setOf(".flac", ".m4a", ".mp3", ".opus", ".ogg") val audioFiles = mutableListOf>() // doc, path, lastModified + // CUE files to scan: (cueDoc, parentDir, lastModified) + val cueFilesToScan = mutableListOf>() + // Unchanged CUE files: (cueDoc, parentDir) — need to discover audio siblings for skip set + val unchangedCueFiles = mutableListOf>() val currentUris = mutableSetOf() val visitedDirUris = mutableSetOf() var traversalErrors = 0 - // Collect all audio files with lastModified + // Build a map of CUE base URIs -> existing virtual track URIs from the database. + // Virtual paths look like "content://...album.cue#track01". + // We need this to preserve virtual paths for unchanged CUE files. + val existingCueVirtualPaths = mutableMapOf>() // cueUri -> [virtualPaths] + for (key in existingFiles.keys) { + val hashIdx = key.indexOf("#track") + if (hashIdx > 0) { + val baseCueUri = key.substring(0, hashIdx) + existingCueVirtualPaths.getOrPut(baseCueUri) { mutableListOf() }.add(key) + } + } + + // Collect all files with lastModified val queue: ArrayDeque> = ArrayDeque() queue.add(root to "") @@ -1076,7 +1291,27 @@ class MainActivity: FlutterFragmentActivity() { val name = child.name ?: continue val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT) - if (ext.isNotBlank() && supportedExt.contains(".$ext")) { + + if (ext == "cue") { + val lastModified = try { + child.lastModified() + } catch (_: Exception) { 0L } + + // Check if any virtual track from this CUE exists with matching modTime + val virtualPaths = existingCueVirtualPaths[uriStr] + val existingModified = virtualPaths?.firstOrNull()?.let { existingFiles[it] } + + if (existingModified != null && existingModified == lastModified) { + // CUE is unchanged — mark virtual paths as current so they aren't removed + unchangedCueFiles.add(child to dir) + for (vp in virtualPaths) { + currentUris.add(vp) + } + } else { + // CUE is new or modified — needs scanning + cueFilesToScan.add(Triple(child, dir, lastModified)) + } + } else if (ext.isNotBlank() && supportedAudioExt.contains(".$ext")) { val existingModified = existingFiles[uriStr] val lastModified = try { child.lastModified() @@ -1104,13 +1339,14 @@ class MainActivity: FlutterFragmentActivity() { // Find removed files (in existing but not in current) val removedUris = existingFiles.keys.filter { !currentUris.contains(it) } val totalFiles = currentUris.size - val skippedCount = (totalFiles - audioFiles.size).coerceAtLeast(0) + val filesToProcess = audioFiles.size + cueFilesToScan.size + val skippedCount = (totalFiles - filesToProcess).coerceAtLeast(0) updateSafScanProgress { it.totalFiles = totalFiles } - if (audioFiles.isEmpty()) { + if (audioFiles.isEmpty() && cueFilesToScan.isEmpty()) { updateSafScanProgress { it.isComplete = true it.scannedFiles = totalFiles @@ -1128,6 +1364,173 @@ class MainActivity: FlutterFragmentActivity() { var scanned = 0 var errors = traversalErrors + // --- CUE first pass: parse new/modified CUE sheets --- + val cueReferencedAudioUris = mutableSetOf() + + for ((cueDoc, parentDir, cueLastModified) in cueFilesToScan) { + if (safScanCancel) { + updateSafScanProgress { it.isComplete = true } + val result = JSONObject() + result.put("files", JSONArray()) + result.put("removedUris", JSONArray()) + result.put("skippedCount", skippedCount) + result.put("totalFiles", totalFiles) + result.put("cancelled", true) + return result.toString() + } + + val cueName = try { cueDoc.name ?: "" } catch (_: Exception) { "" } + updateSafScanProgress { it.currentFile = cueName } + + var tempCuePath: String? = null + var tempAudioPath: String? = null + try { + // Copy CUE to temp + tempCuePath = copyUriToTemp(cueDoc.uri, ".cue") + if (tempCuePath == null) { + errors++ + android.util.Log.w("SpotiFLAC", "SAF incremental scan: failed to copy CUE ${cueDoc.uri}") + scanned++ + continue + } + + // Extract the audio filename from the CUE sheet text + val audioFileName = extractCueAudioFileName(tempCuePath) + + // Find the referenced audio file as a sibling in the same SAF directory + var audioDoc: DocumentFile? = null + if (!audioFileName.isNullOrBlank()) { + audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null } + } + + // Fallback: try common audio extensions with the CUE base name + if (audioDoc == null) { + val cueBaseName = cueName.substringBeforeLast('.') + val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a") + for (ext in commonExts) { + audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null } + if (audioDoc != null) break + audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null } + if (audioDoc != null) break + } + } + + if (audioDoc == null) { + android.util.Log.w("SpotiFLAC", "SAF incremental scan: no audio file found for CUE $cueName") + errors++ + scanned++ + continue + } + + // Mark this audio file so we skip it in the regular audio pass + cueReferencedAudioUris.add(audioDoc.uri.toString()) + + // Copy audio to same temp dir so Go can resolve it + val tempDir = File(tempCuePath).parent ?: cacheDir.absolutePath + val audioName = try { audioDoc.name ?: "audio.flac" } catch (_: Exception) { "audio.flac" } + val audioExt = audioName.substringAfterLast('.', "").lowercase(Locale.ROOT) + val fallbackAudioExt = if (audioExt.isNotBlank()) ".$audioExt" else null + + tempAudioPath = copyUriToTemp(audioDoc.uri, fallbackAudioExt) + if (tempAudioPath == null) { + android.util.Log.w("SpotiFLAC", "SAF incremental scan: failed to copy audio for CUE $cueName") + errors++ + scanned++ + continue + } + + // Rename temp audio to its original name so Go can find it by name + val renamedAudio = File(tempDir, audioName) + val tempAudioFile = File(tempAudioPath) + if (renamedAudio.absolutePath != tempAudioFile.absolutePath) { + tempAudioFile.renameTo(renamedAudio) + tempAudioPath = renamedAudio.absolutePath + } + + // Call Go to produce library scan entries for each CUE track + val cueResultsJson = Gobackend.scanCueSheetForLibrary( + tempCuePath, + tempDir, + cueDoc.uri.toString(), + cueLastModified + ) + + val cueArray = JSONArray(cueResultsJson) + for (j in 0 until cueArray.length()) { + val trackObj = cueArray.getJSONObject(j) + results.put(trackObj) + // Register each virtual path as current so deletion detection works + val virtualPath = trackObj.optString("filePath", "") + if (virtualPath.isNotBlank()) { + currentUris.add(virtualPath) + } + } + + android.util.Log.d( + "SpotiFLAC", + "SAF incremental scan: CUE $cueName -> ${cueArray.length()} tracks" + ) + } catch (e: Exception) { + errors++ + android.util.Log.w("SpotiFLAC", "SAF incremental scan: error processing CUE $cueName: ${e.message}") + } finally { + try { tempCuePath?.let { File(it).delete() } } catch (_: Exception) {} + try { tempAudioPath?.let { File(it).delete() } } catch (_: Exception) {} + } + + scanned++ + val processed = skippedCount + scanned + val pct = if (totalFiles > 0) { + processed.toDouble() / totalFiles.toDouble() * 100.0 + } else { + 100.0 + } + updateSafScanProgress { + it.scannedFiles = processed + it.errorCount = errors + it.progressPct = pct + } + } + + // Discover audio siblings for unchanged CUE files so we skip them + // in the regular audio pass. Copy the .cue to temp (tiny file) to extract + // the audio filename, then find the sibling by name. + for ((cueDoc, parentDir) in unchangedCueFiles) { + var tempCue: String? = null + try { + tempCue = copyUriToTemp(cueDoc.uri, ".cue") + if (tempCue != null) { + val audioFileName = extractCueAudioFileName(tempCue) + var audioDoc: DocumentFile? = null + if (!audioFileName.isNullOrBlank()) { + audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null } + } + // Fallback: try common extensions with CUE base name + if (audioDoc == null) { + val cueName = try { cueDoc.name ?: "" } catch (_: Exception) { "" } + val cueBaseName = cueName.substringBeforeLast('.') + if (cueBaseName.isNotBlank()) { + val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a") + for (ext in commonExts) { + audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null } + if (audioDoc != null) break + audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null } + if (audioDoc != null) break + } + } + } + if (audioDoc != null) { + cueReferencedAudioUris.add(audioDoc.uri.toString()) + } + } + } catch (e: Exception) { + android.util.Log.w("SpotiFLAC", "SAF incremental scan: failed to resolve audio for unchanged CUE: ${e.message}") + } finally { + try { tempCue?.let { File(it).delete() } } catch (_: Exception) {} + } + } + + // --- Regular audio file pass: skip files referenced by CUE sheets --- for ((doc, _, lastModified) in audioFiles) { if (safScanCancel) { updateSafScanProgress { it.isComplete = true } @@ -1140,6 +1543,22 @@ class MainActivity: FlutterFragmentActivity() { return result.toString() } + // Skip audio files that are represented by CUE track entries + if (cueReferencedAudioUris.contains(doc.uri.toString())) { + scanned++ + val processed = skippedCount + scanned + val pct = if (totalFiles > 0) { + processed.toDouble() / totalFiles.toDouble() * 100.0 + } else { + 100.0 + } + updateSafScanProgress { + it.scannedFiles = processed + it.progressPct = pct + } + continue + } + val name = try { doc.name ?: "" } catch (_: Exception) { "" } updateSafScanProgress { it.currentFile = name @@ -1194,6 +1613,9 @@ class MainActivity: FlutterFragmentActivity() { } } + // Recalculate removedUris now that CUE virtual paths have been registered + val finalRemovedUris = existingFiles.keys.filter { !currentUris.contains(it) } + updateSafScanProgress { it.isComplete = true it.progressPct = 100.0 @@ -1201,7 +1623,7 @@ class MainActivity: FlutterFragmentActivity() { val result = JSONObject() result.put("files", results) - result.put("removedUris", JSONArray(removedUris)) + result.put("removedUris", JSONArray(finalRemovedUris)) result.put("skippedCount", skippedCount) result.put("totalFiles", totalFiles) return result.toString() @@ -2756,6 +3178,89 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } + // CUE Sheet Parsing + "parseCueSheet" -> { + val cuePath = call.argument("cue_path") ?: "" + val audioDir = call.argument("audio_dir") ?: "" + val response = withContext(Dispatchers.IO) { + try { + if (cuePath.startsWith("content://")) { + val uri = Uri.parse(cuePath) + val tempCuePath = copyUriToTemp(uri, ".cue") + ?: return@withContext """{"error":"Failed to copy CUE file to temp"}""" + var tempAudioPath: String? = null + try { + // Extract audio filename from CUE text + val audioFileName = extractCueAudioFileName(tempCuePath) + + // Try to find the audio sibling in SAF + var audioDoc: DocumentFile? = null + val parentDir = safParentDir(uri) + if (parentDir != null && !audioFileName.isNullOrBlank()) { + audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null } + } + + // Fallback: try common extensions with the CUE base name + if (audioDoc == null && parentDir != null) { + val cueName = try { + DocumentFile.fromSingleUri(this@MainActivity, uri)?.name ?: "" + } catch (_: Exception) { "" } + val cueBaseName = cueName.substringBeforeLast('.') + if (cueBaseName.isNotBlank()) { + val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a") + for (ext in commonExts) { + audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null } + if (audioDoc != null) break + audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null } + if (audioDoc != null) break + } + } + } + + val tempDir = File(tempCuePath).parent ?: cacheDir.absolutePath + if (audioDoc != null) { + // Copy audio to same temp dir with original name + val audioName = try { audioDoc.name ?: "audio.flac" } catch (_: Exception) { "audio.flac" } + val audioExt = audioName.substringAfterLast('.', "").lowercase(Locale.ROOT) + val fallbackExt = if (audioExt.isNotBlank()) ".$audioExt" else null + val copiedAudio = copyUriToTemp(audioDoc.uri, fallbackExt) + if (copiedAudio != null) { + val renamedAudio = File(tempDir, audioName) + val copiedFile = File(copiedAudio) + if (renamedAudio.absolutePath != copiedFile.absolutePath) { + copiedFile.renameTo(renamedAudio) + } + tempAudioPath = renamedAudio.absolutePath + } + } + + // Parse with audio in temp dir; Go will resolve there + val resultJson = Gobackend.parseCueSheet(tempCuePath, tempDir) + + // Replace the temp audio_path with the SAF content:// URI + // so Dart knows it's a SAF file and handles it accordingly + if (audioDoc != null) { + val resultObj = JSONObject(resultJson) + resultObj.put("audio_path", audioDoc.uri.toString()) + // Also pass the original CUE URI for reference + resultObj.put("cue_path", cuePath) + resultObj.toString() + } else { + resultJson + } + } finally { + try { File(tempCuePath).delete() } catch (_: Exception) {} + try { tempAudioPath?.let { File(it).delete() } } catch (_: Exception) {} + } + } else { + Gobackend.parseCueSheet(cuePath, audioDir) + } + } catch (e: Exception) { + """{"error":"${e.message?.replace("\"", "'")}"}""" + } + } + result.success(response) + } else -> result.notImplemented() } } catch (e: Exception) { diff --git a/go_backend/cue_parser.go b/go_backend/cue_parser.go new file mode 100644 index 00000000..a938be2a --- /dev/null +++ b/go_backend/cue_parser.go @@ -0,0 +1,577 @@ +package gobackend + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +// CueSheet represents a parsed .cue file +type CueSheet struct { + // Album-level metadata + Performer string `json:"performer"` + Title string `json:"title"` + FileName string `json:"file_name"` + FileType string `json:"file_type"` // WAVE, FLAC, MP3, AIFF, etc. + Genre string `json:"genre,omitempty"` + Date string `json:"date,omitempty"` + Comment string `json:"comment,omitempty"` + Composer string `json:"composer,omitempty"` + Tracks []CueTrack `json:"tracks"` +} + +// CueTrack represents a single track in a cue sheet +type CueTrack struct { + Number int `json:"number"` + Title string `json:"title"` + Performer string `json:"performer"` + ISRC string `json:"isrc,omitempty"` + Composer string `json:"composer,omitempty"` + // Index positions in seconds (fractional) + StartTime float64 `json:"start_time"` // INDEX 01 in seconds + PreGap float64 `json:"pre_gap"` // INDEX 00 in seconds (or -1 if not present) +} + +// CueSplitInfo represents the information needed to split a CUE+audio file +type CueSplitInfo struct { + CuePath string `json:"cue_path"` + AudioPath string `json:"audio_path"` + Album string `json:"album"` + Artist string `json:"artist"` + Genre string `json:"genre,omitempty"` + Date string `json:"date,omitempty"` + Tracks []CueSplitTrack `json:"tracks"` +} + +// CueSplitTrack has the FFmpeg split parameters for a single track +type CueSplitTrack struct { + Number int `json:"number"` + Title string `json:"title"` + Artist string `json:"artist"` + ISRC string `json:"isrc,omitempty"` + Composer string `json:"composer,omitempty"` + StartSec float64 `json:"start_sec"` + EndSec float64 `json:"end_sec"` // -1 means until end of file +} + +var ( + reRemCommand = regexp.MustCompile(`^REM\s+(\S+)\s+(.+)$`) + reQuoted = regexp.MustCompile(`"([^"]*)"`) +) + +// ParseCueFile parses a .cue file and returns a CueSheet +func ParseCueFile(cuePath string) (*CueSheet, error) { + f, err := os.Open(cuePath) + if err != nil { + return nil, fmt.Errorf("failed to open cue file: %w", err) + } + defer f.Close() + + sheet := &CueSheet{} + var currentTrack *CueTrack + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + + // Handle BOM at start of file + if strings.HasPrefix(line, "\xef\xbb\xbf") { + line = strings.TrimPrefix(line, "\xef\xbb\xbf") + line = strings.TrimSpace(line) + } + + upper := strings.ToUpper(line) + + // REM commands (album-level metadata) + if strings.HasPrefix(upper, "REM ") { + matches := reRemCommand.FindStringSubmatch(line) + if len(matches) == 3 { + key := strings.ToUpper(matches[1]) + value := unquoteCue(matches[2]) + switch key { + case "GENRE": + sheet.Genre = value + case "DATE": + sheet.Date = value + case "COMMENT": + sheet.Comment = value + case "COMPOSER": + if currentTrack != nil { + currentTrack.Composer = value + } else { + sheet.Composer = value + } + } + } + continue + } + + // PERFORMER + if strings.HasPrefix(upper, "PERFORMER ") { + value := unquoteCue(line[len("PERFORMER "):]) + if currentTrack != nil { + currentTrack.Performer = value + } else { + sheet.Performer = value + } + continue + } + + // TITLE + if strings.HasPrefix(upper, "TITLE ") { + value := unquoteCue(line[len("TITLE "):]) + if currentTrack != nil { + currentTrack.Title = value + } else { + sheet.Title = value + } + continue + } + + // FILE + if strings.HasPrefix(upper, "FILE ") { + rest := line[len("FILE "):] + // Extract filename and type + // Format: FILE "filename.flac" WAVE + // or: FILE filename.flac WAVE + fname, ftype := parseCueFileLine(rest) + sheet.FileName = fname + sheet.FileType = ftype + continue + } + + // TRACK + if strings.HasPrefix(upper, "TRACK ") { + // Save previous track + if currentTrack != nil { + sheet.Tracks = append(sheet.Tracks, *currentTrack) + } + + parts := strings.Fields(line) + trackNum := 0 + if len(parts) >= 2 { + trackNum, _ = strconv.Atoi(parts[1]) + } + + currentTrack = &CueTrack{ + Number: trackNum, + PreGap: -1, + } + continue + } + + // INDEX + if strings.HasPrefix(upper, "INDEX ") && currentTrack != nil { + parts := strings.Fields(line) + if len(parts) >= 3 { + indexNum, _ := strconv.Atoi(parts[1]) + timeSec := parseCueTimestamp(parts[2]) + switch indexNum { + case 0: + currentTrack.PreGap = timeSec + case 1: + currentTrack.StartTime = timeSec + } + } + continue + } + + // ISRC + if strings.HasPrefix(upper, "ISRC ") && currentTrack != nil { + currentTrack.ISRC = strings.TrimSpace(line[len("ISRC "):]) + continue + } + + // SONGWRITER (used as composer sometimes) + if strings.HasPrefix(upper, "SONGWRITER ") { + value := unquoteCue(line[len("SONGWRITER "):]) + if currentTrack != nil { + currentTrack.Composer = value + } else { + sheet.Composer = value + } + continue + } + } + + // Don't forget the last track + if currentTrack != nil { + sheet.Tracks = append(sheet.Tracks, *currentTrack) + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading cue file: %w", err) + } + + if len(sheet.Tracks) == 0 { + return nil, fmt.Errorf("no tracks found in cue file") + } + + return sheet, nil +} + +// parseCueTimestamp converts MM:SS:FF (frames at 75fps) to seconds +func parseCueTimestamp(ts string) float64 { + parts := strings.Split(ts, ":") + if len(parts) != 3 { + return 0 + } + + minutes, _ := strconv.Atoi(parts[0]) + seconds, _ := strconv.Atoi(parts[1]) + frames, _ := strconv.Atoi(parts[2]) + + return float64(minutes)*60 + float64(seconds) + float64(frames)/75.0 +} + +// formatCueTimestamp converts seconds to HH:MM:SS.mmm format for FFmpeg +func formatCueTimestamp(seconds float64) string { + if seconds < 0 { + return "0" + } + hours := int(seconds) / 3600 + mins := (int(seconds) % 3600) / 60 + secs := seconds - float64(hours*3600) - float64(mins*60) + return fmt.Sprintf("%02d:%02d:%06.3f", hours, mins, secs) +} + +// unquoteCue removes surrounding quotes from a CUE value +func unquoteCue(s string) string { + s = strings.TrimSpace(s) + if matches := reQuoted.FindStringSubmatch(s); len(matches) == 2 { + return matches[1] + } + return s +} + +// parseCueFileLine parses the FILE command's filename and type +func parseCueFileLine(rest string) (string, string) { + rest = strings.TrimSpace(rest) + + var filename, ftype string + + if strings.HasPrefix(rest, "\"") { + // Quoted filename + endQuote := strings.Index(rest[1:], "\"") + if endQuote >= 0 { + filename = rest[1 : endQuote+1] + remaining := strings.TrimSpace(rest[endQuote+2:]) + ftype = remaining + } else { + filename = rest + } + } else { + // Unquoted filename - last word is the type + parts := strings.Fields(rest) + if len(parts) >= 2 { + ftype = parts[len(parts)-1] + filename = strings.Join(parts[:len(parts)-1], " ") + } else if len(parts) == 1 { + filename = parts[0] + } + } + + return filename, strings.TrimSpace(ftype) +} + +// ResolveCueAudioPath finds the actual audio file referenced by a .cue sheet. +// It checks relative to the cue file's directory. +func ResolveCueAudioPath(cuePath string, cueFileName string) string { + cueDir := filepath.Dir(cuePath) + + // 1. Try the exact filename from the .cue + candidate := filepath.Join(cueDir, cueFileName) + if _, err := os.Stat(candidate); err == nil { + return candidate + } + + // 2. Try common case variations + baseName := strings.TrimSuffix(cueFileName, filepath.Ext(cueFileName)) + commonExts := []string{".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a"} + for _, ext := range commonExts { + candidate = filepath.Join(cueDir, baseName+ext) + if _, err := os.Stat(candidate); err == nil { + return candidate + } + // Try uppercase ext + candidate = filepath.Join(cueDir, baseName+strings.ToUpper(ext)) + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + + // 3. Try to find any audio file with the same base name as the .cue file + cueBase := strings.TrimSuffix(filepath.Base(cuePath), filepath.Ext(cuePath)) + for _, ext := range commonExts { + candidate = filepath.Join(cueDir, cueBase+ext) + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + + // 4. If there's only one audio file in the directory, use that + entries, err := os.ReadDir(cueDir) + if err == nil { + audioExts := map[string]bool{ + ".flac": true, ".wav": true, ".ape": true, ".mp3": true, + ".ogg": true, ".wv": true, ".m4a": true, ".aiff": true, + } + var audioFiles []string + for _, entry := range entries { + if entry.IsDir() { + continue + } + ext := strings.ToLower(filepath.Ext(entry.Name())) + if audioExts[ext] { + audioFiles = append(audioFiles, filepath.Join(cueDir, entry.Name())) + } + } + if len(audioFiles) == 1 { + return audioFiles[0] + } + } + + return "" +} + +// BuildCueSplitInfo creates the split information from a parsed CUE sheet. +// This is returned to the Dart side so FFmpeg can perform the splitting. +// audioDir, if non-empty, overrides the directory for audio file resolution. +func BuildCueSplitInfo(cuePath string, sheet *CueSheet, audioDir string) (*CueSplitInfo, error) { + resolveDir := cuePath + if audioDir != "" { + // Create a virtual path in audioDir so ResolveCueAudioPath looks there + resolveDir = filepath.Join(audioDir, filepath.Base(cuePath)) + } + audioPath := ResolveCueAudioPath(resolveDir, sheet.FileName) + if audioPath == "" { + return nil, fmt.Errorf("audio file not found for cue sheet: %s (referenced: %s)", cuePath, sheet.FileName) + } + + info := &CueSplitInfo{ + CuePath: cuePath, + AudioPath: audioPath, + Album: sheet.Title, + Artist: sheet.Performer, + Genre: sheet.Genre, + Date: sheet.Date, + } + + for i, track := range sheet.Tracks { + performer := track.Performer + if performer == "" { + performer = sheet.Performer + } + + composer := track.Composer + if composer == "" { + composer = sheet.Composer + } + + // End time is the start of the next track, or -1 for the last track + endSec := float64(-1) + if i+1 < len(sheet.Tracks) { + nextTrack := sheet.Tracks[i+1] + // Use pre-gap of next track if available, otherwise its start time + if nextTrack.PreGap >= 0 { + endSec = nextTrack.PreGap + } else { + endSec = nextTrack.StartTime + } + } + + info.Tracks = append(info.Tracks, CueSplitTrack{ + Number: track.Number, + Title: track.Title, + Artist: performer, + ISRC: track.ISRC, + Composer: composer, + StartSec: track.StartTime, + EndSec: endSec, + }) + } + + return info, nil +} + +// ParseCueFileJSON parses a .cue file and returns JSON with split info. +// This is the main entry point called from Dart via the platform bridge. +// audioDir, if non-empty, overrides the directory used for resolving the +// referenced audio file (useful when the .cue was copied to a temp dir +// but the audio still lives in the original location, e.g. SAF). +func ParseCueFileJSON(cuePath string, audioDir string) (string, error) { + sheet, err := ParseCueFile(cuePath) + if err != nil { + return "", fmt.Errorf("failed to parse cue file: %w", err) + } + + info, err := BuildCueSplitInfo(cuePath, sheet, audioDir) + if err != nil { + return "", err + } + + jsonBytes, err := json.Marshal(info) + if err != nil { + return "", fmt.Errorf("failed to marshal cue split info: %w", err) + } + + return string(jsonBytes), nil +} + +// ScanCueFileForLibrary parses a .cue file and returns multiple LibraryScanResult +// entries, one per track. This is used by the library scanner to populate the +// library with individual track entries from a single CUE+FLAC album. +func ScanCueFileForLibrary(cuePath string, scanTime string) ([]LibraryScanResult, error) { + return scanCueFileForLibraryInternal(cuePath, "", "", 0, scanTime) +} + +// ScanCueFileForLibraryExt is like ScanCueFileForLibrary but with extra parameters +// for SAF (Storage Access Framework) scenarios: +// - audioDir: if non-empty, overrides the directory used to find the audio file +// - virtualPathPrefix: if non-empty, used instead of cuePath as the base for +// virtual file paths (e.g. a content:// URI). IDs are also based on this. +// - fileModTime: if > 0, used as the FileModTime for all results instead of +// stat-ing the cuePath on disk (useful when the real file lives behind SAF) +func ScanCueFileForLibraryExt(cuePath, audioDir, virtualPathPrefix string, fileModTime int64, scanTime string) ([]LibraryScanResult, error) { + return scanCueFileForLibraryInternal(cuePath, audioDir, virtualPathPrefix, fileModTime, scanTime) +} + +func scanCueFileForLibraryInternal(cuePath, audioDir, virtualPathPrefix string, fileModTime int64, scanTime string) ([]LibraryScanResult, error) { + sheet, err := ParseCueFile(cuePath) + if err != nil { + return nil, err + } + + // Resolve audio file — optionally in an overridden directory + resolveBase := cuePath + if audioDir != "" { + resolveBase = filepath.Join(audioDir, filepath.Base(cuePath)) + } + audioPath := ResolveCueAudioPath(resolveBase, sheet.FileName) + if audioPath == "" { + return nil, fmt.Errorf("audio file not found for cue: %s (referenced: %s)", cuePath, sheet.FileName) + } + + // Try to get quality info from the audio file + var bitDepth, sampleRate int + var totalDurationSec float64 + audioExt := strings.ToLower(filepath.Ext(audioPath)) + switch audioExt { + case ".flac": + quality, qErr := GetAudioQuality(audioPath) + if qErr == nil { + bitDepth = quality.BitDepth + sampleRate = quality.SampleRate + if quality.SampleRate > 0 && quality.TotalSamples > 0 { + totalDurationSec = float64(quality.TotalSamples) / float64(quality.SampleRate) + } + } + case ".mp3": + quality, qErr := GetMP3Quality(audioPath) + if qErr == nil { + sampleRate = quality.SampleRate + totalDurationSec = float64(quality.Duration) + } + } + + // Extract cover from audio file for all tracks + var coverPath string + libraryCoverCacheMu.RLock() + coverCacheDir := libraryCoverCacheDir + libraryCoverCacheMu.RUnlock() + if coverCacheDir != "" { + cp, err := SaveCoverToCache(audioPath, coverCacheDir) + if err == nil && cp != "" { + coverPath = cp + } + } + + // Determine the base path for virtual paths and IDs + pathBase := cuePath + if virtualPathPrefix != "" { + pathBase = virtualPathPrefix + } + + // Determine fileModTime + modTime := fileModTime + if modTime <= 0 { + if info, err := os.Stat(cuePath); err == nil { + modTime = info.ModTime().UnixMilli() + } + } + + var results []LibraryScanResult + for i, track := range sheet.Tracks { + performer := track.Performer + if performer == "" { + performer = sheet.Performer + } + if performer == "" { + performer = "Unknown Artist" + } + + title := track.Title + if title == "" { + title = fmt.Sprintf("Track %02d", track.Number) + } + + album := sheet.Title + if album == "" { + album = "Unknown Album" + } + + // Calculate duration for this track + var duration int + if i+1 < len(sheet.Tracks) { + nextStart := sheet.Tracks[i+1].StartTime + if sheet.Tracks[i+1].PreGap >= 0 { + nextStart = sheet.Tracks[i+1].PreGap + } + duration = int(nextStart - track.StartTime) + } else if totalDurationSec > 0 { + duration = int(totalDurationSec - track.StartTime) + } + + // Use a unique ID based on pathBase + track number + id := generateLibraryID(fmt.Sprintf("%s#track%d", pathBase, track.Number)) + + // Use a virtual file path that includes the track number to ensure + // uniqueness in the database (file_path has a UNIQUE constraint). + // Format: /path/to/album.cue#track01 or content://...album.cue#track01 + virtualFilePath := fmt.Sprintf("%s#track%02d", pathBase, track.Number) + + result := LibraryScanResult{ + ID: id, + TrackName: title, + ArtistName: performer, + AlbumName: album, + AlbumArtist: sheet.Performer, + FilePath: virtualFilePath, + CoverPath: coverPath, + ScannedAt: scanTime, + ISRC: track.ISRC, + TrackNumber: track.Number, + DiscNumber: 1, + Duration: duration, + ReleaseDate: sheet.Date, + BitDepth: bitDepth, + SampleRate: sampleRate, + Genre: sheet.Genre, + Format: "cue+" + strings.TrimPrefix(audioExt, "."), + } + + result.FileModTime = modTime + + results = append(results, result) + } + + return results, nil +} diff --git a/go_backend/exports.go b/go_backend/exports.go index 08efb1fb..5f83330a 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -901,6 +901,32 @@ func ReadFileMetadata(filePath string) (string, error) { return string(jsonBytes), nil } +// ParseCueSheet parses a .cue file and returns JSON with split information. +// This is called from Dart to get track listing and timing data for CUE splitting. +// audioDir, if non-empty, overrides the directory used for resolving the +// referenced audio file (useful for SAF temp file scenarios). +func ParseCueSheet(cuePath string, audioDir string) (string, error) { + return ParseCueFileJSON(cuePath, audioDir) +} + +// ScanCueSheetForLibrary parses a .cue file and returns a JSON array of +// LibraryScanResult entries (one per track). This is the SAF-friendly variant: +// - audioDir overrides where the referenced audio file is resolved +// - virtualPathPrefix replaces cuePath in filePath / id fields (e.g. a content:// URI) +// - fileModTime is stamped on every result (pass 0 to stat cuePath instead) +func ScanCueSheetForLibrary(cuePath, audioDir, virtualPathPrefix string, fileModTime int64) (string, error) { + scanTime := time.Now().UTC().Format(time.RFC3339) + results, err := ScanCueFileForLibraryExt(cuePath, audioDir, virtualPathPrefix, fileModTime, scanTime) + if err != nil { + return "[]", err + } + jsonBytes, err := json.Marshal(results) + if err != nil { + return "[]", fmt.Errorf("failed to marshal cue scan results: %w", err) + } + return string(jsonBytes), nil +} + // EditFileMetadata writes metadata to an audio file. // For FLAC files, uses native Go FLAC library. // For MP3/Opus, returns the metadata map so Dart can use FFmpeg. diff --git a/go_backend/library_scan.go b/go_backend/library_scan.go index d9e53663..34d5c718 100644 --- a/go_backend/library_scan.go +++ b/go_backend/library_scan.go @@ -63,6 +63,7 @@ var supportedAudioFormats = map[string]bool{ ".mp3": true, ".opus": true, ".ogg": true, + ".cue": true, } type libraryAudioFileInfo struct { @@ -166,6 +167,23 @@ func ScanLibraryFolder(folderPath string) (string, error) { scanTime := time.Now().UTC().Format(time.RFC3339) errorCount := 0 + // Track audio files referenced by .cue sheets to avoid duplicates + cueReferencedAudioFiles := make(map[string]bool) + + // First pass: scan .cue files to collect referenced audio paths + for _, filePath := range audioFiles { + ext := strings.ToLower(filepath.Ext(filePath)) + if ext == ".cue" { + sheet, err := ParseCueFile(filePath) + if err == nil && sheet.FileName != "" { + audioPath := ResolveCueAudioPath(filePath, sheet.FileName) + if audioPath != "" { + cueReferencedAudioFiles[audioPath] = true + } + } + } + } + for i, filePath := range audioFiles { select { case <-cancelCh: @@ -179,6 +197,28 @@ func ScanLibraryFolder(folderPath string) (string, error) { libraryScanProgress.ProgressPct = float64(i+1) / float64(totalFiles) * 100 libraryScanProgressMu.Unlock() + ext := strings.ToLower(filepath.Ext(filePath)) + + // Handle .cue files: produce multiple track results + if ext == ".cue" { + cueResults, err := ScanCueFileForLibrary(filePath, scanTime) + if err != nil { + errorCount++ + GoLog("[LibraryScan] Error scanning cue %s: %v\n", filePath, err) + continue + } + results = append(results, cueResults...) + GoLog("[LibraryScan] CUE sheet %s: %d tracks\n", filepath.Base(filePath), len(cueResults)) + continue + } + + // Skip audio files that are referenced by a .cue sheet + // (they will be represented by the cue sheet's track entries instead) + if cueReferencedAudioFiles[filePath] { + GoLog("[LibraryScan] Skipping %s (referenced by .cue sheet)\n", filepath.Base(filePath)) + continue + } + result, err := scanAudioFile(filePath, scanTime) if err != nil { errorCount++ @@ -502,9 +542,44 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, var filesToScan []libraryAudioFileInfo skippedCount := 0 + // Build a set of existing CUE virtual path base files for incremental matching. + // CUE tracks are stored with virtual paths like "/path/album.cue#track01". + // We need to match these against the actual .cue file's modTime. + cueBaseModTimes := make(map[string]int64) // base cue path -> modTime from disk + for _, f := range currentFiles { + if strings.ToLower(filepath.Ext(f.path)) == ".cue" { + cueBaseModTimes[f.path] = f.modTime + } + } + for _, f := range currentFiles { existingModTime, exists := existingFiles[f.path] if !exists { + // For .cue files, also check if any virtual path entries exist + if strings.ToLower(filepath.Ext(f.path)) == ".cue" { + hasCueTracks := false + for existingPath := range existingFiles { + if strings.HasPrefix(existingPath, f.path+"#track") { + hasCueTracks = true + break + } + } + if hasCueTracks { + // CUE file exists in DB via virtual paths; check if modTime changed + // Use modTime from any virtual path (they all share the same .cue modTime) + for existingPath, modTime := range existingFiles { + if strings.HasPrefix(existingPath, f.path+"#track") { + if f.modTime == modTime { + skippedCount++ + } else { + filesToScan = append(filesToScan, f) + } + break + } + } + continue + } + } filesToScan = append(filesToScan, f) } else if f.modTime != existingModTime { filesToScan = append(filesToScan, f) @@ -515,7 +590,16 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, var deletedPaths []string for existingPath := range existingFiles { - if !currentPathSet[existingPath] { + // For CUE virtual paths (e.g. "/path/album.cue#track01"), + // check if the base .cue file still exists on disk + if idx := strings.LastIndex(existingPath, "#track"); idx > 0 { + baseCuePath := existingPath[:idx] + if currentPathSet[baseCuePath] { + continue // Base .cue file still exists, not deleted + } + // Base CUE file is gone, mark virtual path as deleted + deletedPaths = append(deletedPaths, existingPath) + } else if !currentPathSet[existingPath] { deletedPaths = append(deletedPaths, existingPath) } } @@ -544,6 +628,21 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, scanTime := time.Now().UTC().Format(time.RFC3339) errorCount := 0 + // Track audio files referenced by .cue sheets to avoid duplicates (incremental) + cueReferencedAudioFilesInc := make(map[string]bool) + for _, f := range filesToScan { + ext := strings.ToLower(filepath.Ext(f.path)) + if ext == ".cue" { + sheet, err := ParseCueFile(f.path) + if err == nil && sheet.FileName != "" { + audioPath := ResolveCueAudioPath(f.path, sheet.FileName) + if audioPath != "" { + cueReferencedAudioFilesInc[audioPath] = true + } + } + } + } + for i, f := range filesToScan { select { case <-cancelCh: @@ -557,6 +656,25 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string, libraryScanProgress.ProgressPct = float64(skippedCount+i+1) / float64(totalFiles) * 100 libraryScanProgressMu.Unlock() + ext := strings.ToLower(filepath.Ext(f.path)) + + // Handle .cue files: produce multiple track results + if ext == ".cue" { + cueResults, err := ScanCueFileForLibrary(f.path, scanTime) + if err != nil { + errorCount++ + GoLog("[LibraryScan] Error scanning cue %s: %v\n", f.path, err) + continue + } + results = append(results, cueResults...) + continue + } + + // Skip audio files referenced by .cue sheets + if cueReferencedAudioFilesInc[f.path] { + continue + } + result, err := scanAudioFile(f.path, scanTime) if err != nil { errorCount++ diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 4cb53238..b85020da 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -969,6 +969,15 @@ import Gobackend // Import Go framework if let error = error { throw error } return response + // CUE Sheet Parsing + case "parseCueSheet": + let args = call.arguments as! [String: Any] + let cuePath = args["cue_path"] as! String + let audioDir = args["audio_dir"] as? String ?? "" + let response = GobackendParseCueSheet(cuePath, audioDir, &error) + if let error = error { throw error } + return response + default: throw NSError( domain: "SpotiFLAC", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c059621b..d722f825 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3812,6 +3812,78 @@ abstract class AppLocalizations { /// **'Conversion failed'** String get trackConvertFailed; + /// Title for CUE split bottom sheet + /// + /// In en, this message translates to: + /// **'Split CUE Sheet'** + String get cueSplitTitle; + + /// Subtitle for CUE split menu item + /// + /// In en, this message translates to: + /// **'Split CUE+FLAC into individual tracks'** + String get cueSplitSubtitle; + + /// Album name in CUE split sheet + /// + /// In en, this message translates to: + /// **'Album: {album}'** + String cueSplitAlbum(String album); + + /// Artist name in CUE split sheet + /// + /// In en, this message translates to: + /// **'Artist: {artist}'** + String cueSplitArtist(String artist); + + /// Number of tracks in CUE sheet + /// + /// In en, this message translates to: + /// **'{count} tracks'** + String cueSplitTrackCount(int count); + + /// CUE split confirmation dialog title + /// + /// In en, this message translates to: + /// **'Split CUE Album'** + String get cueSplitConfirmTitle; + + /// CUE split confirmation dialog message + /// + /// In en, this message translates to: + /// **'Split \"{album}\" into {count} individual FLAC files?\n\nFiles will be saved to the same directory.'** + String cueSplitConfirmMessage(String album, int count); + + /// Snackbar while splitting CUE + /// + /// In en, this message translates to: + /// **'Splitting CUE sheet... ({current}/{total})'** + String cueSplitSplitting(int current, int total); + + /// Snackbar after successful CUE split + /// + /// In en, this message translates to: + /// **'Split into {count} tracks successfully'** + String cueSplitSuccess(int count); + + /// Snackbar when CUE split fails + /// + /// In en, this message translates to: + /// **'CUE split failed'** + String get cueSplitFailed; + + /// Error when CUE audio file is missing + /// + /// In en, this message translates to: + /// **'Audio file not found for this CUE sheet'** + String get cueSplitNoAudioFile; + + /// Button text to start CUE splitting + /// + /// In en, this message translates to: + /// **'Split into Tracks'** + String get cueSplitButton; + /// Generic action button - create /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 246f5314..1bb33ef5 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2164,6 +2164,54 @@ class AppLocalizationsDe extends AppLocalizations { @override String get trackConvertFailed => 'Konvertierung fehlgeschlagen'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Erstellen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index c583ab11..bd9d4589 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2137,6 +2137,54 @@ class AppLocalizationsEn extends AppLocalizations { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index f4758a27..ed9236ad 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2137,6 +2137,54 @@ class AppLocalizationsEs extends AppLocalizations { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index c93b863f..d6a3b948 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2139,6 +2139,54 @@ class AppLocalizationsFr extends AppLocalizations { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index a12620b5..a461bd5b 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2137,6 +2137,54 @@ class AppLocalizationsHi extends AppLocalizations { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index e408e8ae..b668481b 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2144,6 +2144,54 @@ class AppLocalizationsId extends AppLocalizations { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 9e898bdf..71d10e1a 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2124,6 +2124,54 @@ class AppLocalizationsJa extends AppLocalizations { @override String get trackConvertFailed => '変換に失敗しました'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 66c66e99..43bfbce3 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2117,6 +2117,54 @@ class AppLocalizationsKo extends AppLocalizations { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 6144f9b6..2eeb00cc 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2137,6 +2137,54 @@ class AppLocalizationsNl extends AppLocalizations { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index d5c8d821..9436f6eb 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2137,6 +2137,54 @@ class AppLocalizationsPt extends AppLocalizations { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 909b5842..943b75f9 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2190,6 +2190,54 @@ class AppLocalizationsRu extends AppLocalizations { @override String get trackConvertFailed => 'Ошибка конвертации'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Создать'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index b608cff7..223fecd3 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2149,6 +2149,54 @@ class AppLocalizationsTr extends AppLocalizations { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 4104845c..57f92319 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2137,6 +2137,54 @@ class AppLocalizationsZh extends AppLocalizations { @override String get trackConvertFailed => 'Conversion failed'; + @override + String get cueSplitTitle => 'Split CUE Sheet'; + + @override + String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks'; + + @override + String cueSplitAlbum(String album) { + return 'Album: $album'; + } + + @override + String cueSplitArtist(String artist) { + return 'Artist: $artist'; + } + + @override + String cueSplitTrackCount(int count) { + return '$count tracks'; + } + + @override + String get cueSplitConfirmTitle => 'Split CUE Album'; + + @override + String cueSplitConfirmMessage(String album, int count) { + return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.'; + } + + @override + String cueSplitSplitting(int current, int total) { + return 'Splitting CUE sheet... ($current/$total)'; + } + + @override + String cueSplitSuccess(int count) { + return 'Split into $count tracks successfully'; + } + + @override + String get cueSplitFailed => 'CUE split failed'; + + @override + String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet'; + + @override + String get cueSplitButton => 'Split into Tracks'; + @override String get actionCreate => 'Create'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index bb8bac99..bb3036c9 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2820,6 +2820,90 @@ "@trackConvertFailed": { "description": "Snackbar when conversion fails" }, + "cueSplitTitle": "Split CUE Sheet", + "@cueSplitTitle": { + "description": "Title for CUE split bottom sheet" + }, + "cueSplitSubtitle": "Split CUE+FLAC into individual tracks", + "@cueSplitSubtitle": { + "description": "Subtitle for CUE split menu item" + }, + "cueSplitAlbum": "Album: {album}", + "@cueSplitAlbum": { + "description": "Album name in CUE split sheet", + "placeholders": { + "album": { + "type": "String" + } + } + }, + "cueSplitArtist": "Artist: {artist}", + "@cueSplitArtist": { + "description": "Artist name in CUE split sheet", + "placeholders": { + "artist": { + "type": "String" + } + } + }, + "cueSplitTrackCount": "{count} tracks", + "@cueSplitTrackCount": { + "description": "Number of tracks in CUE sheet", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cueSplitConfirmTitle": "Split CUE Album", + "@cueSplitConfirmTitle": { + "description": "CUE split confirmation dialog title" + }, + "cueSplitConfirmMessage": "Split \"{album}\" into {count} individual FLAC files?\n\nFiles will be saved to the same directory.", + "@cueSplitConfirmMessage": { + "description": "CUE split confirmation dialog message", + "placeholders": { + "album": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cueSplitSplitting": "Splitting CUE sheet... ({current}/{total})", + "@cueSplitSplitting": { + "description": "Snackbar while splitting CUE", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "cueSplitSuccess": "Split into {count} tracks successfully", + "@cueSplitSuccess": { + "description": "Snackbar after successful CUE split", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cueSplitFailed": "CUE split failed", + "@cueSplitFailed": { + "description": "Snackbar when CUE split fails" + }, + "cueSplitNoAudioFile": "Audio file not found for this CUE sheet", + "@cueSplitNoAudioFile": { + "description": "Error when CUE audio file is missing" + }, + "cueSplitButton": "Split into Tracks", + "@cueSplitButton": { + "description": "Button text to start CUE splitting" + }, "actionCreate": "Create", "@actionCreate": { "description": "Generic action button - create" diff --git a/lib/providers/playback_provider.dart b/lib/providers/playback_provider.dart index c35f6a00..bcaa14c0 100644 --- a/lib/providers/playback_provider.dart +++ b/lib/providers/playback_provider.dart @@ -24,6 +24,9 @@ class PlaybackController extends Notifier { String coverUrl = '', Track? track, }) async { + if (isCueVirtualPath(path)) { + throw Exception(cueVirtualTrackRequiresSplitMessage); + } _log.d('Opening external player for "$title" by $artist: $path'); await openFile(path); } @@ -32,11 +35,16 @@ class PlaybackController extends Notifier { if (tracks.isEmpty) return; final orderedTracks = _orderedTracksFromStartIndex(tracks, startIndex); + var skippedCueVirtualTrack = false; for (final track in orderedTracks) { final resolvedPath = await _resolveTrackPath(track); if (resolvedPath == null) { continue; } + if (isCueVirtualPath(resolvedPath)) { + skippedCueVirtualTrack = true; + continue; + } _log.d( 'Opening first available external track for list playback: ' @@ -46,6 +54,10 @@ class PlaybackController extends Notifier { return; } + if (skippedCueVirtualTrack) { + throw Exception(cueVirtualTrackRequiresSplitMessage); + } + throw Exception( 'No local audio file is available to open. Download the track first.', ); diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 79eca05b..9c560546 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -38,6 +38,14 @@ class _LocalAlbumScreenState extends ConsumerState { final ScrollController _scrollController = ScrollController(); late List _sortedTracksCache; late Map> _discGroupsCache; + + void _showCueVirtualTrackSnackBar() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text(cueVirtualTrackRequiresSplitMessage), + ), + ); + } late List _sortedDiscNumbersCache; late bool _hasMultipleDiscsCache; String? _commonQualityCache; @@ -178,9 +186,11 @@ class _LocalAlbumScreenState extends ConsumerState { for (final id in idsToDelete) { final item = tracksById[id]; if (item != null) { - try { - await deleteFile(item.filePath); - } catch (_) {} + if (!isCueVirtualPath(item.filePath)) { + try { + await deleteFile(item.filePath); + } catch (_) {} + } await libraryNotifier.removeItem(id); deletedCount++; } @@ -203,6 +213,10 @@ class _LocalAlbumScreenState extends ConsumerState { } Future _openFile(LocalLibraryItem track) async { + if (isCueVirtualPath(track.filePath)) { + _showCueVirtualTrackSnackBar(); + return; + } try { await ref .read(playbackProvider.notifier) diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index fdcd3eef..d8693612 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -13,11 +13,14 @@ import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:share_plus/share_plus.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; +import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/playback_provider.dart'; +import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/services/ffmpeg_service.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/logger.dart'; +import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; final _log = AppLogger('TrackMetadata'); @@ -214,10 +217,7 @@ class _TrackMetadataScreenState extends ConsumerState { } Future _checkFile() async { - var filePath = _filePath; - if (filePath.startsWith('EXISTS:')) { - filePath = filePath.substring(7); - } + final filePath = cleanFilePath; bool exists = false; int? size; @@ -544,11 +544,65 @@ class _TrackMetadataScreenState extends ConsumerState { ); } - String get cleanFilePath { + /// The raw file path, with EXISTS: prefix stripped but #trackNN preserved. + /// Use this when you need the full virtual path (e.g. for display or DB lookups). + String get rawFilePath { final path = _filePath; return path.startsWith('EXISTS:') ? path.substring(7) : path; } + /// The clean file path with both EXISTS: prefix and #trackNN suffix stripped. + /// Use this for actual filesystem/SAF operations. + String get cleanFilePath { + var path = _filePath; + if (path.startsWith('EXISTS:')) path = path.substring(7); + // Strip CUE virtual path suffix for filesystem operations + if (isCueVirtualPath(path)) path = stripCueTrackSuffix(path); + return path; + } + + bool get _isCueVirtualTrack => isCueVirtualPath(rawFilePath); + + String _cueVirtualTrackGuidance(BuildContext context) { + return 'This CUE track is virtual. Use ${context.l10n.cueSplitButton} first.'; + } + + void _showCueVirtualTrackSnackBar(BuildContext context) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(_cueVirtualTrackGuidance(context))), + ); + } + + void _hideCurrentSnackBar() { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + } + + String get _l10nCueSplitFailed => context.l10n.cueSplitFailed; + String get _l10nCueSplitNoAudioFile => context.l10n.cueSplitNoAudioFile; + + String _l10nCueSplitSplitting(int current, int total) { + return context.l10n.cueSplitSplitting(current, total); + } + + String _l10nCueSplitSuccess(int count) { + return context.l10n.cueSplitSuccess(count); + } + + void _showSnackBarMessage(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + + void _showLongSnackBarMessage(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(seconds: 60), + ), + ); + } + String _formatPathForDisplay(String pathOrUri) { if (pathOrUri.isEmpty || !pathOrUri.startsWith('content://')) { return pathOrUri; @@ -1153,8 +1207,8 @@ class _TrackMetadataScreenState extends ConsumerState { bool fileExists, int? fileSize, ) { - final displayFilePath = _formatPathForDisplay(cleanFilePath); - final fileName = _extractFileNameFromPathOrUri(cleanFilePath); + final displayFilePath = _formatPathForDisplay(rawFilePath); + final fileName = _extractFileNameFromPathOrUri(rawFilePath); final fileExtension = fileName.contains('.') ? fileName.split('.').last.toUpperCase() : 'Unknown'; @@ -2365,7 +2419,7 @@ class _TrackMetadataScreenState extends ConsumerState { flex: 2, child: FilledButton.icon( onPressed: fileExists - ? () => _openFile(context, cleanFilePath) + ? () => _openFile(context, rawFilePath) : null, icon: const Icon(Icons.play_arrow), label: Text(context.l10n.trackMetadataPlay), @@ -2487,6 +2541,16 @@ class _TrackMetadataScreenState extends ConsumerState { _showConvertSheet(context); }, ), + if (_fileExists && _isCueFile) + ListTile( + leading: const Icon(Icons.call_split), + title: Text(context.l10n.cueSplitTitle), + subtitle: Text(context.l10n.cueSplitSubtitle), + onTap: () { + Navigator.pop(context); + _showCueSplitSheet(context); + }, + ), const Divider(height: 1), ListTile( leading: const Icon(Icons.share), @@ -2524,11 +2588,34 @@ class _TrackMetadataScreenState extends ConsumerState { lower.endsWith('.ogg'); } + /// Whether the current file is a CUE sheet (or CUE-referenced) + bool get _isCueFile { + // Check if the raw path has a CUE virtual path suffix + if (isCueVirtualPath(rawFilePath)) return true; + final lower = cleanFilePath.toLowerCase(); + if (lower.endsWith('.cue')) return true; + // Check if local library item has cue+ format + if (_isLocalItem && _localLibraryItem != null) { + final format = _localLibraryItem!.format ?? ''; + if (format.startsWith('cue+')) return true; + } + return false; + } + String get _currentFileFormat { + // For CUE tracks, use the format from the library item (e.g. "cue+flac") + if (_isCueFile && _isLocalItem && _localLibraryItem != null) { + final format = _localLibraryItem!.format ?? ''; + if (format.startsWith('cue+')) { + final audioFmt = format.substring(4).toUpperCase(); + return 'CUE+$audioFmt'; + } + } final lower = cleanFilePath.toLowerCase(); if (lower.endsWith('.flac')) return 'FLAC'; if (lower.endsWith('.mp3')) return 'MP3'; if (lower.endsWith('.opus') || lower.endsWith('.ogg')) return 'Opus'; + if (lower.endsWith('.cue')) return 'CUE'; return 'Unknown'; } @@ -2766,6 +2853,470 @@ class _TrackMetadataScreenState extends ConsumerState { ); } + void _showCueSplitSheet(BuildContext context) async { + // Strip the #trackNN suffix from virtual CUE paths to get the real .cue path + var cuePath = cleanFilePath; + final trackSuffix = RegExp(r'#track\d+$'); + if (trackSuffix.hasMatch(cuePath)) { + cuePath = cuePath.replaceFirst(trackSuffix, ''); + } + + // Show loading indicator + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Loading CUE sheet...')), + ); + + try { + final cueInfo = await PlatformBridge.parseCueSheet(cuePath); + + if (!mounted) return; + _hideCurrentSnackBar(); + + if (cueInfo.containsKey('error')) { + _showSnackBarMessage(_l10nCueSplitNoAudioFile); + return; + } + + final album = cueInfo['album'] as String? ?? 'Unknown Album'; + final artist = cueInfo['artist'] as String? ?? 'Unknown Artist'; + final audioPath = cueInfo['audio_path'] as String? ?? ''; + final genre = cueInfo['genre'] as String? ?? ''; + final date = cueInfo['date'] as String? ?? ''; + final tracksRaw = cueInfo['tracks'] as List? ?? []; + + if (audioPath.isEmpty) { + _showSnackBarMessage(_l10nCueSplitNoAudioFile); + return; + } + + final tracks = tracksRaw + .map((t) => CueSplitTrackInfo.fromJson(t as Map)) + .toList(); + + if (tracks.isEmpty) { + _showSnackBarMessage(_l10nCueSplitFailed); + return; + } + + if (!mounted) return; + + showModalBottomSheet( + context: this.context, + useRootNavigator: true, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (sheetContext) { + final colorScheme = Theme.of(sheetContext).colorScheme; + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: colorScheme.onSurfaceVariant.withValues( + alpha: 0.4, + ), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const SizedBox(height: 16), + Text( + sheetContext.l10n.cueSplitTitle, + style: Theme.of(sheetContext).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Text( + sheetContext.l10n.cueSplitAlbum(album), + style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + sheetContext.l10n.cueSplitArtist(artist), + style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + sheetContext.l10n.cueSplitTrackCount(tracks.length), + style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + // Track list preview (scrollable, max 200px) + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200), + child: ListView.builder( + shrinkWrap: true, + itemCount: tracks.length, + itemBuilder: (context, index) { + final track = tracks[index]; + final duration = track.endSec > 0 + ? track.endSec - track.startSec + : 0.0; + final durationStr = duration > 0 + ? '${(duration ~/ 60).toString().padLeft(2, '0')}:${(duration.toInt() % 60).toString().padLeft(2, '0')}' + : ''; + return ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + radius: 14, + backgroundColor: colorScheme.primaryContainer, + child: Text( + '${track.number}', + style: TextStyle( + fontSize: 11, + color: colorScheme.onPrimaryContainer, + ), + ), + ), + title: Text( + track.title, + style: const TextStyle(fontSize: 13), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: track.artist.isNotEmpty + ? Text( + track.artist, + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : null, + trailing: durationStr.isNotEmpty + ? Text( + durationStr, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ) + : null, + ); + }, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: () { + Navigator.pop(sheetContext); + _confirmAndSplitCue( + context: this.context, + audioPath: audioPath, + album: album, + artist: artist, + genre: genre, + date: date, + tracks: tracks, + ); + }, + icon: const Icon(Icons.call_split), + label: Text(sheetContext.l10n.cueSplitButton), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ); + }, + ); + } catch (e) { + if (!mounted) return; + _hideCurrentSnackBar(); + _showSnackBarMessage(_l10nCueSplitFailed); + _log.e('Failed to parse CUE sheet: $e'); + } + } + + void _confirmAndSplitCue({ + required BuildContext context, + required String audioPath, + required String album, + required String artist, + required String genre, + required String date, + required List tracks, + }) { + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Text(dialogContext.l10n.cueSplitConfirmTitle), + content: Text( + dialogContext.l10n.cueSplitConfirmMessage(album, tracks.length), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(dialogContext.l10n.dialogCancel), + ), + FilledButton( + onPressed: () { + Navigator.pop(dialogContext); + _performCueSplit( + audioPath: audioPath, + album: album, + artist: artist, + genre: genre, + date: date, + tracks: tracks, + ); + }, + child: Text(dialogContext.l10n.cueSplitButton), + ), + ], + ); + }, + ); + } + + Future _resolvePersistentCueSplitOutputDir() async { + final settings = ref.read(settingsProvider); + final queueState = ref.read(downloadQueueProvider); + final configuredOutputDir = queueState.outputDir.trim(); + if (settings.storageMode != 'saf' && + configuredOutputDir.isNotEmpty && + !isContentUri(configuredOutputDir)) { + final dir = Directory(configuredOutputDir); + await dir.create(recursive: true); + return dir; + } + + if (Platform.isAndroid) { + final externalDir = await getExternalStorageDirectory(); + if (externalDir != null) { + final musicDir = Directory( + '${externalDir.parent.parent.parent.parent.path}' + '${Platform.pathSeparator}Music' + '${Platform.pathSeparator}SpotiFLAC', + ); + await musicDir.create(recursive: true); + return musicDir; + } + } + + final docsDir = await getApplicationDocumentsDirectory(); + final fallbackDir = Directory( + '${docsDir.path}${Platform.pathSeparator}SpotiFLAC', + ); + await fallbackDir.create(recursive: true); + return fallbackDir; + } + + Future?> _exportCueSplitOutputsToSaf({ + required List outputPaths, + required String treeUri, + required String relativeDir, + }) async { + final exportedUris = []; + for (final path in outputPaths) { + final fileName = path.split(Platform.pathSeparator).last; + final safUri = await PlatformBridge.createSafFileFromPath( + treeUri: treeUri, + relativeDir: relativeDir, + fileName: fileName, + mimeType: audioMimeTypeForPath(path), + srcPath: path, + ); + if (safUri != null && safUri.isNotEmpty) { + exportedUris.add(safUri); + } + } + return exportedUris.isEmpty ? null : exportedUris; + } + + Future _performCueSplit({ + required String audioPath, + required String album, + required String artist, + required String genre, + required String date, + required List tracks, + }) async { + if (_isConverting) return; + setState(() => _isConverting = true); + + String? safTempAudioPath; + Directory? tempSplitDir; + try { + // For SAF content:// audio paths, copy to temp for FFmpeg processing + String workingAudioPath = audioPath; + final isSafSource = isContentUri(audioPath); + if (isSafSource) { + final tempPath = await PlatformBridge.copyContentUriToTemp(audioPath); + if (tempPath == null || tempPath.isEmpty) { + throw Exception('Failed to copy SAF audio file to temp'); + } + safTempAudioPath = tempPath; + workingAudioPath = tempPath; + } + + // Determine output directory + final String outputDir; + final treeUri = !_isLocalItem ? (_downloadItem?.downloadTreeUri ?? '') : ''; + final relativeDir = !_isLocalItem ? (_downloadItem?.safRelativeDir ?? '') : ''; + final writeBackToSaf = isSafSource && treeUri.isNotEmpty; + if (writeBackToSaf) { + final tempDir = await getTemporaryDirectory(); + tempSplitDir = Directory( + '${tempDir.path}${Platform.pathSeparator}' + 'cue_split_${DateTime.now().millisecondsSinceEpoch}', + ); + await tempSplitDir.create(recursive: true); + outputDir = tempSplitDir.path; + } else if (isSafSource) { + final persistentDir = await _resolvePersistentCueSplitOutputDir(); + outputDir = persistentDir.path; + } else { + outputDir = File(audioPath).parent.path; + } + + if (!mounted) return; + _showLongSnackBarMessage(_l10nCueSplitSplitting(1, tracks.length)); + + // Extract cover from audio file for embedding + String? coverPath; + try { + final tempDir = await getTemporaryDirectory(); + final coverOutput = + '${tempDir.path}${Platform.pathSeparator}cue_cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final coverResult = await PlatformBridge.extractCoverToFile( + workingAudioPath, + coverOutput, + ); + if (coverResult['error'] == null) { + coverPath = coverOutput; + } + } catch (_) {} + + final albumMetadata = { + 'artist': artist, + 'album': album, + 'genre': genre, + 'date': date, + }; + + final outputPaths = await FFmpegService.splitCueToTracks( + audioPath: workingAudioPath, + outputDir: outputDir, + tracks: tracks, + albumMetadata: albumMetadata, + coverPath: coverPath, + onProgress: (current, total) { + if (mounted) { + _hideCurrentSnackBar(); + _showLongSnackBarMessage(_l10nCueSplitSplitting(current, total)); + } + }, + ); + + var finalOutputPaths = outputPaths; + + // Embed cover art into split FLAC files using Go backend + if (coverPath != null && finalOutputPaths != null) { + for (final path in finalOutputPaths) { + if (path.toLowerCase().endsWith('.flac')) { + try { + // Read existing metadata first + final metadata = await PlatformBridge.readFileMetadata(path); + if (metadata['error'] == null) { + final fields = { + 'cover_path': coverPath, + }; + // Preserve existing fields + for (final entry in metadata.entries) { + if (entry.key == 'error' || entry.value == null) continue; + final v = entry.value.toString().trim(); + if (v.isNotEmpty) { + fields[entry.key] = v; + } + } + await PlatformBridge.editFileMetadata(path, fields); + } + } catch (e) { + _log.w('Failed to embed cover to split track: $e'); + } + } + } + } + + if (writeBackToSaf && finalOutputPaths != null) { + final exportedUris = await _exportCueSplitOutputsToSaf( + outputPaths: finalOutputPaths, + treeUri: treeUri, + relativeDir: relativeDir, + ); + finalOutputPaths = exportedUris; + } + + // Cleanup cover temp + if (coverPath != null) { + try { + await File(coverPath).delete(); + } catch (_) {} + } + + if (mounted) { + _hideCurrentSnackBar(); + if (finalOutputPaths != null && finalOutputPaths.isNotEmpty) { + _showSnackBarMessage(_l10nCueSplitSuccess(finalOutputPaths.length)); + } else { + _showSnackBarMessage(_l10nCueSplitFailed); + } + } + } catch (e) { + _log.e('CUE split failed: $e'); + if (mounted) { + _hideCurrentSnackBar(); + _showSnackBarMessage(_l10nCueSplitFailed); + } + } finally { + // Cleanup SAF temp audio copy + if (safTempAudioPath != null) { + try { + await File(safTempAudioPath).delete(); + } catch (_) {} + } + if (tempSplitDir != null) { + try { + await tempSplitDir.delete(recursive: true); + } catch (_) {} + } + if (mounted) { + setState(() => _isConverting = false); + } + } + } + void _confirmAndConvert({ required BuildContext context, required String sourceFormat, @@ -3117,14 +3668,17 @@ class _TrackMetadataScreenState extends ConsumerState { TextButton( onPressed: () async { if (_isLocalItem) { - // For local items, just delete the file - try { - await deleteFile(cleanFilePath); - } catch (e) { - debugPrint('Failed to delete file: $e'); + if (_isCueVirtualTrack && _localLibraryItem != null) { + await ref + .read(localLibraryProvider.notifier) + .removeItem(_localLibraryItem!.id); + } else { + try { + await deleteFile(cleanFilePath); + } catch (e) { + debugPrint('Failed to delete file: $e'); + } } - // Also remove from local library database - // ref.read(localLibraryProvider.notifier).removeItem(_localLibraryItem!.id); } else { try { await deleteFile(cleanFilePath); @@ -3153,6 +3707,10 @@ class _TrackMetadataScreenState extends ConsumerState { } Future _openFile(BuildContext context, String filePath) async { + if (isCueVirtualPath(filePath)) { + _showCueVirtualTrackSnackBar(context); + return; + } try { await ref .read(playbackProvider.notifier) @@ -3185,6 +3743,11 @@ class _TrackMetadataScreenState extends ConsumerState { } Future _shareFile(BuildContext context) async { + if (_isCueVirtualTrack) { + _showCueVirtualTrackSnackBar(context); + return; + } + String sharePath = cleanFilePath; if (!await fileExists(sharePath)) { if (context.mounted) { diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index 2b28ca9d..8154f03c 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1353,6 +1353,160 @@ class FFmpegService { return id3Map; } + + /// Split a CUE+audio file into individual track files using FFmpeg. + /// Each track is extracted with `-c copy` (no re-encoding) and metadata is embedded. + /// [audioPath] is the source audio file (FLAC, WAV, etc.) + /// [outputDir] is where individual track files will be saved + /// [tracks] is the list of track split info from the Go CUE parser + /// [albumMetadata] contains album-level metadata (artist, album, genre, date) + /// Returns list of output file paths on success, null on failure. + static Future?> splitCueToTracks({ + required String audioPath, + required String outputDir, + required List tracks, + required Map albumMetadata, + String? coverPath, + void Function(int current, int total)? onProgress, + }) async { + if (tracks.isEmpty) { + _log.e('No tracks to split'); + return null; + } + + final outputPaths = []; + final inputExt = audioPath.toLowerCase().split('.').last; + // For lossless formats, keep as FLAC; for others, keep original format + final outputExt = (inputExt == 'flac' || inputExt == 'wav' || inputExt == 'ape' || inputExt == 'wv') + ? 'flac' + : inputExt; + + for (var i = 0; i < tracks.length; i++) { + final track = tracks[i]; + onProgress?.call(i + 1, tracks.length); + + // Sanitize filename + final sanitizedTitle = track.title + .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + final trackNumStr = track.number.toString().padLeft(2, '0'); + final outputFileName = '$trackNumStr - $sanitizedTitle.$outputExt'; + final outputPath = '$outputDir${Platform.pathSeparator}$outputFileName'; + + // Build FFmpeg command for this track + final StringBuffer cmdBuffer = StringBuffer(); + cmdBuffer.write('-i "$audioPath" '); + + // Time range + final startTime = _formatSecondsForFFmpeg(track.startSec); + cmdBuffer.write('-ss $startTime '); + + if (track.endSec > 0) { + final endTime = _formatSecondsForFFmpeg(track.endSec); + cmdBuffer.write('-to $endTime '); + } + + if (outputExt == 'flac') { + cmdBuffer.write('-c:a flac -compression_level 8 '); + } else { + cmdBuffer.write('-c:a copy '); + } + + // Metadata + final artist = track.artist.isNotEmpty ? track.artist : (albumMetadata['artist'] ?? ''); + final album = albumMetadata['album'] ?? ''; + final genre = albumMetadata['genre'] ?? ''; + final date = albumMetadata['date'] ?? ''; + + void addMeta(String key, String value) { + if (value.isNotEmpty) { + final sanitized = value.replaceAll('"', '\\"'); + cmdBuffer.write('-metadata $key="$sanitized" '); + } + } + + addMeta('TITLE', track.title); + addMeta('ARTIST', artist); + addMeta('ALBUM', album); + addMeta('ALBUMARTIST', albumMetadata['artist'] ?? ''); + addMeta('TRACKNUMBER', track.number.toString()); + addMeta('GENRE', genre); + addMeta('DATE', date); + if (track.isrc.isNotEmpty) addMeta('ISRC', track.isrc); + if (track.composer.isNotEmpty) addMeta('COMPOSER', track.composer); + + cmdBuffer.write('"$outputPath" -y'); + + final command = cmdBuffer.toString(); + _log.d('CUE split track ${track.number}: ${_previewCommandForLog(command)}'); + + final result = await _execute(command); + if (!result.success) { + _log.e('CUE split failed for track ${track.number}: ${result.output}'); + // Continue with remaining tracks instead of failing completely + continue; + } + + // Embed cover art if available (for FLAC output) + if (coverPath != null && coverPath.isNotEmpty && outputExt == 'flac') { + // Use the Go backend for FLAC cover embedding via PlatformBridge + // (handled by the caller) + } + + outputPaths.add(outputPath); + _log.i('CUE split: track ${track.number} -> $outputFileName'); + } + + if (outputPaths.isEmpty) { + _log.e('CUE split: no tracks were successfully extracted'); + return null; + } + + _log.i('CUE split complete: ${outputPaths.length}/${tracks.length} tracks'); + return outputPaths; + } + + static String _formatSecondsForFFmpeg(double seconds) { + if (seconds < 0) return '0'; + final hours = seconds ~/ 3600; + final mins = (seconds % 3600) ~/ 60; + final secs = seconds - (hours * 3600) - (mins * 60); + return '${hours.toString().padLeft(2, '0')}:${mins.toInt().toString().padLeft(2, '0')}:${secs.toStringAsFixed(3).padLeft(6, '0')}'; + } +} + +/// Track info for CUE splitting, passed from the CUE parser +class CueSplitTrackInfo { + final int number; + final String title; + final String artist; + final String isrc; + final String composer; + final double startSec; + final double endSec; + + CueSplitTrackInfo({ + required this.number, + required this.title, + required this.artist, + this.isrc = '', + this.composer = '', + required this.startSec, + required this.endSec, + }); + + factory CueSplitTrackInfo.fromJson(Map json) { + return CueSplitTrackInfo( + number: json['number'] as int? ?? 0, + title: json['title'] as String? ?? '', + artist: json['artist'] as String? ?? '', + isrc: json['isrc'] as String? ?? '', + composer: json['composer'] as String? ?? '', + startSec: (json['start_sec'] as num?)?.toDouble() ?? 0.0, + endSec: (json['end_sec'] as num?)?.toDouble() ?? -1.0, + ); + } } class FFmpegResult { diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 86ae6d0d..bf20d621 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1298,4 +1298,20 @@ class PlatformBridge { await _channel.invokeMethod('clearStoreCache'); } + /// Parse a .cue file and return split information (track listing, timing, metadata). + /// Returns a map with: cue_path, audio_path, album, artist, genre, date, tracks[] + /// Each track has: number, title, artist, isrc, composer, start_sec, end_sec + /// [audioDir] optionally overrides the directory for audio file resolution (used for SAF). + static Future> parseCueSheet( + String cuePath, { + String audioDir = '', + }) async { + _log.i('parseCueSheet: $cuePath (audioDir: $audioDir)'); + final result = await _channel.invokeMethod('parseCueSheet', { + 'cue_path': cuePath, + 'audio_dir': audioDir, + }); + return jsonDecode(result as String) as Map; + } + } diff --git a/lib/utils/file_access.dart b/lib/utils/file_access.dart index 1dd4f1fc..026fa57e 100644 --- a/lib/utils/file_access.dart +++ b/lib/utils/file_access.dart @@ -212,16 +212,39 @@ bool isContentUri(String? path) { return path != null && path.startsWith('content://'); } +/// Pattern matching CUE virtual path suffixes like #track01, #track12, etc. +final _cueTrackSuffix = RegExp(r'#track\d+$'); + +const cueVirtualTrackRequiresSplitMessage = + 'This CUE track is virtual. Use Split into Tracks first.'; + +/// Whether the path is a CUE virtual path (contains #trackNN suffix). +bool isCueVirtualPath(String? path) { + return path != null && _cueTrackSuffix.hasMatch(path); +} + +/// Strip the #trackNN suffix from a CUE virtual path to get the base .cue path. +/// Returns the path unchanged if it's not a CUE virtual path. +String stripCueTrackSuffix(String path) { + return path.replaceFirst(_cueTrackSuffix, ''); +} + Future fileExists(String? path) async { if (path == null || path.isEmpty) return false; - if (isContentUri(path)) { - return PlatformBridge.safExists(path); + // For CUE virtual paths, check if the base .cue file exists + final realPath = isCueVirtualPath(path) ? stripCueTrackSuffix(path) : path; + if (isContentUri(realPath)) { + return PlatformBridge.safExists(realPath); } - return File(path).exists(); + return File(realPath).exists(); } Future deleteFile(String? path) async { if (path == null || path.isEmpty) return; + // CUE virtual paths should NOT be deleted through this function — + // deleting album.cue would remove ALL tracks. Callers should handle + // CUE deletion specially (e.g. only delete when all tracks are removed). + if (isCueVirtualPath(path)) return; if (isContentUri(path)) { await PlatformBridge.safDelete(path); return; @@ -233,8 +256,10 @@ Future deleteFile(String? path) async { Future fileStat(String? path) async { if (path == null || path.isEmpty) return null; - if (isContentUri(path)) { - final stat = await PlatformBridge.safStat(path); + // For CUE virtual paths, stat the base .cue file + final realPath = isCueVirtualPath(path) ? stripCueTrackSuffix(path) : path; + if (isContentUri(realPath)) { + final stat = await PlatformBridge.safStat(realPath); final exists = stat['exists'] as bool? ?? true; if (!exists) return null; return FileAccessStat( @@ -245,18 +270,23 @@ Future fileStat(String? path) async { ); } - final stat = await FileStat.stat(path); + final stat = await FileStat.stat(realPath); if (stat.type == FileSystemEntityType.notFound) return null; return FileAccessStat(size: stat.size, modified: stat.modified); } Future openFile(String path) async { - if (isContentUri(path)) { - await PlatformBridge.openContentUri(path, mimeType: ''); + if (isCueVirtualPath(path)) { + throw Exception(cueVirtualTrackRequiresSplitMessage); + } + + final realPath = path; + if (isContentUri(realPath)) { + await PlatformBridge.openContentUri(realPath, mimeType: ''); return; } - final mimeType = audioMimeTypeForPath(path); - final result = await OpenFilex.open(path, type: mimeType); + final mimeType = audioMimeTypeForPath(realPath); + final result = await OpenFilex.open(realPath, type: mimeType); if (result.type != ResultType.done) { throw Exception(result.message); } From c2736a61fb7596bb8aafc353470c14f2d95403ca Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Mar 2026 00:58:07 +0700 Subject: [PATCH 028/145] refactor: remove built-in Spotify API provider, use Deezer as sole default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove all Spotify credential management (client ID/secret, secure storage) - Remove Spotify platform channel handlers from MainActivity - Remove exported Go functions: GetSpotifyMetadata, SearchSpotify, SearchSpotifyAll, GetSpotifyRelatedArtists, SetSpotifyAPICredentials - Simplify GetSpotifyMetadataWithDeezerFallback to SpotFetch-only path - Remove Spotify built-in fallback in ReEnrichFile search pipeline - Always return false from HasSpotifyCredentials; getCredentials always errors - Default metadataProviderPriority is now ['deezer'] only - Sanitize provider priority list to strip 'spotify' entries on load/save - Add migration v5 to clear saved Spotify credentials from existing installs - Remove Spotify source chip and credentials UI from options settings page - Remove metadataSource param from search() — always uses Deezer - spotify-web extension remains supported via the extension provider system --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 46 --- go_backend/exports.go | 188 +---------- go_backend/extension_providers.go | 24 +- go_backend/spotify.go | 32 +- lib/models/settings.dart | 4 +- lib/models/settings.g.dart | 2 +- lib/providers/extension_provider.dart | 43 ++- lib/providers/settings_provider.dart | 108 ++---- lib/providers/track_provider.dart | 45 +-- lib/screens/home_tab.dart | 34 +- lib/screens/search_screen.dart | 10 +- .../metadata_provider_priority_page.dart | 7 - .../settings/options_settings_page.dart | 310 +----------------- lib/services/platform_bridge.dart | 61 ---- 14 files changed, 127 insertions(+), 787 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 8fe38bfc..5d5650da 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -1877,38 +1877,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - "getSpotifyMetadata" -> { - val url = call.argument("url") ?: "" - val response = withContext(Dispatchers.IO) { - Gobackend.getSpotifyMetadata(url) - } - result.success(response) - } - "searchSpotify" -> { - val query = call.argument("query") ?: "" - val limit = call.argument("limit") ?: 10 - val response = withContext(Dispatchers.IO) { - Gobackend.searchSpotify(query, limit.toLong()) - } - result.success(response) - } - "searchSpotifyAll" -> { - val query = call.argument("query") ?: "" - val trackLimit = call.argument("track_limit") ?: 15 - val artistLimit = call.argument("artist_limit") ?: 3 - val response = withContext(Dispatchers.IO) { - Gobackend.searchSpotifyAll(query, trackLimit.toLong(), artistLimit.toLong()) - } - result.success(response) - } - "getSpotifyRelatedArtists" -> { - val artistId = call.argument("artist_id") ?: "" - val limit = call.argument("limit") ?: 12 - val response = withContext(Dispatchers.IO) { - Gobackend.getSpotifyRelatedArtists(artistId, limit.toLong()) - } - result.success(response) - } "checkAvailability" -> { val spotifyId = call.argument("spotify_id") ?: "" val isrc = call.argument("isrc") ?: "" @@ -2542,20 +2510,6 @@ class MainActivity: FlutterFragmentActivity() { "isDownloadServiceRunning" -> { result.success(DownloadService.isServiceRunning()) } - "setSpotifyCredentials" -> { - val clientId = call.argument("client_id") ?: "" - val clientSecret = call.argument("client_secret") ?: "" - withContext(Dispatchers.IO) { - Gobackend.setSpotifyAPICredentials(clientId, clientSecret) - } - result.success(null) - } - "hasSpotifyCredentials" -> { - val hasCredentials = withContext(Dispatchers.IO) { - Gobackend.checkSpotifyCredentials() - } - result.success(hasCredentials) - } "preWarmTrackCache" -> { val tracksJson = call.argument("tracks") ?: "[]" withContext(Dispatchers.IO) { diff --git a/go_backend/exports.go b/go_backend/exports.go index 5f83330a..00791180 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -32,126 +32,6 @@ func ParseSpotifyURL(url string) (string, error) { return string(jsonBytes), nil } -func SetSpotifyAPICredentials(clientID, clientSecret string) { - SetSpotifyCredentials(clientID, clientSecret) -} - -func CheckSpotifyCredentials() bool { - return HasSpotifyCredentials() -} - -func GetSpotifyMetadata(spotifyURL string) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - client, err := NewSpotifyMetadataClient() - if err != nil { - if shouldTrySpotFetchFallback(err) { - data, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL) - if apiErr == nil { - jsonBytes, marshalErr := json.Marshal(data) - if marshalErr != nil { - return "", marshalErr - } - return string(jsonBytes), nil - } - } - return "", err - } - data, err := client.GetFilteredData(ctx, spotifyURL, false, 0) - if err != nil { - if shouldTrySpotFetchFallback(err) { - fallbackData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL) - if apiErr == nil { - jsonBytes, marshalErr := json.Marshal(fallbackData) - if marshalErr != nil { - return "", marshalErr - } - return string(jsonBytes), nil - } - } - return "", err - } - - jsonBytes, err := json.Marshal(data) - if err != nil { - return "", err - } - - return string(jsonBytes), nil -} - -func SearchSpotify(query string, limit int) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - - client, err := NewSpotifyMetadataClient() - if err != nil { - return "", err - } - results, err := client.SearchTracks(ctx, query, limit) - if err != nil { - return "", err - } - - jsonBytes, err := json.Marshal(results) - if err != nil { - return "", err - } - - return string(jsonBytes), nil -} - -func SearchSpotifyAll(query string, trackLimit, artistLimit int) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - - client, err := NewSpotifyMetadataClient() - if err != nil { - return "", err - } - results, err := client.SearchAll(ctx, query, trackLimit, artistLimit) - if err != nil { - return "", err - } - - jsonBytes, err := json.Marshal(results) - if err != nil { - return "", err - } - - return string(jsonBytes), nil -} - -func GetSpotifyRelatedArtists(artistID string, limit int) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - - client, err := NewSpotifyMetadataClient() - if err != nil { - return "", err - } - - normalizedArtistID := strings.TrimSpace(strings.TrimPrefix(artistID, "spotify:")) - if normalizedArtistID == "" { - return "", fmt.Errorf("invalid Spotify artist ID") - } - - artists, err := client.GetRelatedArtists(ctx, normalizedArtistID, limit) - if err != nil { - return "", err - } - - resp := map[string]interface{}{ - "artists": artists, - } - jsonBytes, err := json.Marshal(resp) - if err != nil { - return "", err - } - return string(jsonBytes), nil -} - func CheckAvailability(spotifyID, isrc string) (string, error) { client := NewSongLinkClient() availability, err := client.CheckTrackAvailability(spotifyID, isrc) @@ -1439,28 +1319,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - var spotifyErr error - - client, err := NewSpotifyMetadataClient() - if err != nil { - LogWarn("Spotify", "Credentials not configured, falling back to Deezer") - spotifyErr = err - } else { - data, err := client.GetFilteredData(ctx, spotifyURL, false, 0) - if err == nil { - jsonBytes, err := json.Marshal(data) - if err != nil { - return "", err - } - return string(jsonBytes), nil - } - - spotifyErr = err - if !shouldTrySpotFetchFallback(err) { - return "", err - } - } - spotFetchData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL) if apiErr == nil { GoLog("[Fallback] Spotify metadata fetched via SpotFetch API\n") @@ -1474,9 +1332,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { parsed, parseErr := parseSpotifyURI(spotifyURL) if parseErr != nil { - if spotifyErr != nil { - return "", fmt.Errorf("spotify failed (%v), SpotFetch fallback failed (%v), and URL parsing failed: %w", spotifyErr, apiErr, parseErr) - } return "", fmt.Errorf("SpotFetch fallback failed (%v) and URL parsing failed: %w", apiErr, parseErr) } @@ -1487,15 +1342,9 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { } if parsed.Type == "artist" { - if spotifyErr != nil { - return "", fmt.Errorf("spotify metadata unavailable (%v) and SpotFetch fallback failed (%v). Artist pages require Spotify/SpotFetch API", spotifyErr, apiErr) - } - return "", fmt.Errorf("SpotFetch fallback failed (%v). Artist pages require Spotify/SpotFetch API", apiErr) + return "", fmt.Errorf("SpotFetch fallback failed (%v). Artist pages now require SpotFetch or a metadata extension such as spotify-web", apiErr) } - if spotifyErr != nil { - return "", fmt.Errorf("spotify metadata unavailable (%v), SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", spotifyErr, apiErr) - } return "", fmt.Errorf("SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", apiErr) } @@ -1826,8 +1675,8 @@ func ReEnrichFile(requestJSON string) (string, error) { GoLog("[ReEnrich] Starting re-enrichment for: %s\n", req.FilePath) - // When search_online is true, search for metadata from internet - // Priority: 1) Deezer (reliable, no credentials) 2) Extension providers (spotify-web etc) 3) Spotify built-in API (last resort, deprecated) + // When search_online is true, search for metadata from internet. + // Priority: 1) Deezer (reliable, no credentials) 2) Extension providers (spotify-web etc) if req.SearchOnline && req.TrackName != "" && req.ArtistName != "" { GoLog("[ReEnrich] Searching online metadata for: %s - %s\n", req.TrackName, req.ArtistName) searchQuery := req.TrackName + " " + req.ArtistName @@ -1901,37 +1750,6 @@ func ReEnrichFile(requestJSON string) (string, error) { } } - // 3) Try Spotify built-in API as last resort (will be deprecated) - if !found { - GoLog("[ReEnrich] Trying Spotify API (fallback)...\n") - spotifyClient, spotifyErr := NewSpotifyMetadataClient() - if spotifyErr == nil { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - results, err := spotifyClient.SearchTracks(ctx, searchQuery, 5) - cancel() - if err == nil && len(results.Tracks) > 0 { - track := results.Tracks[0] - GoLog("[ReEnrich] Spotify match: %s - %s (album: %s)\n", track.Name, track.Artists, track.AlbumName) - req.SpotifyID = track.SpotifyID - req.AlbumName = track.AlbumName - req.AlbumArtist = track.AlbumArtist - req.TrackNumber = track.TrackNumber - req.DiscNumber = track.DiscNumber - req.ReleaseDate = track.ReleaseDate - req.ISRC = track.ISRC - if track.Images != "" { - req.CoverURL = track.Images - } - req.DurationMs = int64(track.DurationMS) - found = true - } else if err != nil { - GoLog("[ReEnrich] Spotify search failed: %v\n", err) - } - } else { - GoLog("[ReEnrich] Spotify client unavailable: %v\n", spotifyErr) - } - } - // Try to get extended metadata (genre, label) from Deezer if not already set if found && req.ISRC != "" && (req.Genre == "" || req.Label == "") { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index a1a419b5..89cf02a2 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -644,8 +644,26 @@ func GetProviderPriority() []string { func SetMetadataProviderPriority(providerIDs []string) { metadataProviderPriorityMu.Lock() defer metadataProviderPriorityMu.Unlock() - metadataProviderPriority = providerIDs - GoLog("[Extension] Metadata provider priority set: %v\n", providerIDs) + + sanitized := make([]string, 0, len(providerIDs)+1) + seen := map[string]struct{}{} + for _, providerID := range providerIDs { + providerID = strings.TrimSpace(providerID) + if providerID == "" || providerID == "spotify" { + continue + } + if _, exists := seen[providerID]; exists { + continue + } + seen[providerID] = struct{}{} + sanitized = append(sanitized, providerID) + } + if _, exists := seen["deezer"]; !exists { + sanitized = append([]string{"deezer"}, sanitized...) + } + + metadataProviderPriority = sanitized + GoLog("[Extension] Metadata provider priority set: %v\n", sanitized) } func GetMetadataProviderPriority() []string { @@ -653,7 +671,7 @@ func GetMetadataProviderPriority() []string { defer metadataProviderPriorityMu.RUnlock() if len(metadataProviderPriority) == 0 { - return []string{"deezer", "spotify"} + return []string{"deezer"} } result := make([]string, len(metadataProviderPriority)) diff --git a/go_backend/spotify.go b/go_backend/spotify.go index 9728a72f..570c4e24 100644 --- a/go_backend/spotify.go +++ b/go_backend/spotify.go @@ -9,7 +9,6 @@ import ( "math/rand" "net/http" "net/url" - "os" "strings" "sync" "time" @@ -64,45 +63,20 @@ var ( credentialsMu sync.RWMutex ) -var ErrNoSpotifyCredentials = errors.New("Spotify credentials not configured. Please set your own Client ID and Secret in Settings, or use Deezer as metadata source (free, no credentials required)") +var ErrNoSpotifyCredentials = errors.New("built-in Spotify API metadata provider has been removed; use Deezer or the spotify-web extension instead") func SetSpotifyCredentials(clientID, clientSecret string) { credentialsMu.Lock() defer credentialsMu.Unlock() - customClientID = clientID - customClientSecret = clientSecret + customClientID = "" + customClientSecret = "" } func HasSpotifyCredentials() bool { - credentialsMu.RLock() - defer credentialsMu.RUnlock() - - if customClientID != "" && customClientSecret != "" { - return true - } - - if os.Getenv("SPOTIFY_CLIENT_ID") != "" && os.Getenv("SPOTIFY_CLIENT_SECRET") != "" { - return true - } - return false } func getCredentials() (string, string, error) { - credentialsMu.RLock() - defer credentialsMu.RUnlock() - - if customClientID != "" && customClientSecret != "" { - return customClientID, customClientSecret, nil - } - - clientID := os.Getenv("SPOTIFY_CLIENT_ID") - clientSecret := os.Getenv("SPOTIFY_CLIENT_SECRET") - - if clientID != "" && clientSecret != "" { - return clientID, clientSecret, nil - } - return "", "", ErrNoSpotifyCredentials } diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 8501a4e3..67e3e9ac 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -104,7 +104,7 @@ class AppSettings { this.askQualityBeforeDownload = true, this.spotifyClientId = '', this.spotifyClientSecret = '', - this.useCustomSpotifyCredentials = true, + this.useCustomSpotifyCredentials = false, this.metadataSource = 'deezer', this.enableLogging = false, this.useExtensionProviders = true, @@ -149,7 +149,7 @@ class AppSettings { String? downloadDirectory, String? storageMode, String? downloadTreeUri, - bool? autoFallback, + bool? autoFallback, bool? embedMetadata, bool? embedLyrics, bool? maxQualityCover, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index cc47ca8e..c3eecb50 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -33,7 +33,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( spotifyClientId: json['spotifyClientId'] as String? ?? '', spotifyClientSecret: json['spotifyClientSecret'] as String? ?? '', useCustomSpotifyCredentials: - json['useCustomSpotifyCredentials'] as bool? ?? true, + json['useCustomSpotifyCredentials'] as bool? ?? false, metadataSource: json['metadataSource'] as String? ?? 'deezer', enableLogging: json['enableLogging'] as bool? ?? false, useExtensionProviders: json['useExtensionProviders'] as bool? ?? true, diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index f53aca0c..58d14ee5 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -823,11 +823,19 @@ class ExtensionNotifier extends Notifier { List priority; if (savedJson != null) { final saved = jsonDecode(savedJson) as List; - priority = saved.map((e) => e as String).toList(); + priority = _sanitizeMetadataProviderPriority( + saved.map((e) => e as String).toList(), + ); _log.d('Loaded metadata provider priority from prefs: $priority'); + await prefs.setString( + _metadataProviderPriorityKey, + jsonEncode(priority), + ); await PlatformBridge.setMetadataProviderPriority(priority); } else { - priority = await PlatformBridge.getMetadataProviderPriority(); + priority = _sanitizeMetadataProviderPriority( + await PlatformBridge.getMetadataProviderPriority(), + ); _log.d('Using default metadata provider priority: $priority'); } @@ -840,11 +848,15 @@ class ExtensionNotifier extends Notifier { Future setMetadataProviderPriority(List priority) async { try { final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_metadataProviderPriorityKey, jsonEncode(priority)); + final sanitized = _sanitizeMetadataProviderPriority(priority); + await prefs.setString( + _metadataProviderPriorityKey, + jsonEncode(sanitized), + ); - await PlatformBridge.setMetadataProviderPriority(priority); - state = state.copyWith(metadataProviderPriority: priority); - _log.d('Saved metadata provider priority: $priority'); + await PlatformBridge.setMetadataProviderPriority(sanitized); + state = state.copyWith(metadataProviderPriority: sanitized); + _log.d('Saved metadata provider priority: $sanitized'); } catch (e) { _log.e('Failed to set metadata provider priority: $e'); state = state.copyWith(error: e.toString()); @@ -880,7 +892,7 @@ class ExtensionNotifier extends Notifier { } List getAllMetadataProviders() { - final providers = ['deezer', 'spotify']; + final providers = ['deezer']; for (final ext in state.extensions) { if (ext.enabled && ext.hasMetadataProvider) { providers.add(ext.id); @@ -889,6 +901,23 @@ class ExtensionNotifier extends Notifier { return providers; } + List _sanitizeMetadataProviderPriority(List input) { + final allowed = getAllMetadataProviders().toSet(); + final result = []; + + for (final provider in input) { + if (allowed.contains(provider) && !result.contains(provider)) { + result.add(provider); + } + } + + if (!result.contains('deezer')) { + result.insert(0, 'deezer'); + } + + return result; + } + List get searchProviders { return state.extensions .where((ext) => ext.enabled && ext.hasCustomSearch) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index a7858319..ed44fea4 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -9,7 +9,7 @@ import 'package:spotiflac_android/utils/logger.dart'; const _settingsKey = 'app_settings'; const _migrationVersionKey = 'settings_migration_version'; -const _currentMigrationVersion = 4; +const _currentMigrationVersion = 5; const _spotifyClientSecretKey = 'spotify_client_secret'; final _log = AppLogger('SettingsProvider'); @@ -41,9 +41,7 @@ class SettingsNotifier extends Notifier { await _normalizeSongLinkRegionIfNeeded(); } - await _loadSpotifyClientSecret(prefs); - - _applySpotifyCredentials(); + await _retireBuiltInSpotifyProvider(); LogBuffer.loggingEnabled = state.enableLogging; @@ -105,6 +103,17 @@ class SettingsNotifier extends Notifier { } state = state.copyWith(lyricsProviders: updatedProviders); } + if (state.metadataSource != 'deezer' || + state.spotifyClientId.isNotEmpty || + state.spotifyClientSecret.isNotEmpty || + state.useCustomSpotifyCredentials) { + state = state.copyWith( + metadataSource: 'deezer', + spotifyClientId: '', + spotifyClientSecret: '', + useCustomSpotifyCredentials: false, + ); + } state = state.copyWith(lastSeenVersion: AppInfo.version); await prefs.setInt(_migrationVersionKey, _currentMigrationVersion); await _saveSettings(); @@ -193,49 +202,28 @@ class SettingsNotifier extends Notifier { await _saveSettings(); } - Future _loadSpotifyClientSecret(SharedPreferences prefs) async { + Future _retireBuiltInSpotifyProvider() async { final storedSecret = await _secureStorage.read( key: _spotifyClientSecretKey, ); - final prefsSecret = state.spotifyClientSecret; - - if ((storedSecret == null || storedSecret.isEmpty) && - prefsSecret.isNotEmpty) { - await _secureStorage.write( - key: _spotifyClientSecretKey, - value: prefsSecret, - ); - } - - final effectiveSecret = (storedSecret != null && storedSecret.isNotEmpty) - ? storedSecret - : (prefsSecret.isNotEmpty ? prefsSecret : ''); - - if (effectiveSecret != state.spotifyClientSecret) { - state = state.copyWith(spotifyClientSecret: effectiveSecret); - } - - if (prefsSecret.isNotEmpty) { - await _saveSettings(); - } - } - - Future _storeSpotifyClientSecret(String secret) async { - if (secret.isEmpty) { + if (storedSecret != null && storedSecret.isNotEmpty) { await _secureStorage.delete(key: _spotifyClientSecretKey); - } else { - await _secureStorage.write(key: _spotifyClientSecretKey, value: secret); } - } - Future _applySpotifyCredentials() async { - if (state.spotifyClientId.isNotEmpty && - state.spotifyClientSecret.isNotEmpty) { - await PlatformBridge.setSpotifyCredentials( - state.spotifyClientId, - state.spotifyClientSecret, - ); + if (state.metadataSource == 'deezer' && + state.spotifyClientId.isEmpty && + state.spotifyClientSecret.isEmpty && + !state.useCustomSpotifyCredentials) { + return; } + + state = state.copyWith( + metadataSource: 'deezer', + spotifyClientId: '', + spotifyClientSecret: '', + useCustomSpotifyCredentials: false, + ); + await _saveSettings(); } void setDefaultService(String service) { @@ -396,45 +384,9 @@ class SettingsNotifier extends Notifier { _saveSettings(); } - void setSpotifyClientId(String clientId) { - state = state.copyWith(spotifyClientId: clientId); - _saveSettings(); - } - - Future setSpotifyClientSecret(String clientSecret) async { - state = state.copyWith(spotifyClientSecret: clientSecret); - await _storeSpotifyClientSecret(clientSecret); - _saveSettings(); - } - - Future setSpotifyCredentials( - String clientId, - String clientSecret, - ) async { - state = state.copyWith( - spotifyClientId: clientId, - spotifyClientSecret: clientSecret, - ); - await _storeSpotifyClientSecret(clientSecret); - _saveSettings(); - _applySpotifyCredentials(); - } - - Future clearSpotifyCredentials() async { - state = state.copyWith(spotifyClientId: '', spotifyClientSecret: ''); - await _storeSpotifyClientSecret(''); - _saveSettings(); - _applySpotifyCredentials(); - } - - void setUseCustomSpotifyCredentials(bool enabled) { - state = state.copyWith(useCustomSpotifyCredentials: enabled); - _saveSettings(); - _applySpotifyCredentials(); - } - void setMetadataSource(String source) { - state = state.copyWith(metadataSource: source); + final normalized = source == 'deezer' ? 'deezer' : 'deezer'; + state = state.copyWith(metadataSource: normalized); _saveSettings(); } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 8ba3f33b..5adae7d0 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -458,7 +458,8 @@ class TrackNotifier extends Notifier { } // If URL doesn't match any known service, it's unrecognized - final isSpotifyUrl = url.contains('open.spotify.com') || + final isSpotifyUrl = + url.contains('open.spotify.com') || url.contains('spotify.link') || url.startsWith('spotify:'); if (!isSpotifyUrl) { @@ -546,11 +547,7 @@ class TrackNotifier extends Notifier { } } - Future search( - String query, { - String? metadataSource, - String? filterOverride, - }) async { + Future search(String query, {String? filterOverride}) async { final requestId = ++_currentRequestId; // Preserve selected filter during loading @@ -576,7 +573,7 @@ class TrackNotifier extends Notifier { searchProvider != null && searchProvider.isNotEmpty; - final source = metadataSource ?? 'deezer'; + const source = 'deezer'; _log.i( 'Search started: source=$source, query="$query", useExtensions=$useExtensions, filter=$currentFilter', @@ -602,32 +599,20 @@ class TrackNotifier extends Notifier { } } } catch (e) { - _log.w('Extension search failed, falling back to built-in: $e'); + _log.w('Extension search failed, falling back to Deezer: $e'); } } - if (source == 'deezer') { - _log.d('Calling Deezer search API...'); - results = await PlatformBridge.searchDeezerAll( - query, - trackLimit: 20, - artistLimit: 2, - filter: currentFilter, - ); - _log.i( - 'Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums', - ); - } else { - _log.d('Calling Spotify search API...'); - results = await PlatformBridge.searchSpotifyAll( - query, - trackLimit: 20, - artistLimit: 2, - ); - _log.i( - 'Spotify returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists', - ); - } + _log.d('Calling Deezer search API...'); + results = await PlatformBridge.searchDeezerAll( + query, + trackLimit: 20, + artistLimit: 2, + filter: currentFilter, + ); + _log.i( + 'Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums', + ); if (!_isRequestValid(requestId)) { _log.w('Search request cancelled (requestId=$requestId)'); diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 65e8fcec..5663caa3 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -549,11 +549,7 @@ class _HomeTabState extends ConsumerState } await ref .read(trackProvider.notifier) - .search( - query, - metadataSource: settings.metadataSource, - filterOverride: selectedFilter, - ); + .search(query, filterOverride: selectedFilter); } ref.read(settingsProvider.notifier).setHasSearchedBefore(); } @@ -587,26 +583,24 @@ class _HomeTabState extends ConsumerState if (trackState.error != null && mounted) { final l10n = context.l10n; final errorMsg = trackState.error!; - final isRateLimit = errorMsg.contains('429') || + final isRateLimit = + errorMsg.contains('429') || errorMsg.toLowerCase().contains('rate limit') || errorMsg.toLowerCase().contains('too many requests'); final displayMessage = errorMsg == 'url_not_recognized' ? l10n.errorUrlNotRecognizedMessage : isRateLimit - ? l10n.errorRateLimitedMessage - : l10n.errorUrlFetchFailed; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(displayMessage)), - ); + ? l10n.errorRateLimitedMessage + : l10n.errorUrlFetchFailed; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(displayMessage))); ref.read(trackProvider.notifier).clear(); } else { _navigateToDetailIfNeeded(); } } else { - final settings = ref.read(settingsProvider); - await ref - .read(trackProvider.notifier) - .search(url, metadataSource: settings.metadataSource); + await ref.read(trackProvider.notifier).search(url); } ref.read(settingsProvider.notifier).setHasSearchedBefore(); } @@ -2315,10 +2309,7 @@ class _HomeTabState extends ConsumerState const SizedBox(height: 4), Text( l10n.errorUrlNotRecognizedMessage, - style: TextStyle( - color: colorScheme.error, - fontSize: 12, - ), + style: TextStyle(color: colorScheme.error, fontSize: 12), ), ], ), @@ -2971,9 +2962,6 @@ class _SearchProviderDropdown extends ConsumerWidget { final currentProvider = ref.watch( settingsProvider.select((s) => s.searchProvider), ); - final metadataSource = ref.watch( - settingsProvider.select((s) => s.metadataSource), - ); final extensions = ref.watch(extensionProvider.select((s) => s.extensions)); final colorScheme = Theme.of(context).colorScheme; @@ -3051,7 +3039,7 @@ class _SearchProviderDropdown extends ConsumerWidget { const SizedBox(width: 12), Expanded( child: Text( - metadataSource == 'spotify' ? 'Spotify' : 'Deezer', + 'Deezer', style: TextStyle( fontWeight: currentProvider == null || currentProvider.isEmpty diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 4523fbee..09b2f656 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -27,10 +27,7 @@ class _SearchScreenState extends ConsumerState { _searchController = TextEditingController(text: widget.query); if (widget.query.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { - final settings = ref.read(settingsProvider); - ref - .read(trackProvider.notifier) - .search(widget.query, metadataSource: settings.metadataSource); + ref.read(trackProvider.notifier).search(widget.query); }); } } @@ -44,10 +41,7 @@ class _SearchScreenState extends ConsumerState { void _search() { final query = _searchController.text.trim(); if (query.isNotEmpty) { - final settings = ref.read(settingsProvider); - ref - .read(trackProvider.notifier) - .search(query, metadataSource: settings.metadataSource); + ref.read(trackProvider.notifier).search(query); } } diff --git a/lib/screens/settings/metadata_provider_priority_page.dart b/lib/screens/settings/metadata_provider_priority_page.dart index 7631e27d..3c614784 100644 --- a/lib/screens/settings/metadata_provider_priority_page.dart +++ b/lib/screens/settings/metadata_provider_priority_page.dart @@ -228,13 +228,6 @@ class _MetadataProviderItem extends StatelessWidget { description: context.l10n.metadataNoRateLimits, isBuiltIn: true, ); - case 'spotify': - return _MetadataProviderInfo( - name: 'Spotify', - icon: Icons.music_note, - description: context.l10n.metadataMayRateLimit, - isBuiltIn: true, - ); default: return _MetadataProviderInfo( name: provider, diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index d57558fa..4a7618d0 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; -import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; @@ -73,68 +72,10 @@ class OptionsSettingsPage extends ConsumerWidget { child: SettingsGroup( children: [ _MetadataSourceSelector( - currentSource: settings.metadataSource, onChanged: (v) => ref .read(settingsProvider.notifier) .setMetadataSource(v), ), - if (settings.metadataSource == 'spotify') ...[ - if (settings.spotifyClientId.isEmpty) - Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Card( - color: Theme.of(context).colorScheme.errorContainer, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Icon( - Icons.warning_amber_rounded, - color: Theme.of( - context, - ).colorScheme.onErrorContainer, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - context.l10n.optionsSpotifyWarning, - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onErrorContainer, - fontSize: 12, - ), - ), - ), - ], - ), - ), - ), - ), - SettingsItem( - icon: Icons.key, - title: context.l10n.optionsSpotifyCredentials, - subtitle: settings.spotifyClientId.isNotEmpty - ? context.l10n.optionsSpotifyCredentialsConfigured( - settings.spotifyClientId.length > 8 - ? settings.spotifyClientId.substring(0, 8) - : settings.spotifyClientId, - ) - : context.l10n.optionsSpotifyCredentialsRequired, - onTap: () => - _showSpotifyCredentialsDialog(context, ref, settings), - trailing: Icon( - settings.spotifyClientId.isNotEmpty - ? Icons.check_circle - : Icons.error_outline, - color: settings.spotifyClientId.isNotEmpty - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.error, - size: 20, - ), - showDivider: false, - ), - ], ], ), ), @@ -372,214 +313,6 @@ class OptionsSettingsPage extends ConsumerWidget { } } } - - void _showSpotifyCredentialsDialog( - BuildContext context, - WidgetRef ref, - AppSettings settings, - ) { - final clientIdController = TextEditingController( - text: settings.spotifyClientId, - ); - final clientSecretController = TextEditingController( - text: settings.spotifyClientSecret, - ); - final colorScheme = Theme.of(context).colorScheme; - - showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - backgroundColor: colorScheme.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - builder: (context) => Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: SingleChildScrollView( - child: SafeArea( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Container( - width: 32, - height: 4, - margin: const EdgeInsets.only(bottom: 24), - decoration: BoxDecoration( - color: colorScheme.outlineVariant, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - Text( - context.l10n.credentialsTitle, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - context.l10n.credentialsDescription, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - - TextField( - controller: clientIdController, - decoration: InputDecoration( - labelText: context.l10n.credentialsClientId, - hintText: context.l10n.credentialsClientIdHint, - filled: true, - fillColor: colorScheme.surfaceContainerHighest.withValues( - alpha: 0.3, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.primary, - width: 2, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, - ), - prefixIcon: const Icon(Icons.person_outline), - ), - ), - const SizedBox(height: 16), - - TextField( - controller: clientSecretController, - obscureText: true, - decoration: InputDecoration( - labelText: context.l10n.credentialsClientSecret, - hintText: context.l10n.credentialsClientSecretHint, - filled: true, - fillColor: colorScheme.surfaceContainerHighest.withValues( - alpha: 0.3, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: colorScheme.primary, - width: 2, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, - ), - prefixIcon: const Icon(Icons.lock_outline), - ), - ), - - const SizedBox(height: 32), - - FilledButton( - onPressed: () { - final clientId = clientIdController.text.trim(); - final clientSecret = clientSecretController.text.trim(); - - if (clientId.isNotEmpty && clientSecret.isNotEmpty) { - ref - .read(settingsProvider.notifier) - .setSpotifyCredentials(clientId, clientSecret); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCredentialsSaved, - ), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.snackbarFillAllFields), - ), - ); - } - }, - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: Text( - context.l10n.actionSaveCredentials, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - - if (settings.spotifyClientId.isNotEmpty) ...[ - const SizedBox(height: 12), - TextButton( - onPressed: () { - ref - .read(settingsProvider.notifier) - .clearSpotifyCredentials(); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.snackbarCredentialsCleared, - ), - ), - ); - }, - style: TextButton.styleFrom( - foregroundColor: colorScheme.error, - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: Text(context.l10n.actionRemoveCredentials), - ), - ], - - const SizedBox(height: 16), - ], - ), - ), - ), - ), - ), - ); - } } class _ConcurrentDownloadsItem extends StatelessWidget { @@ -875,12 +608,8 @@ class _ChannelChip extends StatelessWidget { } class _MetadataSourceSelector extends ConsumerWidget { - final String currentSource; final ValueChanged onChanged; - const _MetadataSourceSelector({ - required this.currentSource, - required this.onChanged, - }); + const _MetadataSourceSelector({required this.onChanged}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -930,7 +659,7 @@ class _MetadataSourceSelector extends ConsumerWidget { _SourceChip( icon: Icons.graphic_eq, label: 'Deezer', - isSelected: currentSource == 'deezer' && !hasExtensionSearch, + isSelected: !hasExtensionSearch, onTap: () { if (hasExtensionSearch) { ref.read(settingsProvider.notifier).setSearchProvider(null); @@ -938,18 +667,6 @@ class _MetadataSourceSelector extends ConsumerWidget { onChanged('deezer'); }, ), - const SizedBox(width: 12), - _SourceChip( - icon: Icons.music_note, - label: 'Spotify', - isSelected: currentSource == 'spotify' && !hasExtensionSearch, - onTap: () { - if (hasExtensionSearch) { - ref.read(settingsProvider.notifier).setSearchProvider(null); - } - onChanged('spotify'); - }, - ), ], ), if (hasExtensionSearch) ...[ @@ -964,7 +681,7 @@ class _MetadataSourceSelector extends ConsumerWidget { const SizedBox(width: 8), Expanded( child: Text( - context.l10n.optionsSwitchBack, + 'Tap Deezer to switch back from extension', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -973,27 +690,6 @@ class _MetadataSourceSelector extends ConsumerWidget { ], ), ], - if (currentSource == 'spotify' && !hasExtensionSearch) ...[ - const SizedBox(height: 12), - Row( - children: [ - Icon( - Icons.warning_amber_rounded, - size: 16, - color: colorScheme.error, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - context.l10n.optionsSpotifyDeprecationWarning, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: colorScheme.error), - ), - ), - ], - ), - ], ], ), ); diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index bf20d621..c41207b9 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -20,51 +20,6 @@ class PlatformBridge { return jsonDecode(result as String) as Map; } - static Future> getSpotifyMetadata(String url) async { - _log.d('getSpotifyMetadata: $url'); - final result = await _channel.invokeMethod('getSpotifyMetadata', { - 'url': url, - }); - return jsonDecode(result as String) as Map; - } - - static Future> searchSpotify( - String query, { - int limit = 10, - }) async { - _log.d('searchSpotify: "$query" (limit: $limit)'); - final result = await _channel.invokeMethod('searchSpotify', { - 'query': query, - 'limit': limit, - }); - return jsonDecode(result as String) as Map; - } - - static Future> searchSpotifyAll( - String query, { - int trackLimit = 15, - int artistLimit = 3, - }) async { - _log.d('searchSpotifyAll: "$query"'); - final result = await _channel.invokeMethod('searchSpotifyAll', { - 'query': query, - 'track_limit': trackLimit, - 'artist_limit': artistLimit, - }); - return jsonDecode(result as String) as Map; - } - - static Future> getSpotifyRelatedArtists( - String artistId, { - int limit = 12, - }) async { - final result = await _channel.invokeMethod('getSpotifyRelatedArtists', { - 'artist_id': artistId, - 'limit': limit, - }); - return jsonDecode(result as String) as Map; - } - static Future> checkAvailability( String spotifyId, String isrc, @@ -517,21 +472,6 @@ class PlatformBridge { return result as bool; } - static Future setSpotifyCredentials( - String clientId, - String clientSecret, - ) async { - await _channel.invokeMethod('setSpotifyCredentials', { - 'client_id': clientId, - 'client_secret': clientSecret, - }); - } - - static Future hasSpotifyCredentials() async { - final result = await _channel.invokeMethod('hasSpotifyCredentials'); - return result as bool; - } - static Future preWarmTrackCache( List> tracks, ) async { @@ -1313,5 +1253,4 @@ class PlatformBridge { }); return jsonDecode(result as String) as Map; } - } From f2ae1398dbafea8c078684e2f50c370748fea9bc Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Mar 2026 01:06:26 +0700 Subject: [PATCH 029/145] feat: add 'By Playlist' folder organization option (#111) --- lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_de.dart | 7 +++++++ lib/l10n/app_localizations_en.dart | 7 +++++++ lib/l10n/app_localizations_es.dart | 7 +++++++ lib/l10n/app_localizations_fr.dart | 7 +++++++ lib/l10n/app_localizations_hi.dart | 7 +++++++ lib/l10n/app_localizations_id.dart | 7 +++++++ lib/l10n/app_localizations_ja.dart | 7 +++++++ lib/l10n/app_localizations_ko.dart | 7 +++++++ lib/l10n/app_localizations_nl.dart | 7 +++++++ lib/l10n/app_localizations_pt.dart | 7 +++++++ lib/l10n/app_localizations_ru.dart | 7 +++++++ lib/l10n/app_localizations_tr.dart | 7 +++++++ lib/l10n/app_localizations_zh.dart | 7 +++++++ lib/l10n/arb/app_en.arb | 8 ++++++++ lib/models/download_item.dart | 4 ++++ lib/models/download_item.g.dart | 2 ++ lib/providers/download_queue_provider.dart | 19 ++++++++++++++++++- lib/screens/playlist_screen.dart | 8 ++++---- .../settings/download_settings_page.dart | 14 ++++++++++++++ 20 files changed, 153 insertions(+), 5 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d722f825..488563dc 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1456,6 +1456,18 @@ abstract class AppLocalizations { /// **'No organization'** String get folderOrganizationNone; + /// Folder option - playlist folders + /// + /// In en, this message translates to: + /// **'By Playlist'** + String get folderOrganizationByPlaylist; + + /// Subtitle for playlist folder option + /// + /// In en, this message translates to: + /// **'Separate folder for each playlist'** + String get folderOrganizationByPlaylistSubtitle; + /// Folder option - artist folders /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1bb33ef5..4cb5b75d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -785,6 +785,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get folderOrganizationNone => 'Keine Organisation'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'Nach Künstler'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index bd9d4589..646f9a58 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -772,6 +772,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ed9236ad..e44fa9d8 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -772,6 +772,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d6a3b948..ae24b7e9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -774,6 +774,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index a461bd5b..08101eac 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -772,6 +772,13 @@ class AppLocalizationsHi extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index b668481b..cab860dd 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -775,6 +775,13 @@ class AppLocalizationsId extends AppLocalizations { @override String get folderOrganizationNone => 'Tidak ada'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'Berdasarkan Artis'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 71d10e1a..6ce38247 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -767,6 +767,13 @@ class AppLocalizationsJa extends AppLocalizations { @override String get folderOrganizationNone => '構成がありません'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'アーティスト別'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 43bfbce3..56287787 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -754,6 +754,13 @@ class AppLocalizationsKo extends AppLocalizations { @override String get folderOrganizationNone => '정리하지 않음'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 2eeb00cc..5f4975d9 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -772,6 +772,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 9436f6eb..62b648ae 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -772,6 +772,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 943b75f9..66c75d45 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -786,6 +786,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get folderOrganizationNone => 'Без организации'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'По исполнителю'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 223fecd3..3a88e179 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -777,6 +777,13 @@ class AppLocalizationsTr extends AppLocalizations { @override String get folderOrganizationNone => 'Organizasyon yok'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'Sanatçıya Göre'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 57f92319..3306bb1c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -772,6 +772,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index bb3036c9..03313d7b 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1015,6 +1015,14 @@ "@folderOrganizationNone": { "description": "Folder option - flat structure" }, + "folderOrganizationByPlaylist": "By Playlist", + "@folderOrganizationByPlaylist": { + "description": "Folder option - playlist folders" + }, + "folderOrganizationByPlaylistSubtitle": "Separate folder for each playlist", + "@folderOrganizationByPlaylistSubtitle": { + "description": "Subtitle for playlist folder option" + }, "folderOrganizationByArtist": "By Artist", "@folderOrganizationByArtist": { "description": "Folder option - artist folders" diff --git a/lib/models/download_item.dart b/lib/models/download_item.dart index 4d8a650e..db55029b 100644 --- a/lib/models/download_item.dart +++ b/lib/models/download_item.dart @@ -34,6 +34,7 @@ class DownloadItem { final DownloadErrorType? errorType; final DateTime createdAt; final String? qualityOverride; // Override quality for this specific download + final String? playlistName; // Playlist context for folder organization const DownloadItem({ required this.id, @@ -48,6 +49,7 @@ class DownloadItem { this.errorType, required this.createdAt, this.qualityOverride, + this.playlistName, }); DownloadItem copyWith({ @@ -63,6 +65,7 @@ class DownloadItem { DownloadErrorType? errorType, DateTime? createdAt, String? qualityOverride, + String? playlistName, }) { return DownloadItem( id: id ?? this.id, @@ -77,6 +80,7 @@ class DownloadItem { errorType: errorType ?? this.errorType, createdAt: createdAt ?? this.createdAt, qualityOverride: qualityOverride ?? this.qualityOverride, + playlistName: playlistName ?? this.playlistName, ); } diff --git a/lib/models/download_item.g.dart b/lib/models/download_item.g.dart index 098290ce..961e6d6d 100644 --- a/lib/models/download_item.g.dart +++ b/lib/models/download_item.g.dart @@ -21,6 +21,7 @@ DownloadItem _$DownloadItemFromJson(Map json) => DownloadItem( errorType: $enumDecodeNullable(_$DownloadErrorTypeEnumMap, json['errorType']), createdAt: DateTime.parse(json['createdAt'] as String), qualityOverride: json['qualityOverride'] as String?, + playlistName: json['playlistName'] as String?, ); Map _$DownloadItemToJson(DownloadItem instance) => @@ -37,6 +38,7 @@ Map _$DownloadItemToJson(DownloadItem instance) => 'errorType': _$DownloadErrorTypeEnumMap[instance.errorType], 'createdAt': instance.createdAt.toIso8601String(), 'qualityOverride': instance.qualityOverride, + 'playlistName': instance.playlistName, }; const _$DownloadStatusEnumMap = { diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 1e69a2fa..afb2e81c 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1573,6 +1573,7 @@ class DownloadQueueNotifier extends Notifier { bool useAlbumArtistForFolders = true, bool usePrimaryArtistOnly = false, bool filterContributingArtistsInAlbumArtist = false, + String? playlistName, }) async { String baseDir = state.outputDir; final normalizedAlbumArtist = normalizeOptionalString(track.albumArtist); @@ -1647,6 +1648,11 @@ class DownloadQueueNotifier extends Notifier { String subPath = ''; switch (folderOrganization) { + case 'playlist': + if (playlistName != null && playlistName.isNotEmpty) { + subPath = _sanitizeFolderName(playlistName); + } + break; case 'artist': final artistName = _sanitizeFolderName(folderArtist); subPath = artistName; @@ -1725,6 +1731,7 @@ class DownloadQueueNotifier extends Notifier { bool useAlbumArtistForFolders = true, bool usePrimaryArtistOnly = false, bool filterContributingArtistsInAlbumArtist = false, + String? playlistName, }) async { final normalizedAlbumArtist = normalizeOptionalString(track.albumArtist); var folderArtist = useAlbumArtistForFolders @@ -1776,6 +1783,11 @@ class DownloadQueueNotifier extends Notifier { } switch (folderOrganization) { + case 'playlist': + if (playlistName != null && playlistName.isNotEmpty) { + return _sanitizeFolderName(playlistName); + } + return ''; case 'artist': return _sanitizeFolderName(folderArtist); case 'album': @@ -1900,7 +1912,7 @@ class DownloadQueueNotifier extends Notifier { ); } - String addToQueue(Track track, String service, {String? qualityOverride}) { + String addToQueue(Track track, String service, {String? qualityOverride, String? playlistName}) { final settings = ref.read(settingsProvider); updateSettings(settings); @@ -1912,6 +1924,7 @@ class DownloadQueueNotifier extends Notifier { service: service, createdAt: DateTime.now(), qualityOverride: qualityOverride, + playlistName: playlistName, ); state = state.copyWith(items: [...state.items, item]); @@ -1928,6 +1941,7 @@ class DownloadQueueNotifier extends Notifier { List tracks, String service, { String? qualityOverride, + String? playlistName, }) { final settings = ref.read(settingsProvider); updateSettings(settings); @@ -1942,6 +1956,7 @@ class DownloadQueueNotifier extends Notifier { service: service, createdAt: DateTime.now(), qualityOverride: qualityOverride, + playlistName: playlistName, ); }).toList(); @@ -3317,6 +3332,7 @@ class DownloadQueueNotifier extends Notifier { usePrimaryArtistOnly: settings.usePrimaryArtistOnly, filterContributingArtistsInAlbumArtist: settings.filterContributingArtistsInAlbumArtist, + playlistName: item.playlistName, ) : ''; String? appOutputDir; @@ -3331,6 +3347,7 @@ class DownloadQueueNotifier extends Notifier { usePrimaryArtistOnly: settings.usePrimaryArtistOnly, filterContributingArtistsInAlbumArtist: settings.filterContributingArtistsInAlbumArtist, + playlistName: item.playlistName, ); var effectiveOutputDir = initialOutputDir; var effectiveSafMode = isSafMode; diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 7ffbd18e..37bf14cf 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -416,7 +416,7 @@ class _PlaylistScreenState extends ConsumerState { onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addToQueue(track, service, qualityOverride: quality); + .addToQueue(track, service, qualityOverride: quality, playlistName: widget.playlistName); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(context.l10n.snackbarAddedToQueue(track.name)), @@ -427,7 +427,7 @@ class _PlaylistScreenState extends ConsumerState { } else { ref .read(downloadQueueProvider.notifier) - .addToQueue(track, settings.defaultService); + .addToQueue(track, settings.defaultService, playlistName: widget.playlistName); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))), ); @@ -586,7 +586,7 @@ class _PlaylistScreenState extends ConsumerState { onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, service, qualityOverride: quality); + .addMultipleToQueue(tracks, service, qualityOverride: quality, playlistName: widget.playlistName); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -599,7 +599,7 @@ class _PlaylistScreenState extends ConsumerState { } else { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, settings.defaultService); + .addMultipleToQueue(tracks, settings.defaultService, playlistName: widget.playlistName); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 1987bb2d..7a60f72c 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -1405,6 +1405,8 @@ class _DownloadSettingsPageState extends ConsumerState { String _getFolderOrganizationLabel(String value) { switch (value) { + case 'playlist': + return 'By Playlist'; case 'artist': return 'By Artist'; case 'album': @@ -1982,6 +1984,18 @@ class _DownloadSettingsPageState extends ConsumerState { Navigator.pop(context); }, ), + _FolderOption( + title: context.l10n.folderOrganizationByPlaylist, + subtitle: context.l10n.folderOrganizationByPlaylistSubtitle, + example: 'SpotiFLAC/Playlist Name/Track.flac', + isSelected: current == 'playlist', + onTap: () { + ref + .read(settingsProvider.notifier) + .setFolderOrganization('playlist'); + Navigator.pop(context); + }, + ), _FolderOption( title: context.l10n.folderOrganizationByArtist, subtitle: context.l10n.folderOrganizationByArtistSubtitle, From 8d45e023b27793eee662c71a396e4e84817f3ec8 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Mar 2026 01:10:50 +0700 Subject: [PATCH 030/145] feat: add totalTracks to Track model and refine EP/single classification (#202) --- lib/models/track.dart | 14 +++++++++++- lib/models/track.g.dart | 2 ++ lib/providers/download_queue_provider.dart | 4 ++++ lib/providers/track_provider.dart | 4 ++++ lib/screens/album_screen.dart | 2 ++ lib/screens/artist_screen.dart | 25 ++++++++++++++-------- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/lib/models/track.dart b/lib/models/track.dart index 244a7a65..e4d850c2 100644 --- a/lib/models/track.dart +++ b/lib/models/track.dart @@ -21,6 +21,7 @@ class Track { final ServiceAvailability? availability; final String? source; final String? albumType; + final int? totalTracks; final String? itemType; const Track({ @@ -41,10 +42,21 @@ class Track { this.availability, this.source, this.albumType, + this.totalTracks, this.itemType, }); - bool get isSingle => albumType == 'single' || albumType == 'ep'; + bool get isSingle { + switch (albumType?.toLowerCase()) { + case 'single': + return true; + case 'ep': + final count = totalTracks; + return count == null || count <= 1; + default: + return false; + } + } bool get isAlbumItem => itemType == 'album'; diff --git a/lib/models/track.g.dart b/lib/models/track.g.dart index f640cfe7..5e361ab6 100644 --- a/lib/models/track.g.dart +++ b/lib/models/track.g.dart @@ -28,6 +28,7 @@ Track _$TrackFromJson(Map json) => Track( ), source: json['source'] as String?, albumType: json['albumType'] as String?, + totalTracks: (json['totalTracks'] as num?)?.toInt(), itemType: json['itemType'] as String?, ); @@ -49,6 +50,7 @@ Map _$TrackToJson(Track instance) => { 'availability': instance.availability, 'source': instance.source, 'albumType': instance.albumType, + 'totalTracks': instance.totalTracks, 'itemType': instance.itemType, }; diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index afb2e81c..18348f96 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -2362,6 +2362,7 @@ class DownloadQueueNotifier extends Notifier { deezerId: baseTrack.deezerId, availability: baseTrack.availability, albumType: baseTrack.albumType, + totalTracks: baseTrack.totalTracks, source: baseTrack.source, ); } @@ -3274,6 +3275,8 @@ class DownloadQueueNotifier extends Notifier { albumType: (data['album_type'] as String?) ?? trackToDownload.albumType, + totalTracks: + data['total_tracks'] as int? ?? trackToDownload.totalTracks, source: trackToDownload.source, ); _log.d( @@ -3488,6 +3491,7 @@ class DownloadQueueNotifier extends Notifier { deezerId: deezerTrackId, availability: trackToDownload.availability, albumType: trackToDownload.albumType, + totalTracks: trackToDownload.totalTracks, source: trackToDownload.source, ); _log.d( diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 5adae7d0..1c3974c2 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -816,6 +816,7 @@ class TrackNotifier extends Notifier { discNumber: track.discNumber, releaseDate: track.releaseDate, albumType: track.albumType, + totalTracks: track.totalTracks, source: track.source, availability: ServiceAvailability( tidal: availability['tidal'] as bool? ?? false, @@ -897,6 +898,8 @@ class TrackNotifier extends Notifier { trackNumber: data['track_number'] as int?, discNumber: data['disc_number'] as int?, releaseDate: data['release_date'] as String?, + albumType: data['album_type'] as String?, + totalTracks: data['total_tracks'] as int?, ); } @@ -919,6 +922,7 @@ class TrackNotifier extends Notifier { trackNumber: data['track_number'] as int?, discNumber: data['disc_number'] as int?, releaseDate: data['release_date']?.toString(), + totalTracks: data['total_tracks'] as int?, source: source ?? data['source']?.toString() ?? diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index ba8d9d1d..f3f22790 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -224,6 +224,8 @@ class _AlbumScreenState extends ConsumerState { trackNumber: data['track_number'] as int?, discNumber: data['disc_number'] as int?, releaseDate: data['release_date'] as String?, + albumType: data['album_type'] as String?, + totalTracks: data['total_tracks'] as int?, ); } diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 8ad6bf03..06fc5063 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -294,7 +294,7 @@ class _ArtistScreenState extends ConsumerState { } } - Track _parseTrack(Map data) { + Track _parseTrack(Map data, {ArtistAlbum? album}) { int durationMs = 0; final durationValue = data['duration_ms']; if (durationValue is int) { @@ -307,18 +307,22 @@ class _ArtistScreenState extends ConsumerState { id: (data['spotify_id'] ?? data['id'] ?? '').toString(), name: (data['name'] ?? '').toString(), artistName: (data['artists'] ?? data['artist'] ?? '').toString(), - albumName: (data['album_name'] ?? data['album'] ?? '').toString(), - albumArtist: data['album_artist']?.toString(), + albumName: (data['album_name'] ?? data['album'] ?? album?.name ?? '') + .toString(), + albumArtist: data['album_artist']?.toString() ?? widget.artistName, artistId: (data['artist_id'] ?? data['artistId'])?.toString() ?? widget.artistId, - albumId: data['album_id']?.toString(), - coverUrl: (data['cover_url'] ?? data['images'])?.toString(), + albumId: data['album_id']?.toString() ?? album?.id, + coverUrl: (data['cover_url'] ?? data['images'] ?? album?.coverUrl) + ?.toString(), isrc: data['isrc']?.toString(), duration: (durationMs / 1000).round(), trackNumber: data['track_number'] as int?, discNumber: data['disc_number'] as int?, releaseDate: data['release_date']?.toString(), + albumType: data['album_type']?.toString() ?? album?.albumType, + totalTracks: data['total_tracks'] as int? ?? album?.totalTracks, source: data['provider_id']?.toString(), ); } @@ -669,7 +673,9 @@ class _ArtistScreenState extends ConsumerState { List albums, ) { final albumsOnly = albums.where((a) => a.albumType == 'album').toList(); - final singles = albums.where((a) => a.albumType == 'single').toList(); + final singles = albums + .where((a) => a.albumType == 'single' || a.albumType == 'ep') + .toList(); final totalTracks = albums.fold(0, (sum, a) => sum + a.totalTracks); final albumTracks = albumsOnly.fold( @@ -940,7 +946,7 @@ class _ArtistScreenState extends ConsumerState { if (result != null && result['tracks'] != null) { final tracksList = result['tracks'] as List; return tracksList - .map((t) => _parseTrack(t as Map)) + .map((t) => _parseTrack(t as Map, album: album)) .toList(); } } else if (album.id.startsWith('deezer:')) { @@ -961,7 +967,7 @@ class _ArtistScreenState extends ConsumerState { if (result != null && result['tracks'] != null) { final tracksList = result['tracks'] as List; return tracksList - .map((t) => _parseTrack(t as Map)) + .map((t) => _parseTrack(t as Map, album: album)) .toList(); } @@ -970,7 +976,7 @@ class _ArtistScreenState extends ConsumerState { if (metadata['tracks'] != null) { final tracksList = metadata['tracks'] as List; return tracksList - .map((t) => _parseTrack(t as Map)) + .map((t) => _parseTrack(t as Map, album: album)) .toList(); } } @@ -1004,6 +1010,7 @@ class _ArtistScreenState extends ConsumerState { discNumber: data['disk_number'] as int? ?? data['disc_number'] as int?, releaseDate: album.releaseDate, albumType: album.albumType, + totalTracks: album.totalTracks, ); } From 5be5c869daa5e54454d1f22e1afc1e26212c91ab Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Mar 2026 01:23:20 +0700 Subject: [PATCH 031/145] chore: add 'a fan' as donor for this month --- lib/screens/settings/donate_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index 175b0793..f822f5d3 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -164,7 +164,7 @@ class _RecentDonorsCard extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - const donorNames = []; + const donorNames = ['a fan']; // Match SettingsGroup color logic final cardColor = isDark From 93a7042a8416245957734c5c65fd024e7af34dc4 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Mar 2026 15:03:14 +0700 Subject: [PATCH 032/145] chore: add design/ directory to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e2e2649f..3c57112b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ Thumbs.db # Kiro specs (development only) .kiro/ +# Design assets (banners, mockups) +design/ + # Reference folder (development only) referensi/ From d82d255bae7c566c366a7b252c178077cc11b92d Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Mar 2026 15:56:42 +0700 Subject: [PATCH 033/145] feat: add dark/light theme-aware README banner and reorganize badges --- README.md | 26 +++++++++++--------------- assets/images/banner-readme-dark.png | Bin 0 -> 14813 bytes assets/images/banner-readme-light.png | Bin 0 -> 14871 bytes image.png | Bin 72121 -> 0 bytes 4 files changed, 11 insertions(+), 15 deletions(-) create mode 100644 assets/images/banner-readme-dark.png create mode 100644 assets/images/banner-readme-light.png delete mode 100644 image.png diff --git a/README.md b/README.md index ae6db959..f23bf87c 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,10 @@ -[![GitHub All Releases](https://img.shields.io/github/downloads/zarzet/SpotiFLAC-Mobile/total?style=for-the-badge&refresh=1)](https://github.com/zarzet/SpotiFLAC-Mobile/releases) -[![VirusTotal](https://img.shields.io/badge/VirusTotal-Safe-brightgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/0a2bd2a033551983fc9fcd83f82fd912c83914fd1094cd8d1c7c6a68eb23233f) -[![Crowdin](https://img.shields.io/badge/HELP%20TRANSLATE%20ON-CROWDIN-%2321252b?style=for-the-badge&logo=crowdin)](https://crowdin.com/project/spotiflac-mobile) -