From ac8267b431f599137d756bb2b5d372fc7721476a Mon Sep 17 00:00:00 2001 From: sai80082 Date: Mon, 29 Dec 2025 21:31:41 +0530 Subject: [PATCH] feat: Importing shared assets - Implemented `upload.ts` for handling the upload of shared assets, including validation and error handling. - Created `video.ts` for proxying video playback requests from shared assets. - Developed `import-shared.tsx` page for importing shared albums, including UI for asset selection and album creation options. - Updated album asset schema to use more descriptive field names (`albumId` and `assetId`). --- bun.lockb | Bin 490115 -> 501396 bytes package.json | 1 + src/config/constants/sidebarNavs.tsx | 7 +- .../api/albums/potential-albums-assets.ts | 4 +- src/pages/api/import-shared.ts | 222 +++++ src/pages/api/import-shared/helpers.ts | 63 ++ src/pages/api/import-shared/thumbnail.ts | 87 ++ src/pages/api/import-shared/upload-all.ts | 518 +++++++++++ src/pages/api/import-shared/upload.ts | 174 ++++ src/pages/api/import-shared/video.ts | 95 ++ src/pages/assets/import-shared.tsx | 814 ++++++++++++++++++ src/schema/albumAssetsAssets.schema.ts | 4 +- 12 files changed, 1984 insertions(+), 5 deletions(-) create mode 100644 src/pages/api/import-shared.ts create mode 100644 src/pages/api/import-shared/helpers.ts create mode 100644 src/pages/api/import-shared/thumbnail.ts create mode 100644 src/pages/api/import-shared/upload-all.ts create mode 100644 src/pages/api/import-shared/upload.ts create mode 100644 src/pages/api/import-shared/video.ts create mode 100644 src/pages/assets/import-shared.tsx diff --git a/bun.lockb b/bun.lockb index f6133adbc69ad5e7cc7fcab1dd5887b36eb046c2..cdde689296782592bbcde7988c8ac0f1642642f3 100755 GIT binary patch delta 97609 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%0AaYO&S;jA0`Eji;i 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 delta 95002 zcmeF4d3+RA+P15+kS@{<-7FGRHW3XxMU93N5IEzB?+hEewSWW#6%{Q&BMzM*v+v@z@_wXV{7 zH#8Ewz>_`KlW!O^=M~7j#|%|`=Dc)ie+|1S{N96J3e8q-#?>en=XWm)wW$hZ-DDVU zIQ)XsOz8JeIFwyGLZ#=;h$$?|^Ax;1!uPVhbI6tvzHjZVLqbOR8k((3JB&7rmT+#2 z%vk#q2nSmebF*Pw4!r{QCD6TN4C5+j+E~MA3*7*_9W-v7VO#-i0GqX43gvlopqD|@ zpi$7DQa@O42H{I+Luj@)+Y>V_!#D(+y|I3Z@=Gr$$;q7ME$lMcH>zRJ(yKF6 zvvWgLY42(~6*f!r6wLPK6ryP!!{`9}-_s1EHT1aF94K3{2g+tQo1wz+I%3P}I9jRV zL$Xx0o`E03z6y%`lgbw23*$Z+%4QdPm1Dj)J3oE8Va%AFiz*pL%uJPj1(coa&B^lQ zdkYJEiIHtf-=3x5^x5f!g`R1~O**rcvsLY87Q~dKXU{g$^JnCx=NEYH1m_TbHCyR( zP$%pmIjYv)89BN6XfX>#m+t|eldJsFGjsEiRBEKpoXbiY!}HXbO@Ojh zv8V{6UzG2SnOR^Mc||!TnYq(EeKo%V%1*xo$_h2qdIa->y|^FBF#evY8h92;{+-rq z(KRlVUpCt_7Zt%!1E51Atx-^BFwNu1i}4m1C-jL&ppxfi7Up|%Jf4E0LN8i6-J3Hl zrYO(Yf%Y)W&uYz!nVp-NUWiO)d$YZT#Oy!kPV_Ey-eSHbM`0t^|$gVWl= z?gizj?*e6@tZ{c?z=npZo_w)Xjo+uB999dVoK2}v=JSt*hS459SN1l(a9}^FK-nTF z=k;VL>)RLF2HG0h4jKvNfY*zcF`-Na{L#fq{{>|Mh56U@ME#AEusO7TfFA?e@GjMW zlJuhav(ai@f2chFt9*)GfQKp=RAN)Fhsd1nFVhx$71=-Fx% zf?oo=Fg>p{J!7`VXpREtV23i4-=RWSx5_+oJvoJF>ie)ci(iAfpwB?r$B#i-u@{BP z@UsZjWuAh}^x5gtJR8B8-zsP`==?47ouIsACOry!%qJA0ZD^Q0=U z1(Xf2Y5ndAWgphM3(Br8eFk3`nhLFVXwB0)QERf+>$JAj>V&f5zdWw=8?A@5z5!)d zZPEIO)}=~I%MPIha>0CB)o{xuWuJqxhNJDOMMq(`h5a&=U67Aih(T`flHkowH}W>C zF|=4mBriWVI}d~Bk!MwYvu1mWFh()$axn1<3a8~3dUEFaIyY%p`qgtPOKVGV;a23r zEy#si3%#W%dms5Q=$~&>^XYF;j)rHo_8X&`vKlsiO) zjsLXgX%nh4imFuOITAKI^GnzqX74~-LeGY%ihaLB@prYY_DszJn2o;`mBap z0k9fu1x__E(7p>BWcApEJ*9fElTl`Ml@%Ckvv}bqapHL2oWbZ)IMQT9r(igYCX9ZHb>4RC7Pu(2xO1e|vofPgSdy zLa#u6HSp)W%KA*T`ZSbLn0`$8jf0}GrDb0sfve>hhHv=KBfCf_ryDAQAe zbgr78Xus_A{8`ww8ZC~iMa&6hSX?JmxJE;{x%On|&h*aqW;B4!^q=78f`&?e>;hl> zyDI3XFH`}#UyS~U3@es7+gs!{uxNd$Rvft*?tyJ7H^tC2aOtNvbKfE#+E1TUkv)hC zv;W@E6`%XHs&H0r?yQ(nENbhJz?K%~r{f;4z*Cr&pIbB|tJ_cCsIHh*tiYqHCZhQy8 z>9GXL0>&n)md}A51v@`ex*68pVb<>|Jtr?)CRp3Q@(`PBc`mdi&P#)G9cUG1lleqI zX&;3my|m1_S6vXU5?%ylsH_EJ05V{O=4E*^vrv?`0HXmK3(gi7Mc8E5-Dk7O>+c~b zug9&S>|;bR6Zc^=-h`hE_KQ$PJl|7@J23P=*3TH;URsf8g^U@hjD{+MG$Cvm9jZdNX`6kVmtIgX0i3hz?~zJ9IVjDWGoy7Qo6!*Emlfg*`{HA#a^Q*c zv0~kX6Is)>c-+DQ3UV`N6`CKIy+*k4(MZA zCu-$xaejLKG+v^3-maFay2Fsp_V!jK^f?somEFX1LgTmpbXslE@}~a05(^tJE0g#rDeWYu|&=$(}%|^!T z@)6KhQ1q(N4*LFnHARYwymE{l>!w$&oN3-nFUELx)q{maIXRx$hB4Hp?k48%6X|T(f&^tp^j7JaP!65x+&iXc zX1#X3O8*6ZZ26)-Dq^+0;Kv~}_y*O&PoV6vUvUEU9oU?0JD@Chi%!1*$_jjq3^>Na zpsdJ#C~vPTwLYcOmqOV^g-}keDNsgqB(#(@#h8*qs4oBuvbI4TVY9~l1}MG;%1L%P zIQzN@lwEWZ6{g=|C@Z`d%1Ay3W&V#t>0be*{~{>;XI*bAl{KEI9UCXBmJET-mi2(L zrfs#lpe*1Egp$k5+fcUXGjs+0FM)FGO!H(E&A>RBg1)BTH2qj{57HT-h+)caYtK^E zqWh7+!F4B;q3j4{3mzG+3LJ!taP=$;(`r3F>^wsGSeC$TYTLZRlx_K4DC{_=u&KV z<NZO&Sz|T0JU0wRJF5np`TOOXOGMWzMARG^6B!)uSmtE3S2I<8fT-)Ln*^}x^St8~qNA<&#)QP`o z{qH`@`D>4IF#rEAJq`lpzT_#^Wx!pJ0md6|qvAZAqwnfwzhh}Wci!FCV zzk4b?HR-C?d%p6)*BKiMUbySsUW0mxTh@(e`uT#_Kg>*M^5R`CYP^vSgbGk zCY8Z**c)!@E!{gx_qHP(wh-TmA*xkHUwf(HcC~xYE*EsuF>k0 z)X@%GsK5BfU`~vsvHk00uyUM=SMoR=-*Jv`Jg?IobYz^>e$$2J_If=i z7+BSVS*eaN_ME>yTT+9YTnmf2shB0EIl{2$D(B->rf*4FV_)s$Sl@v(byBztZPfVS zh)f-?vItdSAMf&2r^WhC!>55ihVc_rXDgpF+!x6-aeSuD^zEGx>F-0=L@V?oCpMEK zLY?v$czb_#)e}{9oU+K+Nr=#--nIyT`mRY<0lV%%0p{dLf3)QwA3517#dVkITaDy8 zU3MJFA%U3`Jw;Vs4gS0-*14Qzl~ZK=#jbJaL+oL0KnFzl+*5Cqb7J{a)n`YvwoQ-s zO-*kWC~eDFv+?DI(HCd223V_9>_<*Cedp36I6YgvfS3Le&ZS&lFcOaG{?1hD#O zZoOBs_RUaKlx-`dU$DyM9%>GRHv495~~ zr~q!6shmT7UB8X>otuftfR!$BmLnVsu%2M$u!h4@Gw1-Uz^WEC8|~i}>zg{;5svGU zUh6i%!p=pXf0{n%!uae!+{&`+48W>vwYIo&g6CaoPT+*ca#a6U2eH^(WSrZZQ%fAb z!e&NZ6IdVo^Ua>8N*@W$%1e|>*zr8oY88X%Ie}vG=Aa^T61gO-%(p_rI40+-@|Wia z;;AP0(fmjm5H*V17O3lp>aVNyN+TlDvS)n4m99> zpipNfw)V8e`dSu6)*Ulbi-N1Tp~zut85pvsiyUFt=BeCb=SImsD4XjD=la)N4wb68 zfiWj%jccAGoK=dH$*J=KZI#K7Avp}mYMrP?GA)1OVvFl^YH@LJD0abO<5br6k_)mP zS>mvnfy!5u1p4GSeCq@;dcJD5OV%=PzUtsgXmLvH%3Fe$<>Xre`7FOBsLTVmB#P50 zGBVCE2I<8~j+x_UF-LBVlkMGdt99)-F&DGr)<~PvKb;d7xO}M#ddoJc3(vqz*%| zs~0u)*-N|nPApQPh`wE|nt8YDXy5J#XQs00EAMdmqVDLe@)4)6wwVqr_`FiU!w76N zj@+T*8SYDeGuD-47~|l=G0K%t&zQxQ3uoPyI#z8REAh@?&+?d=afpYB-P3LHc3f3x$uwt%wpdSpKe;x5viC@>YZ5Eu z{s9Tx#6=*1y)UEVzIv!(bY_+yjFPObdnzb1;nZa&lDZzzz9h_^i-(#NNZ`fH=4&@E z&P*O=7!#$HG&IirB&<%bn7O}GoCiR;uK+aKS@Y7_`6e~Fuvauf{WHQ{0z44T+$6L1 z4t9$BHBh#PvndjXydb##1&S$JhFeG8Hu@JhcN{91gakGnw^QnhXcmLUgCh3B;+$2m z2EbxH0~-j_fxbfU5cU-l;v8dP^@GI*$yEozH7fvlrgU+C1}oOe2X1N%I8rPhj)9S| zxZSpiIa6%$Ok-oKo`4npTTSgb}9W@UAj`3)!sqfHL!!8aSm zSZTF87#{(H0rMSCi#NX?gEvfM3L0USz-h8&**}Ig)pAV3ijpCdIPzbCCHsPHH-Cqv zvZpmB)i6*5i^h!E2#d|P`O?S4+tXBzGGt~kEQ~Z2K=UnFtd=~@-DrFuY8>ZsC2>Ci z%8@UJ0H_R*!!aQ+Fswdzj|Akrm805nbya{OoU$|Y5aHdg-TVOwyaSRobH`wYut9Y3 zcaM2HC?k!ye;x0DVO1rt;dh2kG7Pl13@2jCrgy031W{76iN}PQ=EN0EYCib_oHM9e84jP>CUF2HTb*f=t<&!n#tXG3~KIn~_ za@NaDt#dahLMweut4#Oh>4t#;TGkMLvb{0_xuA>yr;v%Gv8?qIpuIo=)Z~+O_#BjV zU`gC`tM#&1hN?py>Rbd$Uyh@JRvnx*+P4lhV>8vb!{Way&iMqa9=enS>r6ArV;K3M zY7}pSg-N6=yM4MEVB++`P(1s<%3cJ*3BYj_=CA3Spjbc30Az1BVq4AF8H$E*X1f={ zVxRI%MoEr#_bZ@QoJr-C({yC11ukF3!0E6l0+dUCw^t2W?^>uvX0dK$=uYX&UlF4ucG`kX=|wk_O$88SZrW*MP!HFnK5jGnYu zBOE9>T8vD`_6Z4zNMKb17ZKMwV5^fDRQj{`9JXwg5laJxgWBQh2zGCnrydb94<^X@?U(_IHBUy)s$FP zGIWS!+}us)=*jJ%WvTRbh@ppi7JOKs`6z2oL%!1fIBb*-u zQGKs<{2|j`s3rl1$)q^f?XY6?nf|tyy)V6Fj{XJK^*vmPJSr?BBrRb->EPU!2)+t_r@~9+~}MF*msenZ??|b!S4H`4%h& zMGk6v$UHr*mrsZ{6Jf9irK6)57BkU(DW{ENhxTVZtr6t>S$ncc{)c|L0Ph_l(PobAl3u|u4d-TusDC!TJj^TZZZvB;=F2s+N#uPgn26{8xbK+%&^6oZ@^5nPH}|X7T8-^ zCAvle(uMi3-qx6NE=NLdByh|I_LqD=KvfCXNpl~po9M%SJo|4k?z#8IDdkz-6 zP*3v}8Sbw3`D%D~wllnr2qN~#_-Yu`|Ld3_ZYD{T{}^ybd>Zy=5?o)$a~%sET+sFC+sufyV* zteQVOw!8fukNbcmZOKiqeIYDH!nbQgy!ko|)~$iBtBAM5;80RA>T!=+Ijr*K%I%m1 z%K1>IxF-M;0cGF1zl6o!u%kxo1hwRMTB@!%JkcK;*HlnhBqAow%$f666C&a~bGJxHsS^aa6;SgIJx|NiCi$Rq$E5e_3$n2T^7zJHholXkTP^ z83?IWy6X%QSS|Kh@L+TH^{IuD-d9>f+Wj!-aJU1?e46{(t%+?6XmwKqBv_fdWA9hh zX2v{ZUEDdKy^seZ)(nS?nsXy4=O8Q6$Y0)Ppd1f?a#x}rVqCdOUm!uP!6W!%kqZe{ z=vfmr0`CWvy~#|htX&_0X6R}#gjTJc@eio6Qzx#@*`R~q&UOS{Xr2ci49qYEuV?13 zNEk+c--!kBE}@nZc6<~L)hX|R-mJ3>j=ig6g$k$@H-FVUNZ=%t1I}uRdktuRP=?NxPtQh)U^_Hh|W4V6IbRV`ne9={R!A1HU{S~42}~^b^ZFE68b-??n3`? z#wsLCt|u$=2PE`j0{5!!&X1{nVo!4slRvb(7J<^|!utzzClV5opjP^pYn3H$?xw-G z0ZE}g+iUSI7}vqzfC%)}N1)w!wx()E+8C`h4fsOWy!x|JBXc@l;@~kTnx)huLCX7g9Mn9x=8|uqp80ersv|?qw^hmw5^~U;gmcdfA_ zhosr#X*KWUWz?|*mgR%RMDC7UdjLn_M7e6A&*XB3AJ6U2sEu=k+|Ojg%7Ryz@=!M{ zhXJSSQ)PbGuDdp?D-(Oa1rGV}&U^|~u0rzZ!)LHKQRw5sA&caM?ewgwU<6=PP=Y%P z2|S;3fK`yIn0{f5^0ha)QYpGTe(`aRSgs`f=zu zx^B@w0DxLSGM54Jf-h&V;}|TC4W4AVINER34|#)O3E<2NJ--CB78(EPNzVrsdq2Dn zaOwr8yS@kH{IaSIZ@E`-CTt5N`1f*-e}M9;YgJb6P9k5Zr?kTQ;2VHeko@&(v^}um zTF^BX@J1BGaO=X<9%?fZIP3@;;E;PLx8udYk0?Av4p{e4&^|%Fk~+#k`vtXutGnYJ zz^NC)YhKci;gO?NGsjXu&N(Xt^5Sp+Fj+&EZ-vp(tukDRv>{9)6wcA->!0KNKgOv%^klp5JRS@E!EwitQm9}h>XhsnA2f}7u4Zr7%|TWpcCOjDKF+bU1qs%28mRX|(*9rdI#>v0iCK#g{#^q zg&a`#p;2(Ppxbp&-BY^(IW}bLUAdraJ?mh_pVupM2NKvZxU#3kxqgS01gm+Fo9rgn z2Z5VVe@x9{&~9+IbN^*-f)xuZ+=rhu%}-$UgcbM+!QuL_p2taJ0SCZOJ(AfBi}OG| z6#oVma|{(-i}9w%A$7r5p0i+aS(ZPwyWfNrPYV_27)NvF#x_2zW&>9+A;B=WB1d88a)jr7H3hEM_m$oSi??O=DrIi>8|qfQ$tSQSS>ppuN=X*?X`! z5V)yyU;c66-YbliliNrKC?mnEtN&eUb0sK?mKAZm1B(+}wh?}868q6@KN48@Oj$2Z z@*$ws9!Z9V6U4j|l$oeNz6C3h#me6r!j7u-J(89)4OTCDBF9Uu!D!wO8V_o++Z~Qi z0=JCTD0JKe$b#*vjxxgTX8<_}!Gb2{PoQiz2Wu1#wFVFQROMQiJ_|biLSM7lXKF4r z@U2)8Z-+4zE_l?6_aMTLsZ~xr(Vq)zFw?}=#kM%->oDcmN2&gRJHtO$@zBFay##L# z1f_3;e1dxqtO>9}{7-Q0Ujwlj{{Fw_i=d7(Ghp>&(el3MNmw#Blx)S!j61IGaUAmT z-)vai)vy|L5qX7p)&HZQ%~z_I<*(u9a#+_POBy zXK&Pr-w7nh-t;ByjgS1NsvuAISI`T{PLu1Wb1keySZqUJjGYMfFOcf(@b(Wwb`eD?>#tcAs>n)33|_Xm{+ zL*ripT(^KSuMkpIRcx&W;nhAQ3_^mcK+_+Cs^c63iy7C+%JhM<{pyS_VR>OSlG~B0 zv+4}_q{(dZlZv@)fO8bAZt!IBs?TsYZU8_%IvH{%mPr>TQ0TE-LP(eh2)^?=w+yC7f@c9 z>^N}Un1~Brh)oU_`Gc?{0)Oyxd{B{cXP&NMxEzkJz+%m@k-_U)J@8L!7<9E)T@34H znT9vR>@b*@-0`@F;iYd5T$ave%B#5D2g(zyPIrF;z)`^X(M|0u+BLArNT_{c0jweL zQ9Gc$usDNcn(J3>@qC5|8OsIA8G}FI!Rt$4+TsC;9k3@MZxkFK!eYMuB{uO*(_vSw z<-#6VQ@x-!!ZT97fx873hmh<&Yj0|=24#W1#GexErcDl1)jj#Jx+4Wys()!9qr483 zxu|#lj=|zsVxDY=ibG^Wn_edQmfsv$a+N~5e+y*31j-1?-)`+^V2yy~i~S|unHZ@W zqcXJa=$3*Ghl?B8aG0&EH0?k`ft>7d7yk8ynpnCn{+RAV0s|(;qPoS#gLN1FAc>q& ztTqn$8;yMg=m1uPZyo!zfrzHXxlh7MKoWdqTh(leZe){-AgPKz?<2}U6XDBQqKa*| z8iN>=nX8}Q!|<0&WF##gc{AP{lyTvi@{o5R&x5iFcJBAwzrnIrV>w0W;~ap$l)^IE z7wY1{wFj^_peedKZ1FB_u}V6+b7LdjHuZi8AOp*8cJ&10d0ac`X$8~FM}ivFh>7_s zERG&I#@%7~6DvFk{()GSiJ)wedJlCztTo z@T$|%^O3-gR?F#I+LCoO8?{tv4Y;tFsj%2${EELV-VTGM%H`DlE-Xd}6<51y^HROO z$^FhGSZs#8Li-{Gum9h!1U z33P_DGyb9wCr{w&pEuOB1F}!Bv7Qv?sDLH+0ytBSLHuyj7>rd7V{VE^Kd>C(j8+{W zmm>QxSiEZZ=i+Su>@9i6V&4Ue#aPMCUjVqO@b1XJs+fH{s&LAMz)=cIcCl3(?p0p` zWG~7%x_^bmO$Zk!R!%;yGS^=dR5iyJ+F}D_qxi%3wW_*m`!pIBBcsOI?XVJI$vY3{ zkFeyZlet^(VD#*y#=8|1RQOPAV?ZuygdwW1Xdf0?J2+R3MqDQxp)smxc7v=ATh#-Q z@sYuF=ECC4VkZ6#m+N)V(V!RJJiD*ytYRtqn`O(Z`aw`;qMF#Qi>jO4;yP!+VxRI3 z%D-!J?gZto_7a(gZmQG7-*4i=Pt)wBq*b0^#>ZLZt%a>?sBBU z-Jj5*t4;pXIfla;Uh*00OeApNGYGsplbdDNdeFh3!CMDx)?C2jfjOMGyr`|>kZv|3 z4b;gQ=z0PcW6V-mKdTDvegNi`em#psP{)>uh2e4IH zs}=HJ)J7nIQ`?s~Dc*g}b!wN{NN1^<^h^(%e1CxTW{|CVxq9@p$@j-tdaIxWXHuMs z8P90!-)yf0W&6Zq9$UQm9xP5sn{W49@eUaMXy63@Y_m^|S34LK*hO@SvNdcBzyU1X zf(mr~iUjMTWi?N3*b@?hH=X6MI40=Q0*CAccT{gRfJk}Bf6`>;gAS7Q>zW$xg29=N zRMmarU^!kK0<}tI4AkWA+(+Fmp<`15h%?y+-~L+~+F`3~c}<>tgSuyucbMjv zu;c)cyTev}EerXW<6tGr>cDb83d;%vs~qAxxHQ)8KVe07d_;w>0j?I3(F4?( zA@VkTD=aQ6GA!^>eQX;TSS6VvN4D7?l*^1wtnlKI3@n+u+}C{wivd^v#Nx6+YRy&K z_zAGsAoZ)kT3B-Mvct_UVaYsYC$_oK%9D=}3Sh|~qXw*ndRKKXs02O@G%w)h}hR9xNiMJ=z6pGMwd&m-#cSRB5qmM+~vD zo%48nV;I~du@YQsWJR$0yK9iZxyt#>MD?S~WkXfI0CG~upST?*pqwhYC2|wx-U?_n zTxKG#0GAHan=;vPS+J~*^ZU3fK{HrgU9{{t_oc&CXz0fj^~h^7Xd--hedGG8Zfc(e zWk)hr;)^S64P5|Si|N(?ha5%DyKhp}Tp6Rr zT3b%uWDTohmBC_UFrbHHQwuAwa&;VMEe5!xngy`rwx4U2Yd5R`IL{h2{_)*7RaXQ} z=D1aBMlPsajzI#mbt7mZ3)j@2NevRLY0N~KiG5I-svSok|6#cEVE}F{Ir^|?);|~? z24&R@R@B`Rf0B-YvGyFe@Bby%0mxq9*lEub#h9F%>WYs!hButjkizK zmwmD0cJ$mpm@LJryL$M!1XQjFY*AoAJ`sc&6tx*hn4~VD0D-q7@&2<5ockVnaulk0 z+H|s_)_77omx-X()wBtGo5`_vKPcM=AYU6+^h;1a_^L}|r_@8mV~cHJ&ObW8Q=hZD z#Z-0GubXiOD4S82KK%#E7iIncIfMG7*O$Sa_0s+I(x{C3a!9Y2ZUVjW57j+WFOAQ< z(3gLP6qq6GrO|O@3 zs+ayyFYWEEFW<%W()U5FVG^is^O^P0iS^Pa>ZRX;TH_>;ZD!Fav~xN^MvYL6hLiv;EtD$3jqRQ;Ahn zZCDNirofe1aut@FWY<+WYBVqd9@g6LoPz`_z=1zB;jRR>ZV*iP%R38qSgxu@!!|$` zH3xz+3)8pzi+BeNUY%L=0J+xlFMN@*t=yYkZe2Q@L-N!%tPXW-08NB1*JGlX1DnPUd53wMC#qksc&BDYK3 zOHm0{M1H8OC9;)$g?7uH#1>5|E@tvWWp_-}iftMHgsoA4gXb6F;3rtwNhLTq25!Z{ z5464%nSM7D@u9N3 zr8MxN(!Q4lK2-YOr**m36;OWaQx2d9WU5l0FK{s5)eHo|gE;u1GT~tyoun`pN%*oaBRWBPp~os+qjG4i7()wZpXn-eae=+jDs1!ii002GkP5d z&)%y2XXL2B|k(1 zpCDC%Jq*YbK4L09RGx4Y2l*#B_@R;?qk+$#DgBPq37_LQSmD3p;3uGJga?5O9aMoQ z6*j2M=o=hdU(ev+eaG)O_@T1G{14S84W;}VqHSt8Z2l>7>1fw#YKOm~HnDS!twlSY z;n1hmtK!%iTboiA*92~iOLOf`Wyj%16WOd*np63;wYL9EIpeR;epKcc4P`mkLYdSF zf4^LQiJ^rLl_z%9HkBvfxtiqhP$uCyl+^2?j61`{50wS=({`{D;rWOB1XMJ9)Yh<+ zWhU!1DuXgy+x02aMp z&tFlx;tDR$%hu;n85CUYp7)n&SMkfU8Wk{$5=ZP=tbSh8W2_b7}>&Lg|;RIhFQcZP%wvAENz+LghAMm_8v` zS-=R*sr;IvZ7RQx#(%DYmS`TVM7L;8Wq!A5o63rmh9LfA3w6SuDNk4gKW4O8XK*K! zU9nW3U!OARUY$;5KKE%|9?~8bKFhTOmD}yr+NRpXz$a~2%Y|Z%##H9>h_kgexW%|odqF1%OlLkIiPOi7Kz6)j2KK@55zwX!eevB3QAz%v+Yn#e~ zKGJru5*^W;%CDbjo64`BX`9O53~Qjw?^`HG!Vge(ZRt<=(imUl-z@$gDzY*nkShza zLsSD4pg4dOxsl2&7t&bq3tLrThtC}h=J>|1Hu>M z3Yr%vPj9a|m0z#Y+7Zh1YY6{?GW}YePNm&RYmDYl`Tz4~!~yWf&3;hcTc>FUD(gQ% z+f?>^I+Ph^Xw8J;f5voe&(QWvZO?*o67v6;oB7Rw@-rt?wm%qH3CgiRJO1w|3n7_n=VGRY8 zm#T-M{7}gs(>9eSJg)UgD3jJ}-9!VQKU3at@6vu+OD_cfk8Sk-1vRSI=HPr6HD?6& zK^gk@wS7SA2T(@rBc1*+lo9<5$`6(29n&_IYpU^u27jh5_lw-)$cBHo5P~M-lfy(jEFU;r)tsS7u;A$uf?4)@Y&AUSRp|VBYbb1dc zmzNu~odo5F$_AuBIr!3`4ycElFRr-x04%T&%24x`g!V#cOX!2zei+J2#uHjMLHVIF zpO>KA@Vx_N#rH#5!2>$|7?ctDQfo~Z+RqyQ3xEaw2xSF+hjO$wM=t!4;c6&%j9sBf zGI~Ji9|vXRuGe;NDE?<8X+A*nL0Xfc%y%-B<<8(8C^MJ|<%i0a%!4viOqY>>((ev! z-w9DhE@MwyCt|Lg`nm zIhDLb>wL|rDz87c>4Z|9K&4}u)`gl=$rovx%9bqFHkDm)x3;NFU#4v;D|#Q4 z4fa9h{O1=sKA;^cv_1&sCs>)m!{DsQ8lC=U$}W0R`%!uRQ&3i5Qv*Z1`<$(#+}Lf_ zPE>x~hX3&7mo=xdk~^U+{58%0OzHQ!_6t^4Vz=hd444X#y{!|dbleMNMGioj;X$2V zpVI$B?RQA)Vfu?bTl5XYC;IeYWwxJcPUX1&M%%$kziQ2?v}?5eXUb}Q*FbePzsL`j z38%D8DsG^f&TqHQYA zYpU&FWx-M4)K)tEKWeGYs5O)YwAFSyePXb35$yub7IoKt*JpiNNvKdtTOp!@_Y^WCa>eag7J5Fy*oFFd_|<;CVz zq;oD*X+J6}uvhClTHl4Tg8Q|7K%NeWnvWhw?+E<8f^V zD>FKw)4$YuQu_rfD}Dx?{{PbYgVxd?^_R0+f71Fhlm(xI@)NAg=ofG~?+_l^Hiqdh zD$k2h{-yE@uj6i=Q4^g}u(Be}G^esomudgYwYJjf!P*{viQqiHpZ4nyWyF)AZmvXW zI$^xl2~gHB9m+152IYs!mU^^JWyUk0^qZ;e**cxdeDbwjuX6k`ff*NQ$6)1Pny2|+ zQM#6De=1vg2b5?j{=;&XL0R$ppz?~hLIa=HRZxDYtiWo`AA&N2hoS7Ur=a+s@htzN zl^Jh=O~0*LpV#U2sG5IFU?& zq9;1z%Txe{&;#WO)3wfk@qO5%lJ8$op}f*v-=3j(>{UH{Zp-Qh!U1?kjL zT1RVtD*4S&&VlJr=9i^)CX~S_fYPt96kqr$3{oZ(!RFn1Ig}aP4P`|ifU+xAYke5X z7OjOc^y{H)@zYSAw^{3RP~Kecf%0Q%srBXmla~5x@>eRr7QKxY@Hyz`x+VWJMgRZL zGWhSE&%t++4dBH_e$mRlt3d+$_#e83RE~*LTK}o_d!0^Y`e`U5^e-sKLM@bo&4$&P z=^;?2hjF#m%JL;(LIWi5gvQ#zQu^>^A|Cuu=|2_+E1HIb1&r7Fzwu&`EdQ^(S%j?X zylM0wZC32BzgTpws`*_qyFYug2o4utEV}q&(Zv^w>U+`X;)_KWUo7Ifd-27h;1`h? zk&7=DU3{^KFB)-sbn(Tai!T;&O~B{RUNGWNx%gty#TSckXT_VVi!T=al^2XSuP(k= zRNo6meCo4{11X@T7lQxCHg5Vas8PK(pF&)GvFPH9MHgQzy7*$z#TSb%zF5R7*u@u% z7zOo$5r6l&_+k-XG~zAO#TSb>F6w{5h}*x5FBV;VvFPH9MaIP!i!Q!cbn(Tai!T<* zYo&a_h>wmgzF2he#iENZ7X6=mv55Z+=|8_%^is{2woW5NkGPPbwr|A3xR7z8vU`YA zoFu3bUE=|=t^-&V5AdBhK@imgU|<5kDX}5}po-uu!S^ERdVu1d0IjYAI3rdQM8^S~ zzaHQRF}N4N5rWOVLWYK%HN-EyLdJ^nc#uiGL4G#Grrsd&2_VisAm$xf1A z4bkuhkc#U;vTp!6Z-|#kl6!%)>I?F_A!haksU_J5VzY^si70$SZ-51f0HI zm?VI3QIZ5;zX9MFL4@eo51^7@Sw8?%93{x=3(%)OK%`jGA0R3b;1q#VBn$wkB3Ls3 zps}bSC{6+xJ`lhyRu2S-?gwz5ps5%<2;d08=0O0>#W{lV{s5D11ZW{P-Utvs0Kl0H z&{Cu&15^|2B)CkNg8?cA0%Q*cXeD+KBo6{;H3XoI@D2f}CD=#MPP7~fu;E641w#Sa zi@gM?$pA6K06K`0VF32Q0LKWf79EEJR1z#34$x5?CCC~A(B~$AYsHeA0HTHhoFa%3 z2_pci2-b`M=pt$eiiZIV9|_P^tR4vvJsjXXL3c4Y1>gw5<`jS);v7NwO#qWd0mO-o zqX6PZ060ejB#5-p0M!IL33>_hW`K&30NFPK^btD&a5{^>4X`Q~V3{~UP(?5>58ytrA`hTA8{jO#3XwDiAUX$N-5da) zI74uRU`#&1DzP>npgb2KyZ}ImlmdYGJb>*4tA(u)pqju_2=I{D3SfI!m_?8^BAxPx z*g<(zxaUG16JE+%v77R^XgLq^gqQ;nsfEbs@H}L+PP8iqe@c{4){BFb4WeTSWTRL} z*(8oqo)%r_L!J>!D4WFzh^U%}j0WCk+y$oRS?Er6!g9McX zJ?;S5D;C}XkhKutB*D9)>tcYYMF6W71MCwg2&xDM-U+Z@thf`P_;!G^1P4XZT>#N{ z0Ia(U;6rhS;0VE(a)85PZ8<>sVu0`^03VB#B>?ev0&FKZDr|QHR1CL;e7yEO94(2d?>2Z&k*uxdHLDRF|JieTUhfbYeM6#&Ke z0-Pl{Ba&7EMBfLnZY97E;tatNf-ydTvtq3epnN$%`2ARof3}HH_hU7VUjee6Ir~VPXWY;69iQR1J?s|5i8aM6h8@YmY}Oh z+5ix}4q)8|fbQZ9!4ZNn8v%NVwHpD-p8^Qq1P~`uHUY%12iQ)KAZ$+qR1#veX#Na9ACdkHK=MX_Jp_G)dow^SLD6P_B(a-d!zO?Z&jR!pbDjlAeH!2}!9daO zIRN`J0E?dkxKSJ=s3hpI1z@mPxCJ01dH@a+KOM9Ox6_~!w(6Ql{- zivZOGo)-Zoh^+(_+W?xs1TaaYzXXu{0>B=EDZ*U|P)kr$36L&!6KvQH&|wEarkJw> zAoWFn!vr4D?qvY`O8|>s2AClZ5>yiOcm=>K7QOH+irkr0?%%MTf|m^iq`;|zX7m7q`v`>{5rrMf>Pmr6QGu$=uLoy zVmHBtT>u^40=Qkwc?%$QH^5+E0I*&hB&a0laS&jm zSa=X1>wSQe1W${u9{@z{2Uzt1z-DoRpo(DNhXBus6(0f=9{@N@uvH`-0*F2cunjZn! zDbkMsBp(LYL-4wA9|fo-C^`zTTkIy-@DV_VPXOK&b3Oq`{TSdd!5-1>QvmxBfW@Bz zye$qAR1);~3}CNV_!&UfQGk;K?~1O+0HQttSal3wpEyBKMKJJlfc;{{=K#f@0-Pl{ zD3ZPai2e*<-4_5KiZcX9KEos&dmNMSuvmK>p!^s-!cV~CW07(KApUcJ?F2`K?e759 z1fIVGd@8mQRD1!@{7ZmiBK=E%0z zL_TkwQvVLp>MLY(Qh2{YHuf*+Lhy}fc@m(KV8Ka%8nKrk>nnhmuK~UjC0_$Xodh^W za7uLi2B3;y**5^+i=za^Ujy{11~?;@R0BkR18|Dq2a!+%aD-q@4ZvAZLr`7~F#KD9 zpT+8L0pe=_&J&yygTDi)CfNKP8uM$2_~kn^rs7+WN&f&jA0jsW10?x75a%h7-$TUs zQy{e@J3+9kF#m~cHv9u1`=0=zVh2I$DS%er1B46j_W<^P0_-D*5G_vwR1z#W4Pc7B z1XqD})GBXEk2{{pBYSoSY~#^NYJ@fmHu#+H3m|*}LLIASE0Q!p^1gW6_t-=8Y z3U4@oJq%zU!HuG21At0`1q}cOi@gL{;Q%oa07FGd1VB^+fMW#1MMpb86~Qt)zzA`a zpg01cj|q?>mY4w1c7Rg^qeVhPfFlHJ8Ul8`CBUO1?Mi@Jf}I3wh1mgM zLwkVi4ggPx9R#UY0<^jcV4d(@1z_(0u#aH9Xn8e2CBcHL0XB-g1X))B#6$x;ElQ#R zqOJxwMzC3Q>^uuUXf3vh&B&9wmAMGZmu zH2}jq0lXwucLIpN7T`R=4ly_epqgNF48SYm96?1VfJvPJc8ZOi0g__?oLvB37inDp zY6*4{>=tG$z=qBM*|7j`iX8;0T>x5j1=u6JT>g^s2Cg%P))Eo9^g}Pj-VnAU{V6WF|jcLAUPhu zc|E`vBJFyBT7sPfCxqDxU_$~xb}xW0#SVhh>j7Hz1~@6ay#egK0QM1lBU<(Ws3cg> z2cSmmCCKUx5OV{-ccSD5fT%tI#|Tb|j(q{D2$uB)_+A_(D82!pPa?n>u_O^7x-Y;f zf*(Xe62K9HHAw(xMGZlDBEax|06&Y>{Q%;V0L~Mf6NCE$R1<9OABycqsQ9ISD7GK{ zKqd_UIUg!E4FE~*58@mM@_VQlKMAA^w1h5-QCg8)Ls4uaHy0IhBW2p8TP z0qlbS_7OygmdOB>1PhV@OtF_B>qdZ>!2pq>WH3NfGQcqcr|38Ypo(DG5P-(wC_(XH zfIdS3++xX4faoCrrwE#ggkbOQSUnsdei*=cf|g?NO#syd zn{NWROq?UA7!ELL1VAgXaRfl}O#sf30BuCtNPt>`odoTKnF6q31VDBQKzp%+Aax`_ zt5Ecy#!g40Ai*93>76)0HP)X93vPm zI!*-!Dx|?0dRz1O$NXiQA1Fk4lq0uV4PT; z2@szFaGoGd44wv1O|W?yzyxs)z&25&cp#I+M#^Min+};G(kN5KR!X`sXFxJUIwe!= zpiC3)EQm*VDbvMn$_&xc3&|35C|uO19`Y3z8!iQgX#nN}lLC z`+v3f=5ab!fBgTvhTaG@3QY?##$sq93-LALK3oN*CNSM zDtkg?Ny?Jl_xZZ7bD1%XKK&k#-#_2S<2w&e=UngeKJT-i>s;r$?(4!CU?$5MXnv71 z$dr2lXRvuq&Jc56&QMdeFU~MCPtI`jr<@Vy`F=Pf&0;yD%q=;i`FI`9i{=A4V@%)x zoOshpPJ;PF&RFxrK%8-=qnz<(gPaNG=|MOXO;0(K%yv01nJj~GUN!^dOg3N3nPPGc z!Fk1ukTcaBkn^f3G!$o=87pVHIgDe5525la4Wsg3Gn0oQC^!_s?-I;1<%T0TB*B}* z5zIE{C3txlf_ftm7&C7Kg0jOA+>>CQd442<-z4~GB!V~0EeYn1K+tX!f;Y_vqY%^@ ziQuWx2;MTSMkBb1VBO}?K}XZ`+0|eZwKyoX_iSxm1G+A7e9(J=j&Np>&vUl+y81JN zatC|obhe?-IH9kbpi8r^)DD+wXs4KBi_CJc;&~sMl*x*1g)E~ zAZUWedo>{-;7M~}QBa$83G;%Q`fYgfOf`EK1@&@LHi2V;T7(Yk+NpoX&bJu~A5_qrc?IvBRaP?uD`#OrhW}40(o;wG`Cp=& za1l4NqvDqMsTVp9?A^5sFXTQ+wU}R)1br47aN7T#lY}QutI9I@%{c`PU2>h*xRK#) zH_YXYLD8jqam=F(9Q)ril%R)&irw_TYb>D*E^ig`%Z7CC)3e)%UW29}OS5jkk3ruD zt}D1Xs8*nNb4VZ(f3}t3ivA40(7Q{|f&FQOR{VaM`Ez4Xant8>D$jrHWZ|h|d{0KUZu6m) z2vz8Kk3XT4yHI?dudReic-r3vp+koBq@H*&U-Y^YyMyLGk=P85f{CJSJhhjs>-%HS zi9qkOd|o8XpYDHX=2C&P5*ieA+U@`S|9@W%ocDyDoTZl&@vhgm&92SC6|)E2f8hK{ zxRFQ9^%TCwhyFI2Tw8)ORN91?e|{qT0lah;fquFmYkGmT{yw+SCD-Rw^!J4sAQQU@ zl2yGj$^TFh?j9@CyTh`B{@hoN>jg%+{1UQiw#$UxJ}wjbL17*nbP$;`te0x#v$7wo zOs`nZZ)HE4r?+C_kTtNVb9*`fB;P8X~q z6G*@&6jc=$k*S*009$S3->sexS#Fv@;g_wRS|g8@T|uT6Qfm~Hhrhp(@h>0;6mnl{ zdfP_MiLjVebYIf0X6vgI_|ps9m8sm&%*q~ESsrBFtW4cVdU>I(l?7Uv+BH!9fj^Jl zn=VB)tj7xV7IgrV*9d^vDz(1qG4mF9MmmTnQ+Q zOyWW|ZZ!U)UfN&6!d6%k|4saAfg)C>{(2C<{`6XVC0rWXTUjwHi$V6Yt?EcCQ-?lj zW%|Xc;+BP9tt?7!lb5g@{9=XfJMzmT(?@eOko3NM{smM3ealF1(ThQ#FL+E%6)`YX8_PzPDOA~!}T+clwqRn!~*rT82)v$ED!Rts4> zD{F&HnRy;$L_@co)nh^EmuZ;BSy>(Yr--XzN)tNYQ`6+p*Osn>Rjh|hFa6bDM=Pt3 zzrK}qva$xqp0%>hR@M+%ODpSQWsQ(EL#DZa&lva%*H~}58)y}~Sz!|ts##fgWGY=# z7-D5TtzI)^S3~)azg||>9RGDI>uuwhA@s*AoA18CLuEwL-nHZVTjrepc2R zza9h6huhy8Y=i#}^Fn^yfygxc+QKb6R}8Un+aU|JvY}QMhb)(s4MXPtlvI0!%Ru9P zI6`Ht1KeY5YK)JxvX1y$A=4NiZ4Gw9|GbsGXl0#|)w8lO$ng3nqxx1h-s*K#`5Ri{ z1S{-@atkY)Xl31z)v>Zk$oLn~1N89*O)gWcUQhh`UAF#Sv9ezH%Th&}AEqMHsOk+B zRc)&OB?$Q!&dS4xP6Ecm!v7ia0 z2JTL)HxB<>$n^KQm5s;0#LB*~vI)p~BGUx+B{F@@$p2l}URLW@hCEeVtl|{ z4pt!4-=9`-9{vfo_y1*O^O3EC)^OFz-oURHY->i;hhS8?1>mu=>sIzAGQId&e>bdb zA^yR7)2{yhw!*jYm#~@9LPQ1^ff8;Hx2$Y2eh+=21MY1rTY`TelTSC?J685K{_R2h z$KPEmdk4R+EcAEJUH`qy!53C|-zqLeRtuSCs|Uyw{2tV`G7pI)*Wcqu; z%09-w3fVZ^P-M!(O3=4cCg7&AdY|AQtq)vC_>>i{!mqEqOu|iTWvlVOfJ}estid(- z^)`9^J#F_afQuL<}1F4*9%5QC_&g7;2C0q}By}kZ2Tg46dr`gC6 zR<;pYbK5_%SlMUDhLhS-+^on{;!V(2IpEJ{^)};oKZ};l>TOZ~(`Red3^}agR{ZfK ztZK_?W!v!UZ4;`YTsHD{{JqJvY9o);+kt-<=r6C8?Zp2zsD|=c+2{Dbfp`h?Tj3Y@ z^>TmJhQ6Pqn)wp+`5D!Q*7YjXS5V2;LJ_N{M2Dic4VQJjKdus0yMKvW%*uA-r``Rs zNG}&iO2nTNgo;qyDh9k0%n=6H2b%|{f^(UBn}buW%db1eC;0e(6<&*Ff=oR_vVFPRgT~ls@?XUwrhcDnu$V)%T4+TKikA-S3f;OZvO2Us_NKFq2&I{yqO;TJdsr{M$m5cHDl_hBdgh!>(xYq4jIfQG)mlVr z4_!~{`mrw5g9e(M8bTvz3c5aQ0j;4e=-Mz2^l6lv@B_7!gNY*xWP|LW4_o>mC+K4m zhu|ARJ*D1b(??K;hiPvW=cEh)zPh6ZNvD0uCR&%@t#-XTd${x@{kzh2u z2t8pabcH(b9O$~T0knsa&;fK^Srz(2Ll_U8HU4#R*$P@i8)yn`p&N98cF-LLKqJsK zW@D%h3D6IQK_3_donSZ&hPp5cn!pQC1I9p2=mpiZX*B{*a~KP?K^LbLp$h1$VlTnV zP@3Z?h=#O~4#pwZh3hrY#p)LN##Yz{+hGUngwNqD;pgBt&_(QW_)4D#*bj%{033on za0I@CUGP183rFEN9E4-=1AGm8;YZjFKfylu26R>YA{aWJmWJ=*e+QO;zDuh!YrIaS z`t0uepwIZe1!F-ksQeTr6RxiS>r1+yz!+EtAH#CkPq?m@kHZt7t7QKNLU1!cIAnor zkR5V>uA0-3!E~Xte;A&pAw6V(aL5FiAuBusLt!|mwkEH&O;`kPgO=o4 zS<|usQ(y?_5^yk#hY9cjdrRA8GUs`VIinkmyS!GK0Hm**bx`5RM>(h`PA|NZ2 zVYrop3Q!(&6?~U?_uxJRa{K^S=Xnp@!LN(rTW}G6hhkj`&=s)0hpGMJtq8Y3b*KR~ zL9ZO13;GDVKA=7c^yT;JPy=ehbD-sX2Hc|X6zEzp4g48k`u-Fg6R$6SG;>dK`2V|MM5-`gi;U#WuPong31sJwIKd^{;LgjpdK`UhR_09LMvzu zZJ;f*gE(jp`m~a+)>=Yqh=b111xmxagx{qgC&{EPGj)p~3f`vx9{}@Qy#I5`6F8U% zQ(!8*3e#W)ya@UvRsxI#UG~L+F7@T+BUbhY*}2~UFY&=Gn= zCddGT$iy)cK1QO)VL!Y|IV#U z2(2z93xRF{>2gt*hq}ntMX+{bw9%sdDs4q+i*Oq(2knK%Pv*a&&>D1Kp*lPVzmV&* zpgS~GKzC(y7iJgqhYpYfzJ_PX@N-ZH>O)J&O56Z49S8>~i0jwKJWs&S@Qd~*PT@HN zzd|jPo5Ax?2`WQzC;@ZH#AmPtTW{iO*Zww5a1*p!ug_IwgiMeVMi4$8Ccs<7nFT{( z88oJVSE-$A+Ml?N=WnUJ8ckQ-obG?LGet=ioGG-}yJtUh@e!1!v$F&=&GZ z(6+I*iMv5}=m9;U8We(btAQ#Pcb9 z44XkuDy)Squnv~NN>~reVFP>uAHj1_3$$g}5T1e}aM-#(QR3b31ssJ#peDHuci>62 ze+5VnR}d)hN9$@YQB{7Q8u$aQ!DZ0a;qUM#Tn9CYwgle=Z3n#w+5#F0ec(H4dZ%SK z_OzjAfIkG1v=w=l4(;4ysng z{T5Vv+U-+~+gtK`T9YB=&kkNS0wT^d;{|l9-#ECsSWMQ4Im>UNMt1R zgg)>BD4_(xwIeqSb`n?Bd>DQN9Vcyyn%L1-u-!~3?v?dS3b+A9e|2`?$P1GI))x}6 z12-3>g*hD0hFLHZZlQM*Zh)3H+G^45xE4^J<1>m!*i@>7S`3(iOlP*qpliR;Fbdj2 z8_U4Rq?U3`>-GN9d%Qcx12p#(%h0npBfc1W~R z(fTVGwDwxY@pU*0@4~NeCSYCmGr{G(m9BB{2Waa=TPWH}xd<2FJp2ad;7_;$SK%+~ zzhb^VADkylm17@MdZ0=&ffs_yn6L}M-gsU2{tb6P^|%9WgL;66tnuhvzytjE;U3(D z0A#5^OGYoG23<#m0#zPBb^EWSbzSAQi7VH!uCsORg+JaOK>`IxDK`N+&V{QBwX7gR zSs((mepN!64P+!7o`!UgJTch`vvS!^nvVP#wBD658IodptE^t+<~SuI$qgm7ky0jP zKXXd9)yLhEr(`s_v78vu-r5o3xue^S@3V6ESUO4lTAnOUB9oo?q)N(wn@g2mVR=!? z55=Jn=p-5mx_&MUMW7fcA(t4A-P$W_{S$FD7CV7<8OH&ADBi!#*p`E_FcP#~*$(nx zusN=_EVXr66^227XhWE`EbHLrhQ9cFLod+TzANa=-xQ(=e-5`Y)Px$KO@S&8d^ach=Y#M0hBrUwXRX=x4lh6-i#BtI!Vnk)s_FqCxsK(kmS2R? z5I;u$K?2A~2TD60cLL0YSuhh`hSy*cxV1DDzh)&h)nxqAc?pzR`Cox4FauQLDDi&jr2MK8CGfLKoRCuT}T&zuQ`4LcENnu3_~$84|guOolUYYINk|6U<*hl zeZ`lq;!EblDaqLz!H4iJyki63#$5u7VG+Cq3*k*zU^@Q6gN#e@egN;od!Uv`1uJ1W zECWrbD{w!8kKq$g*H~rU&u}-w22fvKkGl@m!l$qX8Y16>o0w-78>+Z%2)BaV&*4k> z3i4B7s)Rha$tyw?sY*~~_z3qxFg)N`6}B6cQ1Xhshx{&Fg&S~D1Lp{yGjJMC!7rdL za}vGaX0|SU@z=}Z@|_40sr?Poqg~E@s*+dpc9+AVJG-bZan{~dR1Kq zLH+e8C{u^wM>qsOL2vYA%zwqn@dY>!zrn9?4$guKqZ0lFf5IQ|J6wXxa0RY`!r~>6 z;&o7@+n|6ua0_n2Mfe*OE`3)|;R@4n@`PL+$)$H6RP{nN5I}Ps2xL8@stMx21A!*r zU%@5g)8S1EPeB@Z5<=k#NDZkV1hRwdXi=0KHy7l9Y;uud-#@UGgmk4Ra$0{H{7-_y zBvTy1Mz6$IVKih}Y`6=ER&uBh?^z;z?^j`Zd5Uftn*}Ge}p>phQw? z3OAn8qY2{wk(d%tJ8MHy?d-O%HYa7+U6i^dOP(P&o?9TrNnRkgnV!{s>ZCc6a&1sE zJT6jhI#vV1l{qC|AJm!L4(VoI8Bk5t1EoeZ|Wn8tP8c3cw zC6-*KCUethNq7sjzYI5rW}rxJd#Rn>HddzG94D1aKP9=Fh{9D`w^JvL>(mtO?<%X^ zRMOUvG$YAtC~5Ckwf?V~QpZ(oDI@==Eza9wgEgxP5Ly#H~Iz8uKl z3(ymk>uw-}`g3zFgDRDDrPCc8jYJ*E-8gP0l&DS6pPn1PoyLE10}6DjFQt;Z##5>( z#dWJaCA)4M*M>IF{1N^UfP65tjJEoHky_X5s zY$=0dApypKHT2Mqblgn1Zc2KNJ?dWuUxG<65n^Eij0ZOnccvSMEO~;eL3bKf4Qd*8 z6INzanB;L}*DsH^A#M*>1O;lcNFGVY(sOIV)vK0*uBv|uxRaF%Sqoypo!n$EUjMm? zCv7t+CT+KrN}(I;ucD_7x9yO0!%g>)bk9-OH}}!`6aIknumRaIT=pH~{oD43IXDD6 zVFwhTl-qH)!3y{YE+boxI~%6K>o5yuLOa4{jXk*I2tSBhjpO+5@f?7?peo%DZk6uCpHh{6hx{i{+aAIF4bH(?I0Gl(IQ$G> zkjQD=dgz|Q{RMPob`qECfq4JHuN+8ODDq{v0KdaUxCB?=9_XT19qA6-hFfqGZooCD zP54#Zzu-Fj4R=90I@LbF_2LFYkmg?*R3u#%%7}-6KnMZd4Q+|MIq2@FMvK}~{YBTQ z&k-*T@+UzzF7&vVZdhgn-LzB<>F%mJxbDOs!c~XXsL>tQFx{3_)u`mU75fZi2Km(= zWJvb`bs?;)qb&F%ARFX_?4ak?H2gH=G|Y6@MG5InjBe4CgeXu4Er=_-xp9?=cvXX* z2gnb3ATQ*DLQouvL18Egx-FyIZqe4SjFkXwH|zF|?kVW09TjR4=;;dGGg4>eMnym@ zB*3enO!U&ti}yLueW4}@#^CCHknR~Z<#-XUZq zgDOxN%7Dgu3~p&C1*!!Vpd4;lC=ZgWkd<&1)(D>0_^*hd6>dv#$GfVkD#(zkO0}YA zKa_hVCS$5{>AB&W8=mL57N`))pu$vI6+)TNv8yjz@yc}_s0|IEKDaq;hhGVg0##p2 zkTKQNV4(f{C6Z20=mBw{Oh~r{s79MYbNGtplB-&f?Y3(FR(M)N8&IIDsM2)hxI1)% z_Ml@8sSdcEpd)mF&LFuP?z%&es|J->bwDL7yXsTwGcDBqeem=Ke!|lW`;&=PW6Zpr1GNX>F8q_FxPX8$pE*Y&V<)t9?XW<>~YdCh0g&4>LaT9x!~BK{aqspP_-!op(OA&?h+UW zDwT$t+gzHoq^s&w=2SB(tz;^UYEmUuX(e~{XK<{-sZbimjb+dkD6#@{p4RGRr9DsJU>xM}#9l46*`sGUJh{nGROCZ+=nII!%0F}&L zoN7_35}ZW-Gn|HB;1o1u34Io~G;S#<33{Bb2;_!e37dkg@*tb1ASd$Tn*R&o$%>l? zq##2DkrjaakQZ`69*{9bdGaGV)3fqtx$5jWgaO6|=Yp8>BP z?!Ahd9`p@^RFDpCA$tI6@vpJTKZQROQbP!MAsB+d1A0S20Nf|u4Y&es_Wr`ZnczQh z*W!|9e86=MRHl1Y_&4rdxC6IAnYamxpol{Haur8@*RUSAQHEtRrG(w^#5go>QlTnL z2BhG&KuUg9zA9E>ilZ8n-_4vVSs8x9%4JJ>Pjalrls^sr&$8ih6Ho-Vg(c%vahIR) zW{Ov2RhyfbjJt7^5gAhR>G^EclrkY5x$1^)JT>p*{OUVxbDiw(Qx?9PjtkBEan= zb?{4}Hm>@F5^?7Y8Gjzsk=?msHM&U?bTcp4CgQiJ0?MYFAvO6Edc8(ngsKu{Le<(F z)K1MnldhVk57qBBkD8__GEKUw`dJumf?pGFW86lNw7J|2sbjhwQ3cefTdOmG64$J! zs#j^$)3ZPXxRKrY!0iJvl)N{)#w8yF17QHPgjP@p^pvXZboR&X2PQ17w_=v|_~Jm% zueOCYpl&|MbWQ866F;28VK5Yiz+jjLhseaMxKly<8aW^>NInCZZkJBS)sBL~73T)- zEZo=O7Jh@P+NuO^;$HxBtxO||;wAVKnUKA(xU>40wZM8Ib-W7(kUP zo|Y%8;SJ*GME(|GI+=I)6%T)BgV&8o=dBj9?np*&`P`;kId6U66vi!E6bb!`U7dV! z`}G5!z_?P8QIS!sHEWsu<-8HTchE?O#>gyZtCW1g*BXtIXp|(aTIT*WZ-i$@Et9=G zn%|*G1tctc>XmUX{nDbiwN;9xKrPd_yf@m@td6;ire{wbvq&+{vM-T=7=1GruXt#L? zqDIepW(TSGMxYUnMvWS^rj8B|?hzhXxe{59ED`W(Jrh=m^p~KK5sk2V?aiN!7gk8L zvCf7px!E@=cvsdJ5<~XXGjWQ028{^fKA$dNX2blS46xQI>0Lq?E(ue%ZU{Si?DRy1 z&<6ZvtZ$ZAqWnGTo0FBi-8@Aam|B%NUfIBmuIx?lY;0ieSN7KRcp96!RS3`5WKK;;uQ`4b3suyCtMLpA+%&FzgX;#+o z77cZE&o?zulTesc)tl9KjsBV*1Gn?{xISQH%>dh&OAGMyqC~ay4u?x%Uk~k*O+S!r8E# zvr|3oNi<36|%xsgXubY`8)x2>@D!~@g)2g|Ns^P7k zw9IahTNWoDZWY-W5uP$F%}U%r;NW)i!3-PUZ|+Nu1{--!C+=D5`P7{AexB{k9qwKiL7(fMCSLnFM*=2YuG z^ELPijS^@uTg+{3(mYQm`=zxx`7Sm0M{Bd_dGE`f3vJBGrSy<$wY~K{j+tU+Yi(~6 zoBe2WWwbZF&CP2|z1hsjI^I&A;&EnW9mZpoIA_#PpZ8pkzn=Nfol;1!R-8Fghb$dL zLnF4qu)=SZKJf8%Ld-M zz6WS(_AH(*b*CJ!t-e6wnhWU3pyKtp4!QvLoTvQ6Ak5e-ewktIrS3YC)9=e2_OW<|abaPM2Qep^`Rl26 zUvs%Bv&F5xCUrB~en3A{u^Gos4QDjHn|U)9y(?O@Zo zIpGn5%{0k#4L0xVxX560jALKg!A`}k{yKhUweHI`d`m<|tLD%QYe5Vw6mCI`6+=y& zjyDYD>yA|Uiea`JzgKzN*@>NABqlYku0S46Dn|R^CcGsT7)mKrO>fN0yQSW&_pYH? z+9^fx5l+a@^Ztrn8oxBwUp{{YRv2MMwq4)!asMTK8_hE|-h9}OQ~lKhQ#8&S5#A=j=?Z5qZOpnY z=RH<+_Y?R$rnkKB$+1pl=KpBM<+dg3{*ai3>|@Oe;>J`#BL|txSSh?~k9Yt6GWcO> z;|R%1NbByUwzk=NZf#=7xUuFgaeeQg!OAt^(9IWqUD@H6iisLujWtp2Y4l5DP3`vH zx-r!!IyvkyXvKru>6Y~jcUp(eTAvWji~CE5H*Iz7S}HAcN~^vD-kNCk$i}Y|&Dr*} z*IyG&)(+HDz)O6`ljG2r%mTUNUo!hUpf~j;bD;z6`0>l8aYyu`CYwbak)ywnht+)*WbN|9>N@^ZePQO3~dpe?bXrsciX;0?ix)t6+2-gZnA0K$vfYdVXD)l zBe#a141YS~twbBV6**03a)TS&nHUXUHLW|7i;2_B`<=;^+sd|=d2Cz$OKTsTW|nus z;)!YIr26H>X-@uI^*UE-$MJ2eJ%Kj+0gmkmQ?#qM)PL)BrD7^hcY4PBaxtOzKRlun zY01bkk!857Cq%Pm!@%UDW66 zEI0dH_uKPNWt>;{V$XAYy2;tYTg(*i?)CaEqRuW*LWLG>@pM8c|*qw|Pp5YATwBOzRYt60ME72@1 zTTua3W|&K|RSOMe?!B(RzgGS5?N(@%WS(W2#LV_Zd zFFWUtc+D*7L1#GrnzRU`)~8#8_)g&ED8f-kf29Ip;dlZQJQ{UO@57d1g-^ z3ij(fXGVCt_3S%~-aWlYtMC$37fmzYEb6ODn(tJ2o}w-5{m`|=)I^Pv^Gy^sVs@jU z#v7cb=;#?AZ(x-}*O7^UhFt2ZXJ(4ZGT?TN-c$S;_OC;xFEC>i_wngJ-m05-y&9q* z)_l zrPNayC=1Z^)j~syuF9LkQHvZ!?`=49Hn)3Yb z^1J08vdC$uTf@iyc{<<1ES`r~?+vD#{?v@ey1ojmBXqLeb-vW{AuCT@O4Pi!$gCWQ zCL{GwKh_OTEN1F8`|f!&nQZ;Nxjd~Fo7ny|`P3zjg`OKKzF#Wjg|opdAWM|hbhXfi zoSQTFufgN;|AlOx-_T}V1lV#v8A z<`_x)Zd;9)gO~Sc|8)7`i5gG6ZNdhSxtwo1Lu^;7hNk9kFYHLvC}l$;Zr?7^cy~qWO`P=5CGPey4En^lw6SeXC^PO8KYWzTmuGkw_-e1I*?dkUtLoO26W-jm@M`>Bj zp6v!!DX<_>DYFgf(_zVjA79&0H8G^vJ7zSE>Rb7tGseq~e>UvQ!A9DL(!w%|8x;O# zkM?auL+g^#g;uqHcFJ(J7nn@_XY0csn)|YG3k_xet9gg=tqQy!m8g++nTa06)JWIq zHpp8&H19I*)S$WYL$hX(cZ~DY{kZTD949t($~V-eBJf z%xJ~2>F;-MjX2WkqeL@bt~QHCQIjWdmr(nAMEsx1oLlP;6HD4`4~4F= zi1jA+6H-oInMwO)id~Q{>2!yOUD3_7>iggOW%9(Fl0>K68{M3`&AfZP**2aWKQdY# z9Uc*$)9X!r4I4X5Ozecj5%8buAjOoD*Lq4R{G&6tWk|W;53}j{V}t21k!;&O`ACKQ z!>HX%^)1`zTv9v}*5u}dRd2tX*tH+2A~z3gs?MFn43cbuNnX8fIo%F2pqYuBnxvyl zZDtnD=Ge`$oroUxiliOrk)H9ND$K1YHy3UnPCCuyb*H(<>_f7v_Mi75|D_V$dUGqt z?P_lCaI@%U*&P^;+QR9ais#l_O8wzsMY#$7TV=Y2{!J%&0o*ulVv5_p)-W;A#2wzkrHQXBz@?u}GbxOLSGJk3xr962=bx*dWO8*4 z)=ybY>^x53$yZ#|&B}S)&ag`-*L^ruop0(~w||rMO5T1M&3pa@Vzh1mI6W!CleCl? zw`=CJ$*}ihXQjOSxi!^)EAZqljiPAXvk36lX?RFEytd%Jl!qK^&;WW+!#9#tjnmV@ zP5*_Qv6*KUD$pqcyJ7wbK0Kr(5jmY+QkYwpPLuz0sqskW-3D+Cx{;l+7H+b?nZiQL zDw+F>sAApfc{mlMSV3-M|7*#TXW(%?!PRq(It3W+HnQ884|9~PXFQa3Cmf4I1{{^xSLbA9se~m zwr(Dlz-G=&`HMs<>UDbP%oUwK-Pn_ESTNnDb8`Mr0;s$kLv*=lG)`$Jbt>k}e6epAJ`6T6rbM3gg z3@QJWb<*Xn+awR$E2Sn#WcG;ukW@~Xp`0o;|x zBTYuq{*L;u>o=Qjvdc@yYEo;DY4nsvQwnweu)+84bG8lB{`p3svvDJ4_)o7TwCQtn zpINz@oA`D1J9mp(?Kg|QW-sIJYH!WZ{#H09)q35HlCKwu=ky$*}(3 zo_%96r`zS)e0a&(=qdQ;i9ZHqzOpVc;dQQK&9^c zyNrIRmu^N!YcHfkK!XFO@;YL~9WW=idY?Co*Lf#;YJG1Ct|$8Z?@e2|?|g4&;QE>z zbXMeT``5fwdqtHW{TcG#Fu!xqoLG+;Zuj0)3~u)p-N0-)wu>3Lfn#nMtk^&}$GbML z`&H+#sl5?7ce=-HWIJ%rVdu8WU!y;Y-P*{bo7pUFbfe=uA=(bS(4QX4jLhRBaFD@rU_nw&^ z$VUCo$WHmAX542?VN;Kq3p%zf5!2(C^MKDAS28Tyd2HTO#MFi$Yp*ke=)TLy*dnJ& z7Hj&X4WXT{95a13(TXjOJFS?iN}nkkb8hK^hSv6IbUSVqq2aNb9(!yqZQ_Bb3qPCG zo5{kBpH0Qhm7#kAJ(+EZql z+*YT}jxDrj*V86!D^EK-d%!H;ir%B$C&9DqSJP=5J!L5GQOiI|%dY;tB+RlbF%>CUg*-1q|eZ^eCmTpjCIa3R-Bhc7AqKc6_#-Fr$OQ1`l-dXqGI zT{kPx^t^Q4Y}4_Q>*mN!Y^PB9c8u)UZ=$|p8rgi^N&S^IXFgmNK0uwoY8=?_H2#NA z_|g@Q%)ZZQ7Uq}|?)1wUV>C~znw4Ksl_59Gp06mDzwbr+a^7@K3=4;DD%SHmuQNJJ zYaAE5Y3lEyL2O>lnq9yyf(bU%wgsdee@d93vC9e-*uL z8h?!$dz$jtGn4u5Yj3HT$LHO5$0=3V_xJbQ8C8pWqi(0qcgIxR&7>H8$Go!}!?tv` zZ}{i%So%oJFf8bl_g#x0PW}AXjMuSX@0xr`NH`&XxB0Pltu2Ey`fJSJP)xkj6gS^J zXU@9TEJyQgTgQa@i{tO=0|?RXdrbJDVdWaVm))N)f5_x}<`{8(3(?RNJYZ6Rd0z#b z+-x;?wD;mY6Sl`&H>k&h0MmYtw+;_yty{jwo73Z|9boqEr8=7gn2KlU7i|NKZy&b0 z1@I|iY*h>Nbm{(f>iO7;VX|hT?;l_qq7gIFY6OohTy4|j;E8Cc)0K!0m|#PmdH&-v zT|emOe-4d8(zMe8%tFPTYc;xc`uvOI@z4C^x9-2kbG@@OyEi-hOOfWuW?K==3c zB?0CVaXlaLa@74i7PTH5nw&~!TN*K8_9p-H3Q8p^U?(A}hsg5>dw0z+^B&gA62L{w zKHjXq9~(!pb)HiGhNfC7uH342{U-jPM*uYSuoG}Sz-&U(=LyvNtpXB?OqqFTa*k2y z{I*J}9>N066=|e!mMO;>o5AOtH2w2!_COPL*_$bQNnTLsd;t2z$Qla^)!C!Yp-Mx) zVxY->h0N6rG*b^?xGkDbll0hc7M#7h_0V@{>V(fy;Dtc54UL#FR%5`}OV|E<^Tp0+ zXitO8O}8NjCcQYsTluU0gp_m6wTl8x+V6<_iPd;2-`0xN4;1f;hQIl^8B0id(!Kco zgU$K!{&aRWQE)Hxwe{ip|glKTEN{#ZE6N+0Mjf|vUeI=P@0m@iNx4VdI(2KjqkxFgnPeGygDR zx{W&M&1gRO-do0#D%gab*&q*yy!K14s!nbpX(Y^_XnOZ z8qykXfFSV_39quqi36(KQU*zy)KH^(m8FqrQ&<>H+tO|L`1uIbV}9D&6bCH+J6cI zp@(=RxK*f$Jw&>lLY+=qzHG1TRf1Lr65XC&oxBz^iw}9rc@95mj-l>xhK9fLPCaSD z4kNNz368RzHl4(wgB%y?s4H)0u|Hycu)QP6_Fp)`-r$JpafNqjjg+o*8GP zGbgX2M6X_ql1A~dXNFd~JGTG3C}}cc6g!scdLqNj!edlX*)X&I7tU{!O?t$MtP+%PXzKEd#Ld^LQM-f6{lakZZ;_SK7ea>t&R zwxu^yiR&2?XqKMPVkv_&w>Dimaz*yh1>Zr_4zvfI&9xJp`#722jSPwK++hv#Gcj3- z4E>qA`sAYMNv>2b_lIHh{bVzQt1?)AKx)m%P};bf;c&(f@>Kc_t3 zs;o{A+dleY$KZFL(;C)RX_9{YkGVX;Eux2^m*!XO{Oi0Ki-zZHpc!)+&8?)U88&~p zllf-P9m$P!cMd#|)l5S(<}tG*)s|xU{T<*;R&z}G;Ndv`h2qm6-h1k9>eFG?I&+{i zsYUp1lD=+9b$WGN>F@S-OpS(SXRNz(UAkvX+*xe>&)HHJ6NR5K%cZ=VHx%iNJhfM` z%(LddrDqmx|8l2J>zby2GQ$);=WS>w(+KnWIq!mpQ>5+6zJNeTu?;LVZ=R2LFv2AU*wtb_lt=Q*Uwfgj!)&@h*6d=y zGc1P*z4-X7x_aiFi$tdWwq0aQRLO19UZNb2Sq)IrZj*5`8uxI>Jf;&;C{D!bf5$Lq zyAHo&`0<0{;pz5wHd>r^bX)DQ=W!CSV<>5i`hR${m~Sq7v$zAy+`r5(U(h6kIo-eZ zitxDWD|*OU$z4mX+;s^rdnGZRnQ#STcIcQ(SExNZbC{fedV3@{Z#Mmjh8+^-iVoa_ z_7*Vt|Kj?gWFa%{FFKKR&9=W#s#e$>L+EQz*jb;vH81A+J3kcMq+Z|*&OU`r7&~je zv1nwaM?GDAS##gfauv|9*UOJy>2&k`uM<>+?+tAEFB$4?sn_+-%$KqC@Z!#dUn8^5 zMNYJ5XA!fKOSzbXMV$59Gx0m0Y%{ISIAZXmq*iCAYB?7-iw z(RYrxI+u_CA|c(H_D40IOYyify^Otp=<9TwPez-B>(sroh2=>fX|DWDqB$d-tC1VA z(>kpA^#@H@3?ZHo4yZ-}WHRXEZsEi7y&f9&aADIq()eyrT|spMBH7(+8j(xMDoYVf71@{(sy2beQXlM@=jYm`CJ_xeMq(r5<_8za{mg0YCQpr}s zme;?8`L~KSm9@Cb)E<|JR${4p&*#tXSD!J%Oi*lZ!YtZAkOMmq)aE zkddE`WJ6MjkoKj`3gUW3mNvU?a}WBB(kAU4u9KYyTZnbbB`=NQN}E~=Pm5XT*mCV z=Phsds@YvCV{+fecx+kI|31bWm7ViMNQB8+lAFVu?z70hQr29`h9vX>(~zt6VmTB0 z0PU~Jnfqx&B78rW3-muCeI?B?W;%RgL_bzA7-*|acU0Xv;^}Wl_o*5GD8;626 zzS9LSJk>thmtYMuIOdk0eKKUeXGwW8I*_&K$K_4KY}6>%zgxCyQ9!mvZ9vo`qHYETjD3DIB(uUfj= zkRAIpkkmTV;l2uHq2ivkalPpWuV3^=O}G8n6S+xm zR<3NW1XFp>S2m+%b7f^y*-O4Q@IH&DW#LqX3w36_rW*>nAH!_5n-I<0LCuCuIXlO9 z!{&>|rD3KL*K?$@S?a|mS-O&)W5-QAcGApfT7-o3e%OjOV;-k)nSx8X9oSzyb30W? zR#(?tc`_ukDIFYACI#`Zv?19ZQS>rO-O=Ok;n5EVQ?f&6Y-QqF*dI{b6vCES^Mq5o z6s5Sy5=xe>HM203JXNc1Him}O^}Jc#q<#|D4ea|Qfk}pNX0zeRM{_prq3+Z)A^w9f z8qnk9BHZkMIwaWlW_4%29e97)?H^A~&F8Od|LrfP#my^U73En#-&CR0vN>E`*mys4Fz96PR4+J70C&Ww{-d2T*CqzfsVbcp|2 z-YnF#wI>~E*`aUlrK9X_E4Xu5qq3%9Sjb+<|50*Js=B7>(@uS+82IDU^srTryW*O%s6yaIe$n42TGRqsy$%MkjMkZ?}4!><=>f`!OHgaaYrbkB> zEaEH6?uvnL;wC=L?5>^x@*=9%o0+{SG=injtbb{ z@bi1mop#S6$f#+b>m9*sU>VutxY&H*;)6&dpWmLe<qnQbR zhHP?(5(CY(#1_1mNfpoduMDr6J}jK({^7o_?%`Y!bd@p&$`~0CT&i_ z8OM8lSgzC3c(T!PTJyeB|Ec)u_^{0R`<1rkp@TYu*PUV_%(!fHP-l2Y`??WLlg-$` zn5p@y)t+mk(ckQ(_b)xbIS)T9oTpSX(^n@7JNKJ~+35+kdzgdSN!2M#H&5%9rezKa zX9up?k|QK*Oz~DuZ8l%}>5OtAv$avA+GK1lp{>*ji!|QwR>S-wpSK}2zgrv5ILpB} zd#AN?o;P`_6g|?nlJhV_v<8qn0ynibLvvDd&XDu}DE)Je!~Z$-4z@O-xk7S!&a^f~ zb5Srm^gML^4!J_Ad;V%|mJ;rJK$=;|-;(X8M&$o~n(AKrdE6O_Xk$+0Vkj1G6X<_h z>wgW!K+aTIA(+rhh)x7YoqPC`$X@o_8CWUNChN!ixQ(BCIzMiUhKjw#PC_6t~i->v5uh!6b%&*o6;a6*s1w*QbcJ1m+2ur(~(FI8@#h)H> zvn!gP9-5VC#H{2Of|_Hu|N7ed88ScjxBr)LevK4c#~*TViMg-1=ZKp@BjnJ<^%>iXfiZEQw{5@w8fZwS)UawM&VprdS$w9p}&>P1{HG@8M$H=S!SL{2cv_vX%Id?d{2dB02aAX3g}4DGb5zJDo`g|mcT`CB zYJ0{wk7`w@HmLvp`RRrpmg!HJjZp@1zmp#FQ zI(F-7IxY&yZQ@3SR9^Q|LP&ueChzEwn03qF3OPC23>XouJDrAyrJvyZ7 Qy7zsl>P|Kz<^|38KP`}{qyPW_ diff --git a/package.json b/package.json index 4574803..f84a5d9 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "date-fns": "^3.6.0", "drizzle-orm": "^0.33.0", "jsonwebtoken": "^9.0.2", + "jszip": "^3.10.1", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "0.1.2", "leaflet.heat": "^0.2.0", diff --git a/src/config/constants/sidebarNavs.tsx b/src/config/constants/sidebarNavs.tsx index 6b95734..27c1834 100644 --- a/src/config/constants/sidebarNavs.tsx +++ b/src/config/constants/sidebarNavs.tsx @@ -1,4 +1,4 @@ -import { Copy, GalleryHorizontal, GalleryVerticalEnd, Image as ImageIcon, MapPin, MapPinX, Rewind, Search, User, Video } from "lucide-react"; +import { Copy, GalleryHorizontal, GalleryVerticalEnd, Image as ImageIcon, MapPin, MapPinX, Rewind, Search, Share2, User, Video } from "lucide-react"; export const sidebarNavs = [ // { @@ -36,6 +36,11 @@ export const sidebarNavs = [ link: "/albums", icon: , }, + { + title: "Import shared", + link: "/assets/import-shared", + icon: , + }, { title: "Geo Heatmap", diff --git a/src/pages/api/albums/potential-albums-assets.ts b/src/pages/api/albums/potential-albums-assets.ts index 75c98c2..01558a2 100644 --- a/src/pages/api/albums/potential-albums-assets.ts +++ b/src/pages/api/albums/potential-albums-assets.ts @@ -27,12 +27,12 @@ const SELECT_ORPHAN_PHOTOS = (date: string, ownerId: string) => asset a LEFT JOIN album_asset aaa - ON a.id = aaa."assetsId" + ON a.id = aaa."assetId" LEFT JOIN asset_exif e ON a.id = e."assetId" WHERE - aaa."albumsId" IS NULL + aaa."albumId" IS NULL AND a."ownerId" = '${ownerId}' AND e."dateTimeOriginal"::date = '${date}' AND a."visibility" = 'timeline' diff --git a/src/pages/api/import-shared.ts b/src/pages/api/import-shared.ts new file mode 100644 index 0000000..d333125 --- /dev/null +++ b/src/pages/api/import-shared.ts @@ -0,0 +1,222 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +interface IAlbumContributorCount { + userId: string; + assetCount: number; +} + +interface ImmichOwner { + name?: string | null; + email?: string | null; +} + +interface ImmichExifInfo { + city?: string | null; + state?: string | null; + country?: string | null; + description?: string | null; + fileSizeInByte?: number | null; +} + +interface ImmichAsset { + id: string; + originalFileName: string; + type: string; + fileCreatedAt?: string | null; + localDateTime?: string | null; + thumbhash?: string | null; + exifInfo?: ImmichExifInfo | null; +} + +interface ImmichAlbumResponse { + albumName: string; + assetCount?: number; + owner?: ImmichOwner | null; + description?: string | null; + startDate?: string | null; + endDate?: string | null; + shared?: boolean; + hasSharedLink?: boolean; + lastModifiedAssetTimestamp?: string | null; + order?: string | null; + contributorCounts?: IAlbumContributorCount[]; + assets: ImmichAsset[]; +} + +interface ImmichSharedLinkResponse { + id: string; + key: string; + type: string; + createdAt: string; + expiresAt?: string | null; + allowUpload?: boolean; + allowDownload?: boolean; + showMetadata?: boolean; + album?: { id: string } | null; +} + +interface IImportSharedAsset { + id: string; + originalFileName: string; + type: string; + fileCreatedAt?: string | null; + localDateTime?: string | null; + description?: string | null; + location?: string | null; + thumbhash?: string | null; + fileSizeInByte?: number | null; +} + +interface IImportSharedAlbum { + albumName: string; + assetCount: number; + owner?: ImmichOwner | null; + description?: string | null; + startDate?: string | null; + endDate?: string | null; + shared?: boolean; + hasSharedLink?: boolean; + lastModifiedAssetTimestamp?: string | null; + order?: string | null; + contributorCounts?: IAlbumContributorCount[]; + assets: IImportSharedAsset[]; +} + +interface IImportSharedResponse { + link: string; + origin: string; + key: string; + sharedLink: { + id: string; + type: string; + createdAt: string; + expiresAt?: string | null; + allowUpload?: boolean; + allowDownload?: boolean; + showMetadata?: boolean; + }; + album: IImportSharedAlbum | null; +} + +const respondWithError = (res: NextApiResponse, status: number, message: string) => { + return res.status(status).json({ error: message }); +}; + +const parseSharedLink = (link: string) => { + const trimmed = link.trim(); + if (!trimmed) { + return null; + } + try { + const url = new URL(trimmed); + const segments = url.pathname.split("/").filter(Boolean); + const key = segments.pop(); + if (!key) { + return null; + } + return { key, origin: url.origin, original: trimmed }; + } catch (_err) { + return null; + } +}; + +const buildLocationString = (exif?: ImmichExifInfo | null) => { + if (!exif) { + return null; + } + const parts = [exif.city, exif.state, exif.country] + .filter((value): value is string => !!value && value.trim().length > 0); + return parts.length > 0 ? parts.join(", ") : null; +}; + +const fetchJson = async (url: string) => { + const response = await fetch(url, { + headers: { accept: "application/json" }, + }); + if (!response.ok) { + throw new Error(`Request failed for ${url} (status ${response.status})`); + } + return (await response.json()) as T; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + res.setHeader("Allow", "POST"); + return respondWithError(res, 405, "Method Not Allowed"); + } + + const payload = typeof req.body === "string" ? JSON.parse(req.body) : req.body; + const link = payload?.link; + + if (!link || typeof link !== "string") { + return respondWithError(res, 400, "A shared album link is required"); + } + + const parsed = parseSharedLink(link); + if (!parsed) { + return respondWithError(res, 400, "Invalid Immich share link"); + } + + try { + const sharedLink = await fetchJson( + `${parsed.origin}/api/shared-links/me?key=${parsed.key}` + ); + + const albumId = sharedLink.album?.id; + let albumResult: IImportSharedAlbum | null = null; + + if (albumId) { + const album = await fetchJson( + `${parsed.origin}/api/albums/${albumId}?key=${parsed.key}&withoutAssets=false` + ); + + const assets = (album.assets || []).map((asset) => ({ + id: asset.id, + originalFileName: asset.originalFileName, + type: asset.type, + fileCreatedAt: asset.fileCreatedAt ?? null, + localDateTime: asset.localDateTime ?? null, + description: asset.exifInfo?.description ?? null, + location: buildLocationString(asset.exifInfo), + thumbhash: asset.thumbhash ?? null, + fileSizeInByte: asset.exifInfo?.fileSizeInByte ?? null, + } satisfies IImportSharedAsset)); + + albumResult = { + albumName: album.albumName, + assetCount: album.assetCount ?? assets.length, + owner: album.owner ?? null, + description: album.description ?? null, + startDate: album.startDate ?? null, + endDate: album.endDate ?? null, + shared: album.shared ?? false, + hasSharedLink: album.hasSharedLink ?? false, + lastModifiedAssetTimestamp: album.lastModifiedAssetTimestamp ?? null, + order: album.order ?? null, + contributorCounts: album.contributorCounts ?? [], + assets, + } satisfies IImportSharedAlbum; + } + + const responseBody: IImportSharedResponse = { + link: parsed.original, + origin: parsed.origin, + key: parsed.key, + sharedLink: { + id: sharedLink.id, + type: sharedLink.type, + createdAt: sharedLink.createdAt, + expiresAt: sharedLink.expiresAt ?? null, + allowUpload: sharedLink.allowUpload ?? false, + allowDownload: sharedLink.allowDownload ?? false, + showMetadata: sharedLink.showMetadata ?? false, + }, + album: albumResult, + }; + + return res.status(200).json(responseBody); + } catch (error: any) { + console.error("Import shared error", error); + return respondWithError(res, 500, error?.message ?? "Failed to import shared album"); + } +} diff --git a/src/pages/api/import-shared/helpers.ts b/src/pages/api/import-shared/helpers.ts new file mode 100644 index 0000000..09aee47 --- /dev/null +++ b/src/pages/api/import-shared/helpers.ts @@ -0,0 +1,63 @@ +import { ENV } from "@/config/environment"; + +export const POWER_TOOLS_TAG_NAME = "immich-power-tools"; + +export type HeadersRecord = Record; + +interface ImmichTagResponse { + id: string; + name: string; +} + +export const ensurePowerToolsTag = async (headers: HeadersRecord) => { + const listResponse = await fetch(`${ENV.IMMICH_URL}/api/tags`, { + headers, + }); + + if (!listResponse.ok) { + throw new Error("Failed to fetch tags from Immich instance"); + } + + const tags = (await listResponse.json()) as ImmichTagResponse[]; + const existing = tags.find((tag) => tag.name === POWER_TOOLS_TAG_NAME); + if (existing) { + return existing; + } + + const createResponse = await fetch(`${ENV.IMMICH_URL}/api/tags`, { + method: "POST", + headers, + body: JSON.stringify({ name: POWER_TOOLS_TAG_NAME }), + }); + + if (!createResponse.ok) { + throw new Error("Failed to create immich-power-tools tag"); + } + + return (await createResponse.json()) as ImmichTagResponse; +}; + +export const tagAssetWithPowerTools = async ( + tagId: string, + assetId: string, + headers: HeadersRecord +) => { + const taggingHeaders: HeadersRecord = { + ...headers, + "Content-Type": "application/json", + Accept: "application/json", + }; + + const tagResponse = await fetch(`${ENV.IMMICH_URL}/api/tags/assets`, { + method: "PUT", + headers: taggingHeaders, + body: JSON.stringify({ tagIds: [tagId], assetIds: [assetId] }), + }); + + if (!tagResponse.ok) { + const errorBody = await tagResponse.text().catch(() => ""); + throw new Error( + `Failed to tag uploaded asset (status ${tagResponse.status}): ${errorBody || "Unknown error"}` + ); + } +}; \ No newline at end of file diff --git a/src/pages/api/import-shared/thumbnail.ts b/src/pages/api/import-shared/thumbnail.ts new file mode 100644 index 0000000..eaed1f8 --- /dev/null +++ b/src/pages/api/import-shared/thumbnail.ts @@ -0,0 +1,87 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +const respondWithError = (res: NextApiResponse, status: number, message: string) => { + return res.status(status).json({ error: message }); +}; + +const ALLOWED_SIZES = new Set(["thumbnail", "preview"]); + +const validateParams = (req: NextApiRequest) => { + const { origin, assetId, key, thumbhash, size } = req.query; + + if (!origin || Array.isArray(origin)) { + return { error: "Query parameter 'origin' is required" }; + } + if (!assetId || Array.isArray(assetId)) { + return { error: "Query parameter 'assetId' is required" }; + } + if (!key || Array.isArray(key)) { + return { error: "Query parameter 'key' is required" }; + } + + let resolvedSize = "thumbnail"; + if (typeof size === "string") { + resolvedSize = size; + } else if (Array.isArray(size)) { + return { error: "Query parameter 'size' must be a single value" }; + } + + if (!ALLOWED_SIZES.has(resolvedSize)) { + return { error: "Query parameter 'size' must be either 'thumbnail' or 'preview'" }; + } + + try { + const parsedOrigin = new URL(origin); + return { + origin: parsedOrigin.origin, + assetId, + key, + size: resolvedSize, + thumbhash: typeof thumbhash === "string" ? thumbhash : undefined, + }; + } catch (_err) { + return { error: "Invalid origin provided" }; + } +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") { + res.setHeader("Allow", "GET"); + return respondWithError(res, 405, "Method Not Allowed"); + } + + const params = validateParams(req); + if ("error" in params) { + return respondWithError(res, 400, params.error); + } + + const search = new URLSearchParams({ key: params.key, size: params.size }); + if (params.thumbhash) { + search.set("c", params.thumbhash); + } + + try { + const response = await fetch( + `${params.origin}/api/assets/${params.assetId}/thumbnail?${search.toString()}`, + { + headers: { + accept: "image/*", + }, + } + ); + + if (!response.ok) { + return respondWithError(res, response.status, "Failed to fetch thumbnail"); + } + + const arrayBuffer = await response.arrayBuffer(); + const contentType = response.headers.get("content-type") ?? "image/jpeg"; + + res.setHeader("Content-Type", contentType); + res.setHeader("Cache-Control", "public, max-age=60"); + return res.status(200).send(Buffer.from(arrayBuffer)); + } catch (error: any) { + console.error("Thumbnail proxy error", error); + return respondWithError(res, 500, error?.message ?? "Thumbnail proxy failed"); + } +} diff --git a/src/pages/api/import-shared/upload-all.ts b/src/pages/api/import-shared/upload-all.ts new file mode 100644 index 0000000..1fae509 --- /dev/null +++ b/src/pages/api/import-shared/upload-all.ts @@ -0,0 +1,518 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { ENV } from "@/config/environment"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; +import { getUserHeaders } from "@/helpers/user.helper"; +import { + HeadersRecord, + ensurePowerToolsTag, + tagAssetWithPowerTools, +} from "./helpers"; + +interface ISharedAssetPayload { + id: string; + originalFileName?: string; + type: string; + fileCreatedAt?: string | null; + localDateTime?: string | null; + duration?: string | null; + isFavorite?: boolean; + isArchived?: boolean; +} + +interface IAlbumImportOptions { + createAlbum: boolean; + albumName?: string; + addToAlbumId?: string; +} + +interface IImportAllPayload { + origin: string; + key: string; + assets: ISharedAssetPayload[]; + albumOptions?: IAlbumImportOptions | null; +} + +interface IFailureDetail { + assetId: string; + reason: string; +} + +interface DownloadedAssetPayload { + buffer: Buffer; + fileName: string; + contentType: string | null; +} + +const DEVICE_ID = "immich-power-tools"; +const ALLOWED_ASSET_TYPES = new Set(["IMAGE", "VIDEO"]); +const CONCURRENT_TRANSFERS = 4; +const makeDeviceAssetId = (assetId: string) => `shared-${assetId}`; + +const respondWithError = (res: NextApiResponse, status: number, message: string) => { + return res.status(status).json({ error: message }); +}; + +const parseBody = (req: NextApiRequest) => { + try { + return typeof req.body === "string" ? JSON.parse(req.body) : req.body; + } catch (_error) { + return null; + } +}; + +const normalizeAlbumOptions = (value: any): IAlbumImportOptions => { + if (!value || typeof value !== "object") { + return { createAlbum: false }; + } + + const createAlbum = Boolean(value.createAlbum); + const addToAlbumId = typeof value.addToAlbumId === "string" && value.addToAlbumId.trim().length > 0 ? value.addToAlbumId.trim() : undefined; + + if (!createAlbum && !addToAlbumId) { + return { createAlbum: false }; + } + + const albumName = + typeof value.albumName === "string" && value.albumName.trim().length > 0 + ? value.albumName.trim() + : undefined; + + return { + createAlbum, + albumName, + addToAlbumId, + }; +}; + +const normalizeAssetPayload = (value: any): ISharedAssetPayload | null => { + if (!value || typeof value !== "object") { + return null; + } + if (!value.id || typeof value.id !== "string") { + return null; + } + if (!value.type || typeof value.type !== "string") { + return null; + } + return { + id: value.id, + originalFileName: typeof value.originalFileName === "string" ? value.originalFileName : undefined, + type: value.type, + fileCreatedAt: value.fileCreatedAt ?? null, + localDateTime: value.localDateTime ?? null, + duration: value.duration ?? null, + isFavorite: value.isFavorite ?? false, + isArchived: value.isArchived ?? false, + }; +}; + +const validatePayload = (payload: any) => { + if (!payload || typeof payload !== "object") { + return { error: "Invalid request payload" }; + } + + const { origin, key, assets } = payload as IImportAllPayload; + const normalizedAlbumOptions = normalizeAlbumOptions((payload as IImportAllPayload).albumOptions); + + if (!origin || typeof origin !== "string") { + return { error: "Field 'origin' is required" }; + } + if (!key || typeof key !== "string") { + return { error: "Field 'key' is required" }; + } + if (!Array.isArray(assets) || assets.length === 0) { + return { error: "Field 'assets' must include at least one entry" }; + } + + let parsedOrigin: URL; + try { + parsedOrigin = new URL(origin.trim()); + } catch (_err) { + return { error: "Invalid origin provided" }; + } + + const normalizedAssets = assets + .map(normalizeAssetPayload) + .filter((asset): asset is ISharedAssetPayload => !!asset); + + if (normalizedAssets.length === 0) { + return { error: "No valid assets provided" }; + } + + return { + origin: parsedOrigin.origin, + key, + assets: normalizedAssets, + albumOptions: normalizedAlbumOptions, + }; +}; + +const inferAssetTypeFromName = (fileName: string): "IMAGE" | "VIDEO" => { + const extension = fileName.split(".").pop()?.toLowerCase(); + if (!extension) { + return "IMAGE"; + } + switch (extension) { + case "mp4": + case "mov": + case "m4v": + case "avi": + case "mkv": + case "webm": + case "3gp": + case "3g2": + case "mts": + case "m2ts": + return "VIDEO"; + default: + return "IMAGE"; + } +}; + +const parseFileNameFromDisposition = (disposition?: string | null) => { + if (!disposition) { + return null; + } + const match = disposition.match(/filename\*?=([^;]+)/i); + if (!match || !match[1]) { + return null; + } + const value = match[1].trim().replace(/^UTF-8''/i, "").replace(/^"|"$/g, "").replace(/^'|'$/g, ""); + try { + return decodeURIComponent(value); + } catch (_error) { + return value; + } +}; + +const guessContentType = (fileName: string) => { + const lower = fileName.toLowerCase(); + const extension = lower.split(".").pop(); + switch (extension) { + case "jpg": + case "jpeg": + case "jfif": + return "image/jpeg"; + case "png": + return "image/png"; + case "heic": + return "image/heic"; + case "heif": + return "image/heif"; + case "webp": + return "image/webp"; + case "gif": + return "image/gif"; + case "tif": + case "tiff": + return "image/tiff"; + case "bmp": + return "image/bmp"; + case "mp4": + case "m4v": + return "video/mp4"; + case "mov": + return "video/quicktime"; + case "avi": + return "video/x-msvideo"; + case "mkv": + return "video/x-matroska"; + case "webm": + return "video/webm"; + case "3gp": + case "3g2": + return "video/3gpp"; + case "mts": + case "m2ts": + return "video/MP2T"; + default: + return "application/octet-stream"; + } +}; + +const createImmichAlbum = async (albumName: string, headers: HeadersRecord) => { + const response = await fetch(`${ENV.IMMICH_URL}/api/albums`, { + method: "POST", + headers, + body: JSON.stringify({ albumName }), + }); + + if (!response.ok) { + const body = await response.text().catch(() => ""); + throw new Error( + `Failed to create album "${albumName}" (status ${response.status}): ${body || "Unknown error"}` + ); + } + + const album = await response.json().catch(() => null); + const albumId = album?.id; + if (!albumId || typeof albumId !== "string") { + throw new Error("Immich album creation response did not include an id"); + } + return albumId; +}; + +const addAssetToAlbum = async (albumId: string, assetId: string, headers: HeadersRecord) => { + const response = await fetch(`${ENV.IMMICH_URL}/api/albums/${albumId}/assets`, { + method: "PUT", + headers, + body: JSON.stringify({ ids: [assetId] }), + }); + + if (!response.ok) { + const body = await response.text().catch(() => ""); + throw new Error( + `Failed to attach asset ${assetId} to album ${albumId} (status ${response.status}): ${ + body || "Unknown error" + }` + ); + } +}; + +const downloadSharedAsset = async ( + asset: ISharedAssetPayload, + origin: string, + key: string +): Promise => { + const url = `${origin}/api/assets/${asset.id}/original?key=${key}`; + const response = await fetch(url, { + method: "GET", + headers: { + accept: "*/*", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to download ${asset.originalFileName ?? asset.id} (status ${response.status})`); + } + + const buffer = Buffer.from(await response.arrayBuffer()); + const disposition = response.headers.get("content-disposition"); + const inferredName = parseFileNameFromDisposition(disposition); + const fallbackExtension = asset.type === "VIDEO" ? "mp4" : "jpg"; + const fileName = inferredName ?? asset.originalFileName ?? `${asset.id}.${fallbackExtension}`; + + return { + buffer, + fileName, + contentType: response.headers.get("content-type"), + }; +}; + +const uploadAssetBuffer = async ( + asset: ISharedAssetPayload, + payload: DownloadedAssetPayload, + uploadHeaders: HeadersRecord, + jsonHeaders: HeadersRecord, + tagId: string +) => { + const fileType = asset.type ?? inferAssetTypeFromName(payload.fileName); + const resolvedFileName = payload.fileName || asset.originalFileName || `${asset.id}.bin`; + const contentType = payload.contentType ?? guessContentType(resolvedFileName); + const blob = new Blob([payload.buffer], { type: contentType }); + const createdAt = asset.fileCreatedAt ?? asset.localDateTime ?? new Date().toISOString(); + const modifiedAt = asset.localDateTime ?? asset.fileCreatedAt ?? createdAt; + + const formData = new FormData(); + formData.set("deviceAssetId", makeDeviceAssetId(asset.id)); + formData.set("deviceId", DEVICE_ID); + formData.set("fileCreatedAt", createdAt); + formData.set("fileModifiedAt", modifiedAt); + formData.set("fileType", fileType); + formData.set("duration", asset.duration ?? "0:00:00.000000"); + formData.set("assetData", blob, resolvedFileName); + + if (asset.isArchived) { + formData.set("isArchived", String(asset.isArchived)); + } + if (asset.isFavorite) { + formData.set("isFavorite", String(asset.isFavorite)); + } + + const uploadResponse = await fetch(`${ENV.IMMICH_URL}/api/assets`, { + method: "POST", + headers: uploadHeaders, + body: formData, + }); + + if (!uploadResponse.ok) { + const body = await uploadResponse.text().catch(() => ""); + throw new Error( + `Upload failed for ${asset.originalFileName ?? asset.id} (status ${uploadResponse.status}): ${ + body || "Unknown error" + }` + ); + } + + const uploaded = await uploadResponse.json(); + const uploadedId = uploaded?.id; + if (!uploadedId) { + throw new Error("Immich upload response did not include an asset id"); + } + + await tagAssetWithPowerTools(tagId, uploadedId, jsonHeaders); + return uploadedId; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + res.setHeader("Allow", "POST"); + return respondWithError(res, 405, "Method Not Allowed"); + } + + if (!ENV.IMMICH_URL) { + return respondWithError(res, 500, "IMMICH_URL must be configured"); + } + + const currentUser = await getCurrentUser(req); + if (!currentUser) { + return respondWithError(res, 401, "Unauthorized"); + } + + const payload = parseBody(req); + const validation = validatePayload(payload); + if ("error" in validation) { + return respondWithError(res, 400, validation.error ?? "Validation failed"); + } + + const { origin, key, assets, albumOptions } = validation; + const uploadableAssets = assets.filter((asset) => ALLOWED_ASSET_TYPES.has(asset.type)); + const normalizedAlbumOptions = albumOptions ?? { createAlbum: false }; + if (uploadableAssets.length === 0) { + return respondWithError(res, 400, "No image or video assets available to import"); + } + + const jsonHeaders = getUserHeaders(currentUser) as HeadersRecord; + const jsonHeadersWithContentType: HeadersRecord = { + ...jsonHeaders, + "Content-Type": "application/json", + }; + + const deviceAssetIds: string[] = []; + const deviceAssetIdLookup = new Map(); + for (const asset of uploadableAssets) { + const deviceAssetId = makeDeviceAssetId(asset.id); + deviceAssetIds.push(deviceAssetId); + deviceAssetIdLookup.set(deviceAssetId.toLowerCase(), asset.id); + } + + const existingAssetIds = new Set(); + if (deviceAssetIds.length > 0) { + try { + const checkResponse = await fetch(`${ENV.IMMICH_URL}/api/assets/exist`, { + method: "POST", + headers: jsonHeadersWithContentType, + body: JSON.stringify({ + deviceAssetIds, + deviceId: DEVICE_ID, + }), + }); + + if (checkResponse.ok) { + const existPayload = await checkResponse.json().catch(() => ({})); + const existingIds: string[] = existPayload?.existingIds ?? []; + for (const deviceAssetId of existingIds) { + if (typeof deviceAssetId !== "string") { + continue; + } + const normalizedId = deviceAssetId.toLowerCase(); + const matchedAssetId = deviceAssetIdLookup.get(normalizedId); + if (matchedAssetId) { + existingAssetIds.add(matchedAssetId); + } + } + } else { + console.warn(`Failed to check existing assets before import (status ${checkResponse.status})`); + } + } catch (error) { + console.warn("Unable to check existing assets before import", error); + } + } + + const assetsToImport = uploadableAssets.filter((asset) => !existingAssetIds.has(asset.id)); + const skippedAssetIds = uploadableAssets + .filter((asset) => existingAssetIds.has(asset.id)) + .map((asset) => asset.id); + + if (assetsToImport.length === 0) { + return res.status(200).json({ + uploadedIds: [], + processedAssetIds: [], + uploadedCount: 0, + failed: [], + requestedCount: 0, + skippedAssetIds, + }); + } + + if (skippedAssetIds.length) { + console.log(`Skipping ${skippedAssetIds.length} assets already stored in Immich`); + } + + let targetAlbumId: string | null = null; + if (normalizedAlbumOptions.createAlbum) { + const desiredAlbumName = + normalizedAlbumOptions.albumName?.trim() || `Shared import ${new Date().toISOString()}`; + try { + targetAlbumId = await createImmichAlbum(desiredAlbumName, jsonHeadersWithContentType); + console.log(`Created Immich album ${targetAlbumId} for shared import (${desiredAlbumName})`); + } catch (error: any) { + console.error("Failed to create Immich album for import", error); + return respondWithError(res, 500, error?.message ?? "Unable to create Immich album"); + } + } else if (normalizedAlbumOptions.addToAlbumId) { + targetAlbumId = normalizedAlbumOptions.addToAlbumId; + } + + const assetIds = assetsToImport.map((asset) => asset.id); + console.log( + `Starting per-asset import for ${assetIds.length} assets (concurrency ${Math.min( + CONCURRENT_TRANSFERS, + assetsToImport.length + )})` + ); + + const { ["Content-Type"]: _omitContentType, ...uploadHeaders } = jsonHeaders; + const tag = await ensurePowerToolsTag(jsonHeaders); + + const uploadedImmichIds: string[] = []; + const processedAssetIds: string[] = []; + const failed: IFailureDetail[] = []; + + let currentIndex = 0; + const concurrency = Math.min(CONCURRENT_TRANSFERS, assetsToImport.length); + + const worker = async () => { + while (true) { + const index = currentIndex++; + if (index >= assetsToImport.length) { + break; + } + + const asset = assetsToImport[index]; + try { + const downloaded = await downloadSharedAsset(asset, origin, key); + const uploadedId = await uploadAssetBuffer(asset, downloaded, uploadHeaders, jsonHeaders, tag.id); + if (targetAlbumId) { + await addAssetToAlbum(targetAlbumId, uploadedId, jsonHeadersWithContentType); + } + uploadedImmichIds.push(uploadedId); + processedAssetIds.push(asset.id); + } catch (error: any) { + failed.push({ assetId: asset.id, reason: error?.message ?? "Upload failed" }); + } + } + }; + + await Promise.all(Array.from({ length: concurrency }, worker)); + + return res.status(200).json({ + uploadedIds: uploadedImmichIds, + processedAssetIds, + uploadedCount: uploadedImmichIds.length, + failed, + requestedCount: assetsToImport.length, + skippedAssetIds, + albumId: targetAlbumId, + }); +} diff --git a/src/pages/api/import-shared/upload.ts b/src/pages/api/import-shared/upload.ts new file mode 100644 index 0000000..a1c16c8 --- /dev/null +++ b/src/pages/api/import-shared/upload.ts @@ -0,0 +1,174 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { ENV } from "@/config/environment"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; +import { getUserHeaders } from "@/helpers/user.helper"; +import { + HeadersRecord, + ensurePowerToolsTag, + tagAssetWithPowerTools, +} from "./helpers"; + +const respondWithError = (res: NextApiResponse, status: number, message: string) => { + return res.status(status).json({ error: message }); +}; + +const parseBody = (req: NextApiRequest) => { + try { + return typeof req.body === "string" ? JSON.parse(req.body) : req.body; + } catch (error) { + return null; + } +}; + +const validatePayload = (payload: any) => { + const { assetId, origin, key } = payload || {}; + if (!assetId || typeof assetId !== "string") { + return { error: "Field 'assetId' is required" }; + } + if (!origin || typeof origin !== "string") { + return { error: "Field 'origin' is required" }; + } + if (!key || typeof key !== "string") { + return { error: "Field 'key' is required" }; + } + + let parsedOrigin: URL | null = null; + try { + parsedOrigin = new URL(origin); + } catch (_err) { + return { error: "Invalid origin provided" }; + } + + return { + assetId, + key, + origin: parsedOrigin.origin, + }; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + res.setHeader("Allow", "POST"); + return respondWithError(res, 405, "Method Not Allowed"); + } + + if (!ENV.IMMICH_URL) { + return respondWithError(res, 500, "IMMICH_URL must be configured"); + } + + const currentUser = await getCurrentUser(req); + if (!currentUser) { + return respondWithError(res, 401, "Unauthorized"); + } + + const payload = parseBody(req); + if (!payload) { + return respondWithError(res, 400, "Invalid request payload"); + } + + const jsonHeaders = getUserHeaders(currentUser) as HeadersRecord; + + const validation = validatePayload(payload); + if ("error" in validation) { + return respondWithError(res, 400, validation.error); + } + + const params = validation; + + try { + const assetResponse = await fetch( + `${params.origin}/api/assets/${params.assetId}?key=${params.key}`, + { + headers: { + accept: "application/json", + }, + } + ); + + if (!assetResponse.ok) { + return respondWithError(res, assetResponse.status, "Failed to fetch remote asset metadata"); + } + + const asset = await assetResponse.json(); + + if (!asset || asset.type !== "IMAGE") { + return respondWithError(res, 400, "Only photo assets can be uploaded from this view"); + } + + const originalResponse = await fetch( + `${params.origin}/api/assets/${params.assetId}/original?key=${params.key}`, + { + headers: { + accept: "application/octet-stream", + }, + } + ); + + if (!originalResponse.ok) { + return respondWithError(res, originalResponse.status, "Failed to download original asset data"); + } + + const contentType = originalResponse.headers.get("content-type") ?? "application/octet-stream"; + const originalBuffer = await originalResponse.arrayBuffer(); + const assetBlob = new Blob([originalBuffer], { type: contentType }); + const fileName = asset.originalFileName ?? `${params.assetId}.bin`; + + const createdAt = asset.fileCreatedAt ?? asset.localDateTime ?? new Date().toISOString(); + const modifiedAt = asset.localDateTime ?? asset.fileCreatedAt ?? createdAt; + + const formData = new FormData(); + formData.set("deviceAssetId", `shared-${params.assetId}`); + formData.set("deviceId", "immich-power-tools"); + formData.set("fileCreatedAt", createdAt); + formData.set("fileModifiedAt", modifiedAt); + formData.set("fileType", asset.type ?? "IMAGE"); + formData.set("duration", asset.duration ?? "0:00:00.000000"); + formData.set("assetData", assetBlob, fileName); + + if (asset.isArchived) { + formData.set("isArchived", asset.isArchived.toString()); + } + if (asset.isFavorite) { + formData.set("isFavorite", asset.isFavorite.toString()); + } + + const { ["Content-Type"]: _omitContentType, ...uploadHeaders } = jsonHeaders; + + const uploadResponse = await fetch(`${ENV.IMMICH_URL}/api/assets`, { + method: "POST", + headers: uploadHeaders, + body: formData, + }); + + if (!uploadResponse.ok) { + const errorBody = await uploadResponse.text(); + console.error("Immich upload error", uploadResponse.status, errorBody); + return respondWithError(res, uploadResponse.status, "Failed to upload asset to Immich instance"); + } + + const uploaded = await uploadResponse.json(); + const uploadedAssetId = uploaded?.id; + + if (!uploadedAssetId) { + throw new Error("Immich upload response did not include an asset id"); + } + + const tagHeaders: HeadersRecord = { + ...jsonHeaders, + Accept: "application/json", + }; + + const tag = await ensurePowerToolsTag(tagHeaders); + await tagAssetWithPowerTools(tag.id, uploadedAssetId, tagHeaders); + + return res.status(200).json({ + status: "uploaded", + immichId: uploadedAssetId, + deviceAssetId: `shared-${params.assetId}`, + tagId: tag.id, + }); + } catch (error: any) { + console.error("Upload shared asset error", error); + return respondWithError(res, 500, error?.message ?? "Unexpected error while uploading asset"); + } +} diff --git a/src/pages/api/import-shared/video.ts b/src/pages/api/import-shared/video.ts new file mode 100644 index 0000000..3682338 --- /dev/null +++ b/src/pages/api/import-shared/video.ts @@ -0,0 +1,95 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { Readable } from "stream"; + +const respondWithError = (res: NextApiResponse, status: number, message: string) => { + return res.status(status).json({ error: message }); +}; + +const validateParams = (req: NextApiRequest) => { + const { origin, assetId, key, thumbhash } = req.query; + + if (!origin || Array.isArray(origin)) { + return { error: "Query parameter 'origin' is required" }; + } + if (!assetId || Array.isArray(assetId)) { + return { error: "Query parameter 'assetId' is required" }; + } + if (!key || Array.isArray(key)) { + return { error: "Query parameter 'key' is required" }; + } + + try { + const parsedOrigin = new URL(origin); + return { + origin: parsedOrigin.origin, + assetId, + key, + thumbhash: typeof thumbhash === "string" ? thumbhash : undefined, + }; + } catch (_err) { + return { error: "Invalid origin provided" }; + } +}; + +const passthroughHeaders = [ + "content-type", + "content-length", + "content-range", + "accept-ranges", + "cache-control", +]; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") { + res.setHeader("Allow", "GET"); + return respondWithError(res, 405, "Method Not Allowed"); + } + + const params = validateParams(req); + if ("error" in params) { + return respondWithError(res, 400, params.error); + } + + const search = new URLSearchParams({ key: params.key }); + if (params.thumbhash) { + search.set("c", params.thumbhash); + } + + const targetUrl = `${params.origin}/api/assets/${params.assetId}/video/playback?${search.toString()}`; + + try { + const upstream = await fetch(targetUrl, { + headers: { + accept: "video/*", + ...(req.headers.range ? { range: req.headers.range } : {}), + referer: req.headers.referer?.toString() ?? params.origin, + "sec-fetch-dest": "video", + }, + }); + + if (!upstream.ok && upstream.status !== 206) { + return respondWithError(res, upstream.status, "Failed to fetch video playback"); + } + + passthroughHeaders.forEach((header) => { + const value = upstream.headers.get(header); + if (value) { + res.setHeader(header, value); + } + }); + + res.status(upstream.status); + + const body = upstream.body; + if (body) { + const nodeStream = Readable.fromWeb(body as any); + nodeStream.pipe(res); + } else { + const buffer = Buffer.from(await upstream.arrayBuffer()); + res.send(buffer); + } + } catch (error: any) { + console.error("Video playback proxy error", error); + return respondWithError(res, 500, error?.message ?? "Video playback proxy failed"); + } +} diff --git a/src/pages/assets/import-shared.tsx b/src/pages/assets/import-shared.tsx new file mode 100644 index 0000000..86843f4 --- /dev/null +++ b/src/pages/assets/import-shared.tsx @@ -0,0 +1,814 @@ +import React, { FormEvent, useEffect, useState } from "react"; +import { Loader2 } from "lucide-react"; +import PageLayout from "@/components/layouts/PageLayout"; +import Header from "@/components/shared/Header"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { listAlbums } from "@/handlers/api/album.handler"; +import { IAlbum } from "@/types/album"; + +interface IAlbumContributorCount { + userId: string; + assetCount: number; +} + +interface IImportSharedAsset { + id: string; + originalFileName: string; + type: string; + fileCreatedAt?: string | null; + localDateTime?: string | null; + description?: string | null; + location?: string | null; + thumbhash?: string | null; + fileSizeInByte?: number | null; + duration?: string | null; + isFavorite?: boolean; + isArchived?: boolean; +} + +interface IImportSharedAlbum { + albumName: string; + assetCount: number; + owner?: { + name?: string | null; + email?: string | null; + } | null; + description?: string | null; + startDate?: string | null; + endDate?: string | null; + shared?: boolean; + hasSharedLink?: boolean; + lastModifiedAssetTimestamp?: string | null; + order?: string | null; + contributorCounts?: IAlbumContributorCount[]; + assets: IImportSharedAsset[]; +} + +interface IImportSharedResponse { + link: string; + origin: string; + key: string; + sharedLink: { + id: string; + type: string; + createdAt: string; + expiresAt?: string | null; + allowUpload?: boolean; + allowDownload?: boolean; + showMetadata?: boolean; + }; + album: IImportSharedAlbum | null; +} + +const formatDate = (value?: string | null) => { + if (!value) { + return "—"; + } + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return value; + } + return date.toLocaleString(); +}; + +const formatDateOnly = (value?: string | null) => { + if (!value) { + return "—"; + } + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return value; + } + return date.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); +}; + +const InfoRow = ({ label, value }: { label: string; value: React.ReactNode }) => ( +
+

{label}

+

{value ?? "—"}

+
+); + +const formatContributors = (counts?: IAlbumContributorCount[]) => { + if (!counts || counts.length === 0) { + return "—"; + } + return counts.map((entry) => `${entry.assetCount} assets`).join(", "); +}; + +const formatPermissions = (link?: IImportSharedResponse["sharedLink"]) => { + if (!link) { + return "View only"; + } + const labels: string[] = []; + if (link.allowDownload) { + labels.push("Download"); + } + if (link.allowUpload) { + labels.push("Upload"); + } + return labels.length ? labels.join(" / ") : "View only"; +}; + +const formatFileSize = (bytes?: number | null) => { + if (!bytes || bytes <= 0) { + return null; + } + const units = ["B", "KB", "MB", "GB", "TB"]; + let size = bytes; + let unit = 0; + while (size >= 1024 && unit < units.length - 1) { + size /= 1024; + unit += 1; + } + return `${size % 1 === 0 ? size : size.toFixed(1)} ${units[unit]}`; +}; + +export default function ImportSharedPage() { + const [shareLink, setShareLink] = useState(""); + const [submittedLink, setSubmittedLink] = useState(null); + const [sharedData, setSharedData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [activeVideoAsset, setActiveVideoAsset] = useState(null); + const [activeImageAsset, setActiveImageAsset] = useState(null); + const [previewLoading, setPreviewLoading] = useState(false); + const [uploadBanner, setUploadBanner] = useState<{ type: "success" | "error"; message: string } | null>(null); + const [importAllLoading, setImportAllLoading] = useState(false); + const [importAllDialogOpen, setImportAllDialogOpen] = useState(false); + const [albumImportMode, setAlbumImportMode] = useState<"album" | "no-album" | "existing-album">("album"); + const [albumNameInput, setAlbumNameInput] = useState(""); + const [selectedAssetIds, setSelectedAssetIds] = useState>(new Set()); + const [existingAlbums, setExistingAlbums] = useState([]); + const [selectedAlbumId, setSelectedAlbumId] = useState(null); + + useEffect(() => { + if (importAllDialogOpen) { + listAlbums().then(setExistingAlbums).catch(console.error); + } + }, [importAllDialogOpen]); + + useEffect(() => { + if (sharedData?.album?.albumName) { + setAlbumNameInput(sharedData.album.albumName); + } else if (sharedData) { + setAlbumNameInput("Imported shared album"); + } else { + setAlbumNameInput(""); + } + }, [sharedData]); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setError(null); + setSharedData(null); + setSubmittedLink(null); + setUploadBanner(null); + setImportAllLoading(false); + setImportAllDialogOpen(false); + setAlbumImportMode("album"); + setAlbumNameInput(""); + setSelectedAssetIds(new Set()); + setSelectedAlbumId(null); + + if (!shareLink.trim()) { + setError("Please paste a shared album link."); + return; + } + + setLoading(true); + try { + const response = await fetch("/api/import-shared", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ link: shareLink }), + }); + + if (!response.ok) { + const errorPayload = await response.json().catch(() => ({})); + throw new Error(errorPayload.error || "Failed to import shared album"); + } + + const payload: IImportSharedResponse = await response.json(); + setSharedData(payload); + setSubmittedLink(payload.link); + setUploadBanner(null); + setImportAllLoading(false); + setAlbumImportMode("album"); + } catch (err) { + const message = err instanceof Error ? err.message : "Unexpected error"; + setError(message); + } finally { + setLoading(false); + } + }; + + const sharedLinkDetails = sharedData?.sharedLink; + const albumDetails = sharedData?.album; + const importableAssetCount = + albumDetails?.assets.filter((asset) => asset.type === "IMAGE" || asset.type === "VIDEO").length ?? 0; + + const buildThumbnailUrl = (asset: IImportSharedAsset) => { + if (!sharedData) { + return null; + } + const params = new URLSearchParams({ + assetId: asset.id, + origin: sharedData.origin, + key: sharedData.key, + size: "thumbnail", + }); + if (asset.thumbhash) { + params.set("thumbhash", asset.thumbhash); + } + return `/api/import-shared/thumbnail?${params.toString()}`; + }; + + const buildVideoPlaybackUrl = (asset: IImportSharedAsset) => { + if (!sharedData) { + return null; + } + const params = new URLSearchParams({ + assetId: asset.id, + origin: sharedData.origin, + key: sharedData.key, + }); + if (asset.thumbhash) { + params.set("thumbhash", asset.thumbhash); + } + return `/api/import-shared/video?${params.toString()}`; + }; + + const buildPreviewUrl = (asset: IImportSharedAsset) => { + if (!sharedData) { + return null; + } + const params = new URLSearchParams({ + assetId: asset.id, + origin: sharedData.origin, + key: sharedData.key, + size: "preview", + }); + if (asset.thumbhash) { + params.set("thumbhash", asset.thumbhash); + } + return `/api/import-shared/thumbnail?${params.toString()}`; + }; + + const handleImportAll = async (options: { createAlbum: boolean; albumName?: string; addToAlbumId?: string }) => { + if (!sharedData || !albumDetails) { + return; + } + let uploadableAssets = albumDetails.assets.filter( + (asset) => asset.type === "IMAGE" || asset.type === "VIDEO" + ); + + if (selectedAssetIds.size > 0) { + uploadableAssets = uploadableAssets.filter((asset) => selectedAssetIds.has(asset.id)); + } + + if (uploadableAssets.length === 0) { + setUploadBanner({ type: "error", message: "No assets selected to import." }); + return; + } + + const normalizedAlbumName = options.createAlbum + ? options.albumName?.trim() || albumDetails.albumName || "Imported shared album" + : undefined; + + const albumOptions = options.createAlbum + ? { createAlbum: true, albumName: normalizedAlbumName } + : options.addToAlbumId + ? { createAlbum: false, addToAlbumId: options.addToAlbumId } + : { createAlbum: false }; + + setUploadBanner(null); + setImportAllLoading(true); + try { + const response = await fetch("/api/import-shared/upload-all", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + origin: sharedData.origin, + key: sharedData.key, + assets: uploadableAssets.map((asset) => ({ + id: asset.id, + originalFileName: asset.originalFileName, + type: asset.type, + fileCreatedAt: asset.fileCreatedAt ?? null, + localDateTime: asset.localDateTime ?? null, + duration: asset.duration ?? null, + isFavorite: asset.isFavorite ?? false, + isArchived: asset.isArchived ?? false, + })), + albumOptions, + }), + }); + + const result = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(result.error || "Failed to import shared archive"); + } + + const processedAssetIds: string[] = result.processedAssetIds ?? []; + const skippedAssetIds: string[] = result.skippedAssetIds ?? []; + const failed: { assetId: string; reason?: string }[] = result.failed ?? []; + + const failedCount = failed.length; + const skippedCount = skippedAssetIds.length; + const uploadedCount = result.uploadedCount ?? processedAssetIds.length; + let bannerType: "success" | "error" = failedCount ? "error" : "success"; + let message: string; + if (!uploadedCount && skippedCount && !failedCount) { + message = `All ${skippedCount} assets already exist on Immich.`; + } else if (failedCount) { + const skippedSuffix = skippedCount ? `, ${skippedCount} skipped` : ""; + message = `Imported ${uploadedCount} assets, ${failedCount} failed${skippedSuffix}.`; + } else if (skippedCount) { + message = `Imported ${uploadedCount} assets, skipped ${skippedCount} already on Immich.`; + } else { + message = `Imported ${uploadedCount} assets successfully.`; + } + if (options.createAlbum && uploadedCount > 0 && normalizedAlbumName) { + message = `${message} Added to album "${normalizedAlbumName}".`; + } else if (options.addToAlbumId && uploadedCount > 0) { + const album = existingAlbums.find((a) => a.id === options.addToAlbumId); + message = `${message} Added to album "${album?.albumName || "existing album"}".`; + } + setUploadBanner({ type: bannerType, message }); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to import shared archive"; + setUploadBanner({ type: "error", message }); + } finally { + setImportAllLoading(false); + } + }; + + return ( + +
{ + setSharedData(null); + setShareLink(""); + setSubmittedLink(null); + setError(null); + setActiveVideoAsset(null); + setActiveImageAsset(null); + setPreviewLoading(false); + setUploadBanner(null); + setImportAllLoading(false); + setImportAllDialogOpen(false); + setAlbumImportMode("album"); + setAlbumNameInput(""); + setSelectedAssetIds(new Set()); + setSelectedAlbumId(null); + }}> + ← Back + + ) : ( + "Import shared" + ) + } + rightComponent={ + sharedData && albumDetails ? ( +
+ {selectedAssetIds.size > 0 && ( + + )} + +
+ ) : null + } + /> +
+
+ {!sharedData && ( + + + Import a shared album + + Paste the shared public link for the album you want to import. + + + +
+
+ + setShareLink(event.target.value)} + placeholder="https://demo.immich.app/share/" + type="url" + required + /> +
+
+ +
+
+ {error && ( +
+ {error} +
+ )} +
+
+ )} + + {sharedLinkDetails && sharedData && albumDetails && ( + + + {albumDetails.albumName} + + + + {albumDetails && ( + <> + + + + + )} + + + )} + + {albumDetails && sharedData && ( + + + Gallery + + + {uploadBanner && ( +
+ {uploadBanner.message} +
+ )} + {albumDetails.description && ( +

{albumDetails.description}

+ )} +
+ {albumDetails.assets.length === 0 && ( +
+ This album has no assets yet. +
+ )} + {albumDetails.assets.map((asset) => { + const formattedSize = formatFileSize(asset.fileSizeInByte); + const thumbnailUrl = buildThumbnailUrl(asset); + const isVideoAsset = asset.type === "VIDEO"; + const isInteractive = true; + const ariaLabel = isVideoAsset + ? `Play video ${asset.originalFileName}` + : `View photo ${asset.originalFileName}`; + const isSelected = selectedAssetIds.has(asset.id); + + return ( +
{ + if (isVideoAsset) { + setActiveImageAsset(null); + setActiveVideoAsset(asset); + setPreviewLoading(false); + } else { + setActiveVideoAsset(null); + setPreviewLoading(true); + setActiveImageAsset(asset); + } + }} + role={isInteractive ? "button" : undefined} + tabIndex={isInteractive ? 0 : undefined} + aria-label={ariaLabel} + onKeyDown={(event) => { + if (!isInteractive) { + return; + } + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + if (isVideoAsset) { + setActiveImageAsset(null); + setActiveVideoAsset(asset); + setPreviewLoading(false); + } else { + setActiveVideoAsset(null); + setPreviewLoading(true); + setActiveImageAsset(asset); + } + } + }} + > +
+
e.stopPropagation()}> + { + const newSelection = new Set(selectedAssetIds); + if (newSelection.has(asset.id)) { + newSelection.delete(asset.id); + } else { + newSelection.add(asset.id); + } + setSelectedAssetIds(newSelection); + }} + className="bg-white/80 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground" + /> +
+ {thumbnailUrl ? ( + {`Thumbnail + ) : ( +
+ No preview +
+ )} + {isVideoAsset && ( +
+
+ ▶ Play +
+
+ )} +
+ + {asset.type} + + {formattedSize && ( + + {formattedSize} + + )} +
+
+
+

+ {asset.originalFileName} +

+

Captured {formatDate(asset.fileCreatedAt || asset.localDateTime)}

+ {asset.location && ( +

{asset.location}

+ )} + {asset.description && ( +

{asset.description}

+ )} +
+
+ ); + })} +
+
+
+ )} +
+
+ { + if (importAllLoading) { + return; + } + setImportAllDialogOpen(open); + }} + > + + + {selectedAssetIds.size > 0 ? "Import selected assets" : "Import all assets"} + + Decide whether to create a new Immich album for these imported assets. + + +
+
+ +
+ + + +
+
+ {albumImportMode === "album" && ( +
+ + setAlbumNameInput(event.target.value)} + placeholder="Imported shared album" + disabled={importAllLoading} + /> +
+ )} + {albumImportMode === "existing-album" && ( +
+ + +
+ )} +
+
+ + +
+
+
+ + { + if (!open) { + setActiveVideoAsset(null); + } + }} + > + + {activeVideoAsset && ( + <> + + {activeVideoAsset.originalFileName} + +
+
+ + )} +
+
+ { + if (!open) { + setActiveImageAsset(null); + setPreviewLoading(false); + } + }} + > + + {activeImageAsset && ( + <> + + {activeImageAsset.originalFileName} + +
+ {activeImageAsset.originalFileName} setPreviewLoading(false)} + onError={() => setPreviewLoading(false)} + /> + {previewLoading && ( +
+ +
+ )} +
+ + )} +
+
+ + ); +} diff --git a/src/schema/albumAssetsAssets.schema.ts b/src/schema/albumAssetsAssets.schema.ts index 5970197..4aabfd5 100644 --- a/src/schema/albumAssetsAssets.schema.ts +++ b/src/schema/albumAssetsAssets.schema.ts @@ -1,7 +1,7 @@ import { pgTable, uuid, timestamp } from 'drizzle-orm/pg-core'; export const albumsAssetsAssets = pgTable('album_asset', { - albumsId: uuid('albumsId').notNull(), - assetsId: uuid('assetsId').notNull(), + albumsId: uuid('albumId').notNull(), + assetsId: uuid('assetId').notNull(), createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(), }); \ No newline at end of file