From f980a2f27a0ae1cb397b93a41c79ee5a88a19762 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 26 Aug 2022 22:53:37 -0700 Subject: [PATCH] Add asset repository and refactor asset service (#540) * build endpoint to get asset count by month * Added asset repository * Added create asset * get asset by device ID * Added test for existing methods * Refactor additional endpoint * Refactor database api to get curated locations and curated objects * Refactor get search properties * Fixed cookies parsing for websocket * Added API to get asset count by time group * Remove unused code --- mobile/openapi/.openapi-generator/FILES | 8 + mobile/openapi/README.md | Bin 7693 -> 8083 bytes mobile/openapi/doc/AssetApi.md | Bin 18550 -> 20214 bytes .../openapi/doc/AssetCountByTimeGroupDto.md | Bin 0 -> 449 bytes .../doc/AssetCountByTimeGroupResponseDto.md | Bin 0 -> 536 bytes .../doc/GetAssetCountByTimeGroupDto.md | Bin 0 -> 453 bytes mobile/openapi/doc/TimeGroupEnum.md | Bin 0 -> 379 bytes mobile/openapi/lib/api.dart | Bin 2748 -> 2943 bytes mobile/openapi/lib/api/asset_api.dart | Bin 23200 -> 25082 bytes mobile/openapi/lib/api_client.dart | Bin 12451 -> 12866 bytes mobile/openapi/lib/api_helper.dart | Bin 3554 -> 3658 bytes .../model/asset_count_by_time_group_dto.dart | Bin 0 -> 3752 bytes ...sset_count_by_time_group_response_dto.dart | Bin 0 -> 3989 bytes .../get_asset_count_by_time_group_dto.dart | Bin 0 -> 3617 bytes mobile/openapi/lib/model/time_group_enum.dart | Bin 0 -> 2617 bytes .../asset_count_by_time_group_dto_test.dart | Bin 0 -> 690 bytes ...count_by_time_group_response_dto_test.dart | Bin 0 -> 770 bytes ...et_asset_count_by_time_group_dto_test.dart | Bin 0 -> 632 bytes mobile/openapi/test/time_group_enum_test.dart | Bin 0 -> 423 bytes .../src/api-v1/asset/asset-repository.ts | 187 ++++++++++++++++++ .../src/api-v1/asset/asset.controller.ts | 17 +- .../immich/src/api-v1/asset/asset.module.ts | 10 +- .../src/api-v1/asset/asset.service.spec.ts | 92 +++++++++ .../immich/src/api-v1/asset/asset.service.ts | 162 +++++---------- .../dto/get-asset-count-by-time-group.dto.ts | 16 ++ .../api-v1/asset/dto/search-properties.dto.ts | 12 ++ .../asset-count-by-time-group-response.dto.ts | 23 +++ .../communication/communication.gateway.ts | 20 +- server/immich-openapi-specs.json | 2 +- web/src/api/open-api/api.ts | 140 ++++++++++++- web/src/lib/stores/websocket.ts | 1 - web/src/routes/+layout.server.ts | 7 +- web/src/routes/photos/+page.svelte | 16 +- 33 files changed, 587 insertions(+), 126 deletions(-) create mode 100644 mobile/openapi/doc/AssetCountByTimeGroupDto.md create mode 100644 mobile/openapi/doc/AssetCountByTimeGroupResponseDto.md create mode 100644 mobile/openapi/doc/GetAssetCountByTimeGroupDto.md create mode 100644 mobile/openapi/doc/TimeGroupEnum.md create mode 100644 mobile/openapi/lib/model/asset_count_by_time_group_dto.dart create mode 100644 mobile/openapi/lib/model/asset_count_by_time_group_response_dto.dart create mode 100644 mobile/openapi/lib/model/get_asset_count_by_time_group_dto.dart create mode 100644 mobile/openapi/lib/model/time_group_enum.dart create mode 100644 mobile/openapi/test/asset_count_by_time_group_dto_test.dart create mode 100644 mobile/openapi/test/asset_count_by_time_group_response_dto_test.dart create mode 100644 mobile/openapi/test/get_asset_count_by_time_group_dto_test.dart create mode 100644 mobile/openapi/test/time_group_enum_test.dart create mode 100644 server/apps/immich/src/api-v1/asset/asset-repository.ts create mode 100644 server/apps/immich/src/api-v1/asset/asset.service.spec.ts create mode 100644 server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-group.dto.ts create mode 100644 server/apps/immich/src/api-v1/asset/dto/search-properties.dto.ts create mode 100644 server/apps/immich/src/api-v1/asset/response-dto/asset-count-by-time-group-response.dto.ts diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 0b891e36af..250e641ccd 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -8,6 +8,8 @@ doc/AdminSignupResponseDto.md doc/AlbumApi.md doc/AlbumResponseDto.md doc/AssetApi.md +doc/AssetCountByTimeGroupDto.md +doc/AssetCountByTimeGroupResponseDto.md doc/AssetFileUploadResponseDto.md doc/AssetResponseDto.md doc/AssetTypeEnum.md @@ -27,6 +29,7 @@ doc/DeviceInfoApi.md doc/DeviceInfoResponseDto.md doc/DeviceTypeEnum.md doc/ExifResponseDto.md +doc/GetAssetCountByTimeGroupDto.md doc/LoginCredentialDto.md doc/LoginResponseDto.md doc/LogoutResponseDto.md @@ -39,6 +42,7 @@ doc/ServerVersionReponseDto.md doc/SignUpDto.md doc/SmartInfoResponseDto.md doc/ThumbnailFormat.md +doc/TimeGroupEnum.md doc/UpdateAlbumDto.md doc/UpdateDeviceInfoDto.md doc/UpdateUserDto.md @@ -66,6 +70,8 @@ lib/model/add_assets_dto.dart lib/model/add_users_dto.dart lib/model/admin_signup_response_dto.dart lib/model/album_response_dto.dart +lib/model/asset_count_by_time_group_dto.dart +lib/model/asset_count_by_time_group_response_dto.dart lib/model/asset_file_upload_response_dto.dart lib/model/asset_response_dto.dart lib/model/asset_type_enum.dart @@ -83,6 +89,7 @@ lib/model/delete_asset_status.dart lib/model/device_info_response_dto.dart lib/model/device_type_enum.dart lib/model/exif_response_dto.dart +lib/model/get_asset_count_by_time_group_dto.dart lib/model/login_credential_dto.dart lib/model/login_response_dto.dart lib/model/logout_response_dto.dart @@ -94,6 +101,7 @@ lib/model/server_version_reponse_dto.dart lib/model/sign_up_dto.dart lib/model/smart_info_response_dto.dart lib/model/thumbnail_format.dart +lib/model/time_group_enum.dart lib/model/update_album_dto.dart lib/model/update_device_info_dto.dart lib/model/update_user_dto.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 03f08f98e3af597639fe428816e6f05dbd73d6c7..f14d52e6d25751b98b2a17ed7dd132678483d628 100644 GIT binary patch delta 350 zcmeCRnQXs7jz_>bzcjDJsWK!pH`To;zqDX-A+LmJVsUY5Nis+#sj>tplMa>%ZT-?q)u++6Pdi8n`5#Zj}Mz}NorAUFX%SFWif&GRa$-qlejY?N zf!B)@CX>H;9q${C0CY#Vl;p>1q~s^->pOy6f<;;{H${_6L02IftAfe*#KbXtwz*O4 n7_*goY6(^?2rF@^*laCjz@!KdDA&BwT&N>a#5O0%tY!oN{!W1n delta 87 zcmbPi-)pl$j%V_89v_yF)S}$U23#VO*K>1BX64<^otTs3lb@Vel9`_e7SH45n*5sA mfeR?^pOlrFTv7}Y-t5PBiDUBy@wLpG7fY)!ZT={`h!Fr3k{)9K diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index dc01b3282e7fc1711700589abf2ed7456463832d..91268af324eb270cc3c1fb6d68e150f35b98e40e 100644 GIT binary patch delta 573 zcmex1f$`g1#tmUi0?zrRc_mJjA(^?U?nU{f1(Oq*Bm|N{f=QJnK*4mdpdyp75Li$* zsZuv3u_RTYMu97O@&$SE$;nJsY`P_>MY+Y37c#9EOfD?~N~h@NSVWaj4;PmX5x z;e?6iZ(hx;FO!RIsg@R(oq}U=acT*=Oi*fZ0nm_Cmy&#i^wbipYBX?305xfHacyoi zzRMVmM~#B5f;$eQfhK5ZS}W-5<5Mx2U)ER?-QhSLHMyTRcyfUf2WH3zsJBMpai*3Q zFwmp5wD7o9ODk3bm-OV1)=HBT-dc2Zsx^+K5473!$8jky@vViFte5Z+;T2_>|c`e*}N@)>Q2GecdyoV-Fx{e^{; z{AEMR%$D+nqxGi|WSj|ou{#y*^eYvy-!HR8V1jiy4xDY`!{Mf`7tgDvU9W0qDntF? o(6cTpt0P4<9w*J=ySv#w{R+EPkyAy5;mompZTTR9Z=@srcSy0NhtgZ=cL z^w7&BdT`p?vPT15Igb(O2?s}u^WOIG9B}YEh#p;picp9wlPlCl(ksZZM|$19=@W>< z;b3$iVSA5(j=B8o ao^N0N*~`fn;EYG!_{)fk;WzVK2=M{RE2~5R literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/GetAssetCountByTimeGroupDto.md b/mobile/openapi/doc/GetAssetCountByTimeGroupDto.md new file mode 100644 index 0000000000000000000000000000000000000000..12dccc261d3c2275d9421213cf58b5fb5b9caa11 GIT binary patch literal 453 zcma)&!D_=W42JK13PBES3{LOcDP2Mttfh?JLLeq~H?!I@wsI)!;b&*v*6y+ygZbr8 z`Xv>BUOVYcWl`x)$k67v(HbX;~0!5gD$BZQX<6FNx>^6 z#9x|mPG4hip6HJA@3~qxSOkBE*b=^A62!ycB3xbo0)c4D(T0OUQvuzVw rC>$rN9m(9YIARd$WQ=_P0Z5KO literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/TimeGroupEnum.md b/mobile/openapi/doc/TimeGroupEnum.md new file mode 100644 index 0000000000000000000000000000000000000000..d037b9f356323c6c3292c3cd3ac6b7c517404f81 GIT binary patch literal 379 zcma)1!HNPg487+o0&{RZwBFs5;vfquvUn3=$~0Pw%Os9E*ceZE#oc!-Z9!d|TolJ4qYX=DsE5kK2c*=07+}Aj=RZ*x*CiHS zTqlRao7R)hdbU{C+S|%<+j|OPimy6yQbWRSjK9~j?Z=C^Ij^E%BfB6^5Sl9$HX~^$64e diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index f83ef32fb08b381abcdfd13b04e7a4a359b7f98a..5c09259062ef7866c54d5ef34777e797edfb64a5 100644 GIT binary patch delta 708 zcmZ3mmGReM#to-cc$4!>^GbA+Ds@v5OHwEE3V52jrq< zatgLMH2~c@*+FvIWLp-m$qUq`PUg{*r@&ta6j`&;{S=g1T#%nvoC>tb-VUcb$n))f z9!*c5W=er0^KA(* diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 262445371e82d5f0e4e89c4f90f6a6fc6ad72da2..6e9050da96ed094cb282e7628066f32183d77bb1 100644 GIT binary patch delta 207 zcmZ3ScqnB9myCdOeraBbQ)NhIZmN4xerdsELs@ZA$KvAD5=l^GJHbA@}#hSm-sw}Rog8L3Y4-0X} zZK)CMzc>m!)n+S*C)^)fUj=G@`!?o!_L>Q{t3i) zYV`D}i`Yx<^FTlQ0O6sK3zGy6$4i+kxrG%otJiAIF+%5}MkE1I@{Ak9^p=2x*jRBR zj1!CMx+KBE2|V*#&f$LfQK>>@m4Ap_b-HbxoYbEB$Ms|Ev+5r^U(>Gh@qraDyvZ2m zTO0t94Zx}!qa^z9fLI90a!5bn8ua0j`2Fs}7ut(VbhmcdouWeh_N|zU!--Vo_UI!- zdsC#aBV7Fy)Pc!4cm^;atbs2qFS&t*R_n^<#4X`;EKs0JV>H2nOkse!ZiI0| z)97)b`2y$IQGhNp#I8vR)hRN96?wKE_aZ3@dUG63#KO zunyo`Qgy_MBpEaIQEwtYmsD6xU?@$gInFh+_`zY#3dDr84B5dg)T95IZw*mJrFg3> zhv(-`rm7-gyNiRp;@M|D2c%+(UCf2u!i;Yf)_%r`45X5$RL*bBosl`rE2BZeZ-d-X z6lh9Fq@bKLg8|~+Q56P5F4Q)vQAweNc;8XD7{ZWct0=quZp5btX^#ui*dEo}3j&(p7tqr)nB%tsjBvV2?t`z7j-GjT5KUTsVT7?Z7y`+YQ&YzVB8W zcM@s(yXHC6*TR(H^e-h{H2t}becN{ySM@gZHla7cO~~qJdh84)4ik-z{h-%axQSPE z{L}aJ5hx+FID9{7II7kYhw0-hageK)^4GJCuq#}mIirhfTUSoKIBHi%FWxIqjc)Q} z6`^(jF@&=Au^W&gi5$dE4F7Dt(MA(hKXCa!#1?g~u3!USaXr=h`Hues(Dh)9w-btz z@D&buI-cp8f|r+diCZ%ccoQ=iKaCsn2jd=9?s`@$za!n6N(T3i=o%MQe{yyGndE-a JTy&7t{szeNzO?`V literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/asset_count_by_time_group_response_dto.dart b/mobile/openapi/lib/model/asset_count_by_time_group_response_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..ddd771f5186f546ba09229184354f6a812cc7072 GIT binary patch literal 3989 zcmds4U2hsW6n*Dc+*DDSXeYxqPb(5=vkj@5wn0kSl~xFi%wWLnU{7sN5lZ>*d#^o) zAt6h&eOPHLr7)iR<$KRLHmz2p)q=}E2L01_z1!a1<)C*3*Y7{}Vz}zVZGQlt`d9C- z|2jf5vV1dP+IG&8S7!}+6f>zbPp3+!Q<3vFl(IB@lJcA{xU_M27K=h@yVrx|OSUp; zQQB1VZ>7-aF4+SAFPO$p%eBGa+U$4Flr)w}n~Di06jQ;qb2odFZgoZl8t8!Q z4-7~2Zhe9q0!(S4=H=Et3S%$!=EvQ2!H08}Z`60LHFg^#jg`u&Uz%B^q0@=_<1akJ zF4{TQC(!AFpGwB^h{}q?n;$>?HxSS0?aP;LU*4m>U(8tF2<#e$7IdDfp87E37Us;% z->M16dR?Lot_jYPXWSU3R|H2y*qj?-oLN-sVG?4Tz%Tx#&%RxM)Vt4C3pvC3{g=qK ztGRasHJ}Y^Njt~2hq&5h=h$Z+HC@I}thn$AVwkLuS0d|T&Ksj7X7qTP3dwRvzu+1I z=8^gR`rP;5n+$ccmbN)Um)i3~Q(ug;@?X7yb~Nz;X=P2g{3-qblN0bVV?bB~-&tOA z15>RQRqUZ#Lds3iph+AZVtYn0$TCM56K0pfI40I0P-s3yv2Z-nWrjdEDN!Ir*|8!^ zQK9B)LXhwa2U+~u)#N5nO~lt4#)0KUu2Lcg78uq6{Gh5iIg=z~$lHeljy%svD{vz{ zX-dseEEv*$&SAj{QU%LUdfdWBPrmY%Aswn5ZcyJ(kU*2#eo#ip(lkYF85Wi!RPEhUQ&$*Z?}hi-Wv7MzhfiDtRG{p)C@! z;O2AxHfdtNDWm^a%R3||IO;fr{p*~lEp(m`NGn{($9YNHLj@JPtS)(2Tt_EcP*m>^ z?K#>6tP#9T`1zB#)t?Sl1;0QQ?MBq(7SpymUUhdu5h9xZ7ILD&_rYtu+UI&bM zE^2Pd+>&Y@YG!vh^0Od%Qwo9-)rqGT&~qXT60Wx>Yz@amu&7F=B^t2q@|>nVtFw9SM~CSI9po_-dj;8e}dWGXMKXq!=JWt+g@ zn(CrB<*&wuVz<$1CMEW_ zdlxuyn5j+j00yv;L+|KBt>d*Q z=t~%V_}l>kx^5VXZNNtoG}krdFN{lJ=eR_Cq$~KLDV=&Zj8dKRcm+Xs`fWnd5sG^d zMJPN>(=G)d)4kY=;d#1%G3uy$X6pYj_Gq%3j@Iz!uB}cDwz?4i2cT@h0Ix<=CE+jJ zFz9YTcO<+yElPYf=785hgY^?FnLmZMX!F0dg7XLR-AGUO-U;N+-Pxay;hb~7(9Rnu Gh<^ht)F5#H literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/get_asset_count_by_time_group_dto.dart b/mobile/openapi/lib/model/get_asset_count_by_time_group_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..1e296d377019d815e5c1c71c40890fcf9b07f5b4 GIT binary patch literal 3617 zcmds4TW{Mo6n^)wI4O!)0Tg-7Q<2PRv&IQ}i-9zI7z{(8B|2g&lNw3YFjD{bokNPU zvUuu{mjN>nTO!Z>eBU868VyDxxcL2UdVD>(ncQC7O)lZ;{ijI`ms7Zz-ofYT<@>8Y z4p59F-{wr)@k#RPWI(UtUMkJgrPAqAl>8KGSsR|GyyPn`Z5-aks#4nS)nMhCZA@C# zHr4#kT4+?4Y=z$y)A(z-HfUU%{py*L#xiMBk)uPg6kI!Xv)5TEBsXcPEbKTFl}3KeFWok@MX+c8IiL%?ELuQpFs2%$gtTzaEfmZY?^Mn)4riqW7~x9tj2pxBhH!zcF1QiKiA8O`NwDCn(#`yf)k5Bb zRsJDz$qsvJKWnXyU$&3)p)2WUR{iSj#E@?gts*-|(;tkIXxKMxDI_Z){fcWah9}|= z+cRG&FEZ1^*2!>=3hkR0(+*r-mR7dZ_2;4vn9RXb(txlAzO%CC29{c_8nZLEgg{-Q zz>q*a!=&di$ht%X5nR^7IQ(|HRB66MVmVIgIzx<^l+c$VMXktEWOJc%>Y88Z!~Po= zjw@+XVjpIxFGd!*APJHfTUZD1m2V2~M3Rgd`=kYtM+q?ubC^g|YmO9S7QZ;GSOu7f zWyn>x(6aCw-xwlpqj;k%hgYXgrYR%=bZ8+fc=nCwfa8&37rC$-Sn!R)@-H~SQ7U;% z75%%p6EcT+WyA*jHHhA-LQ_KGG$=VU7$EK)G-2S3;@UyhMrl3yO9njG^oZPw?T(^d=c%smYoDbX?9z z2@b{%SI=>WjHii9sBF+6pSq+e8JssvvbyGBaT^`&pwR61WE!=$T9TeVp1=uZ?x0I$ zea!8n1Df0PX@c6i*TyX+((=0@$tQu}?TP374U{Ny6?_J|knb2C+|0k%YRwiUj#gN* z((rg2P~h33xv5J_gR0NbZ0<`&+#~DMY>EyJ4}snjVUP`zRb@LMBlJg&7qun?lW1^H zh47ocedE5Fu&XPk)9LAM7BooeS|=t@atX_GKQTI*zW6jxDUe64Xtb=N45LuMQwUGa zikrhtpSq_)UlnPz>}^(v+Sh{yQ0L(2vltGOZeK8(-nO{@jeXnxX*|+g^jEx7Xm4XF z(ia!$e&^3$Y~ZfNxT-gy$qBs)ZbD+uY__wRIDB+k_mie$`DR|xjhL|?4}tQb*Wve3 z=TWugAXWfhX~wvMDSuyD3tQk4(T=W+J-s>h8fc(7X7QeYnskjO!w9=Q=pn@Q67igL zNop^S{w?XzMi*f}_xk_D8FjI(V+U(-OZ6S;&mLe rtpx|Xei@9P*vI@KxPzCwleNkpNw*y?=MQfBb#9>UUyHK=PPe}R(_EVj literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/time_group_enum.dart b/mobile/openapi/lib/model/time_group_enum.dart new file mode 100644 index 0000000000000000000000000000000000000000..b6b443a8b81ec06637d5b7c35bfdd17098e21b1d GIT binary patch literal 2617 zcmai0U2oeq6n*!vxB-U70X%u@(~!<}1H|bvw8^kK4})O{j6}s;B~l})7)Fu*zH>=Q ziBz`@5StS3_qq36&*zi*oG$Lxw?DsM-K_q;Sg$VW>iWZKMwhpAbGxRGx0lyf|C~UK zCEw-3`o-(y*Vhw%R1ex%nHI*Tg(~GaHM(&!Ph}}9sr{^bR#k1RKdK?hy*RkEZhUIx ze~q$G*P_B_EiAshv<}9tJFcD??Yz)FRXH50LP;Cc-BD+y($b}+(GL*wJE7Cxeo3>P zux>JeJ5Qz{Q%fysmEix)WHQN0;T)}1CEr`q)GOUo^kqUcNBY;%w-R2GAT9SK`FgMX zj{HtiZOtR%%4zc`%0_Mz=!JlX_L6k!cgiI}=nPJy^))OsI+wOQDE&a;mIGwj=%|v_ zy|iq#P+FAq$=hBsn0aR&rL_nFBJHI&@i0B3#oK5!i1yciI-|#zD6v;r8+$GewD>9m zdy4m`h@NO5T^WsP9Fq?>tkLHCLAd~cC`+o1bLzg7F+fL3$Fw(G;O%ArM=0ySXDF3} z)87YzZ#VEngg9s65CY)iHpI}H7^p0%u%-$n$q#5t6d|I-#T!Vj54Budp&f>yl6HEQ zZ{A3B3DX)H}+Q^75lpGBYVJEyTJ$LRJogKp>u z_8%jN3MUKFOr1QR9!T;0biTP?y zJwoVblp+o{h`>`OG@-i~vMu4P!fa=fCvMbl z8Ay(?%BX+Ur6W89cBUs-ExOmc_u(Zf>kP8bD*I1hwXmLF)(*}&q(U)O}KhAoGpqe?G!Q=C5} zi!sAu!kAlGVN2x9gIp1o#}2oUuo6_Rz{ZW^wF1MYgt5AQ-f;YaM~0&uG{mwk`?N!x zq3)=x2NP_?Q1(p59L0k46+t*G)od4Uljj}wl0h=zB*is!-eNh7DFO3GE@WP5#p4$G zhC;Y}R>@@{z_I5@w)^9VPx^dbhQkt`A*2iY(D2Wp0PXbjPJh+_Hm>l92PPnP={fzx x`Nuul`S=d|F^h71ptlI$Cm>Zy4{jTB&S<95U@gBfa>q8`NMZTRFmISl{|6wkVX*)J literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/asset_count_by_time_group_dto_test.dart b/mobile/openapi/test/asset_count_by_time_group_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..3012dde35ab516f985d8a419c716f76ba56d45df GIT binary patch literal 690 zcma)&K~IA~5QXpjit$97)S^9^HchNTnvn>2&_G+r;C&#cTTuM4heJ{ez50ym0E zDvhjEnw6r)DRi^nl(^8A%oI8`l5-ipXR zW4CoU`z0YX6uILdaG`ECV1EfLj6u3pour#S6AiAk>e`r8@UBDw4Ht4SgwFuLx=JB= z4Z<&zBg{{l2g6xlEtO*pLKk*Y8jj#Qz(+ii7P105G_e%$Z7oLm3(yv!)pm1DJ-qxV oU-l$!dYSve&S^_$X`Fh>ZePlO6p|o!OaICDs{gSq%V+F@FIsruY5)KL literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/asset_count_by_time_group_response_dto_test.dart b/mobile/openapi/test/asset_count_by_time_group_response_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..1320fb4bbcaae261b4d0ac60bc565339b0077f64 GIT binary patch literal 770 zcma)4UrXFT5P#37m_FSdTutkfXce!`DS{f1^g%?{C7IkU-R!PA6Xhs=_s;HBDD>cb z$R-nhe*^AT0=qw0;tV;J2?*eSF)pyT=bY=+&eM~5JuuM9q)i-nq~r72`q?^fB4by_f2C4yo`PfcSu1pW5vFB@uq`88#kT7 z?*c&By>Z%t$)#F^)4d|o*-0VPRnneLlKEcBWDb7~v!!ti>{;8KmqZM52~qR;0eFoN zqAVox2A=-GK5eBmZCslhB(Ah*>Z;By8jIT literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/get_asset_count_by_time_group_dto_test.dart b/mobile/openapi/test/get_asset_count_by_time_group_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..993c7ce5c9d9f893bc23741a39f2702abb4cb153 GIT binary patch literal 632 zcmaKpPfx-?5XJBP6yvFoAXQEVLLyoqBuW#l2T#kUotBO5Zrzy z`qbg%pM=m+&12v5;*=u#;8GzZlj=(>Clg u0lGYlFx>@ci_lnp8%ulG{I%E|5z_5-*5&a|&Xgu`(seobf%(>*hu{a+q0*E9 literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/time_group_enum_test.dart b/mobile/openapi/test/time_group_enum_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..3c23d323331edd07ac4505521ce19e9b5473b875 GIT binary patch literal 423 zcmZvYO-sW-5QgvjE5=h=h>i9nErNxlrJxN+Ja}4`WRebMKim&Rr2pMrr3YyaJIl*F z@4UNH6s#y<^U~DUyXsIqZ1BW%1qS{H{~H*IgV za$YFUsf&Ts6WU+9n+zLCq2r@gL&W@1D1LXt+oQ0KvA;J$W<>iY23%=_N*F5QS)^ZQ z>kGuA^F=CWAcZFFAg%-&lDM-vm`Vl7J~VY81*zE_z8M1;Qfrna{y&=KaAr>PHDf3C E1H=%IX8-^I literal 0 HcmV?d00001 diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts new file mode 100644 index 0000000000..9424750776 --- /dev/null +++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts @@ -0,0 +1,187 @@ +import { SearchPropertiesDto } from './dto/search-properties.dto'; +import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; +import { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm/repository/Repository'; +import { CreateAssetDto } from './dto/create-asset.dto'; +import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; +import { AssetCountByTimeGroupDto } from './response-dto/asset-count-by-time-group-response.dto'; +import { TimeGroupEnum } from './dto/get-asset-count-by-time-group.dto'; + +export interface IAssetRepository { + create(createAssetDto: CreateAssetDto, ownerId: string, originalPath: string, mimeType: string): Promise; + getAllByUserId(userId: string): Promise; + getAllByDeviceId(userId: string, deviceId: string): Promise; + getById(assetId: string): Promise; + getLocationsByUserId(userId: string): Promise; + getDetectedObjectsByUserId(userId: string): Promise; + getSearchPropertiesByUserId(userId: string): Promise; + getAssetCountByTimeGroup(userId: string, timeGroup: TimeGroupEnum): Promise; +} + +export const ASSET_REPOSITORY = 'ASSET_REPOSITORY'; + +@Injectable() +export class AssetRepository implements IAssetRepository { + constructor( + @InjectRepository(AssetEntity) + private assetRepository: Repository, + ) {} + async getAssetCountByTimeGroup(userId: string, timeGroup: TimeGroupEnum) { + let result: AssetCountByTimeGroupDto[] = []; + + if (timeGroup === TimeGroupEnum.Month) { + result = await this.assetRepository + .createQueryBuilder('asset') + .select(`COUNT(asset.id)::int`, 'count') + .addSelect(`to_char(date_trunc('month', "createdAt"::timestamptz), 'YYYY_MM')`, 'timeGroup') + .where('"userId" = :userId', { userId: userId }) + .groupBy(`date_trunc('month', "createdAt"::timestamptz)`) + .orderBy(`date_trunc('month', "createdAt"::timestamptz)`, 'DESC') + .getRawMany(); + } else if (timeGroup === TimeGroupEnum.Day) { + result = await this.assetRepository + .createQueryBuilder('asset') + .select(`COUNT(asset.id)::int`, 'count') + .addSelect(`to_char(date_trunc('day', "createdAt"::timestamptz), 'YYYY_MM_DD')`, 'timeGroup') + .where('"userId" = :userId', { userId: userId }) + .groupBy(`date_trunc('day', "createdAt"::timestamptz)`) + .orderBy(`date_trunc('day', "createdAt"::timestamptz)`, 'DESC') + .getRawMany(); + } + + return result; + } + + async getSearchPropertiesByUserId(userId: string): Promise { + return await this.assetRepository + .createQueryBuilder('asset') + .where('asset.userId = :userId', { userId: userId }) + .leftJoin('asset.exifInfo', 'ei') + .leftJoin('asset.smartInfo', 'si') + .select('si.tags', 'tags') + .addSelect('si.objects', 'objects') + .addSelect('asset.type', 'assetType') + .addSelect('ei.orientation', 'orientation') + .addSelect('ei."lensModel"', 'lensModel') + .addSelect('ei.make', 'make') + .addSelect('ei.model', 'model') + .addSelect('ei.city', 'city') + .addSelect('ei.state', 'state') + .addSelect('ei.country', 'country') + .distinctOn(['si.tags']) + .getRawMany(); + } + + async getDetectedObjectsByUserId(userId: string): Promise { + return await this.assetRepository.query( + ` + SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId" + FROM assets a + LEFT JOIN smart_info si ON a.id = si."assetId" + WHERE a."userId" = $1 + AND si.objects IS NOT NULL + `, + [userId], + ); + } + + async getLocationsByUserId(userId: string): Promise { + return await this.assetRepository.query( + ` + SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId" + FROM assets a + LEFT JOIN exif e ON a.id = e."assetId" + WHERE a."userId" = $1 + AND e.city IS NOT NULL + AND a.type = 'IMAGE'; + `, + [userId], + ); + } + + /** + * Get a single asset information by its ID + * - include exif info + * @param assetId + */ + async getById(assetId: string): Promise { + return await this.assetRepository.findOneOrFail({ + where: { + id: assetId, + }, + relations: ['exifInfo'], + }); + } + + /** + * Get all assets belong to the user on the database + * @param userId + */ + async getAllByUserId(userId: string): Promise { + const query = this.assetRepository + .createQueryBuilder('asset') + .where('asset.userId = :userId', { userId: userId }) + .andWhere('asset.resizePath is not NULL') + .leftJoinAndSelect('asset.exifInfo', 'exifInfo') + .orderBy('asset.createdAt', 'DESC'); + + return await query.getMany(); + } + + /** + * Create new asset information in database + * @param createAssetDto + * @param ownerId + * @param originalPath + * @param mimeType + * @returns Promise + */ + async create( + createAssetDto: CreateAssetDto, + ownerId: string, + originalPath: string, + mimeType: string, + ): Promise { + const asset = new AssetEntity(); + asset.deviceAssetId = createAssetDto.deviceAssetId; + asset.userId = ownerId; + asset.deviceId = createAssetDto.deviceId; + asset.type = createAssetDto.assetType || AssetType.OTHER; + asset.originalPath = originalPath; + asset.createdAt = createAssetDto.createdAt; + asset.modifiedAt = createAssetDto.modifiedAt; + asset.isFavorite = createAssetDto.isFavorite; + asset.mimeType = mimeType; + asset.duration = createAssetDto.duration || null; + + const createdAsset = await this.assetRepository.save(asset); + + if (!createdAsset) { + throw new BadRequestException('Asset not created'); + } + return createdAsset; + } + + /** + * Get assets by device's Id on the database + * @param userId + * @param deviceId + * + * @returns Promise - Array of assetIds belong to the device + */ + async getAllByDeviceId(userId: string, deviceId: string): Promise { + const rows = await this.assetRepository.find({ + where: { + userId: userId, + deviceId: deviceId, + }, + select: ['deviceAssetId'], + }); + const res: string[] = []; + rows.forEach((v) => res.push(v.deviceAssetId)); + + return res; + } +} diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 18447de8d1..7009f1e11a 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -2,7 +2,6 @@ import { Controller, Post, UseInterceptors, - UploadedFiles, Body, UseGuards, Get, @@ -44,6 +43,8 @@ import { CreateAssetDto } from './dto/create-asset.dto'; import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto'; +import { AssetCountByTimeGroupResponseDto } from './response-dto/asset-count-by-time-group-response.dto'; +import { GetAssetCountByTimeGroupDto } from './dto/get-asset-count-by-time-group.dto'; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @@ -117,17 +118,17 @@ export class AssetController { return this.assetService.getAssetThumbnail(assetId, query); } - @Get('/allObjects') + @Get('/curated-objects') async getCuratedObjects(@GetAuthUser() authUser: AuthUserDto): Promise { return this.assetService.getCuratedObject(authUser); } - @Get('/allLocation') + @Get('/curated-locations') async getCuratedLocations(@GetAuthUser() authUser: AuthUserDto): Promise { return this.assetService.getCuratedLocation(authUser); } - @Get('/searchTerm') + @Get('/search-terms') async getAssetSearchTerms(@GetAuthUser() authUser: AuthUserDto): Promise { return this.assetService.getAssetSearchTerm(authUser); } @@ -140,6 +141,14 @@ export class AssetController { return this.assetService.searchAsset(authUser, searchAssetDto); } + @Get('/count-by-date') + async getAssetCountByTimeGroup( + @GetAuthUser() authUser: AuthUserDto, + @Body(ValidationPipe) getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, + ): Promise { + return this.assetService.getAssetCountByTimeGroup(authUser, getAssetCountByTimeGroupDto); + } + /** * Get all AssetEntity belong to the user */ diff --git a/server/apps/immich/src/api-v1/asset/asset.module.ts b/server/apps/immich/src/api-v1/asset/asset.module.ts index a09831391f..13df9997f9 100644 --- a/server/apps/immich/src/api-v1/asset/asset.module.ts +++ b/server/apps/immich/src/api-v1/asset/asset.module.ts @@ -8,6 +8,7 @@ import { BackgroundTaskModule } from '../../modules/background-task/background-t import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; import { CommunicationModule } from '../communication/communication.module'; import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant'; +import { AssetRepository, ASSET_REPOSITORY } from './asset-repository'; @Module({ imports: [ @@ -24,7 +25,14 @@ import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant'; }), ], controllers: [AssetController], - providers: [AssetService, BackgroundTaskService], + providers: [ + AssetService, + BackgroundTaskService, + { + provide: ASSET_REPOSITORY, + useClass: AssetRepository, + }, + ], exports: [AssetService], }) export class AssetModule {} diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts new file mode 100644 index 0000000000..c16050ea85 --- /dev/null +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -0,0 +1,92 @@ +import { AssetRepository, IAssetRepository } from './asset-repository'; +import { AuthUserDto } from '../../decorators/auth-user.decorator'; +import { AssetService } from './asset.service'; +import { Repository } from 'typeorm'; +import { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; +import { CreateAssetDto } from './dto/create-asset.dto'; + +describe('AssetService', () => { + let sui: AssetService; + let a: Repository; // TO BE DELETED AFTER FINISHED REFACTORING + let assetRepositoryMock: jest.Mocked; + + const authUser: AuthUserDto = Object.freeze({ + id: '3ea54709-e168-42b7-90b0-a0dfe8a7ecbd', + email: 'auth@test.com', + }); + + const _getCreateAssetDto = (): CreateAssetDto => { + const createAssetDto = new CreateAssetDto(); + createAssetDto.deviceAssetId = 'deviceAssetId'; + createAssetDto.deviceId = 'deviceId'; + createAssetDto.assetType = AssetType.OTHER; + createAssetDto.createdAt = '2022-06-19T23:41:36.910Z'; + createAssetDto.modifiedAt = '2022-06-19T23:41:36.910Z'; + createAssetDto.isFavorite = false; + createAssetDto.duration = '0:00:00.000000'; + + return createAssetDto; + }; + const _getAsset = () => { + const assetEntity = new AssetEntity(); + + assetEntity.id = 'e8edabfd-7d8a-45d0-9d61-7c7ca60f2c67'; + assetEntity.userId = '3ea54709-e168-42b7-90b0-a0dfe8a7ecbd'; + assetEntity.deviceAssetId = '4967046344801'; + assetEntity.deviceId = '116766fd-2ef2-52dc-a3ef-149988997291'; + assetEntity.type = AssetType.VIDEO; + assetEntity.originalPath = + 'upload/3ea54709-e168-42b7-90b0-a0dfe8a7ecbd/original/116766fd-2ef2-52dc-a3ef-149988997291/51c97f95-244f-462d-bdf0-e1dc19913516.jpg'; + assetEntity.resizePath = ''; + assetEntity.createdAt = '2022-06-19T23:41:36.910Z'; + assetEntity.modifiedAt = '2022-06-19T23:41:36.910Z'; + assetEntity.isFavorite = false; + assetEntity.mimeType = 'image/jpeg'; + assetEntity.webpPath = ''; + assetEntity.encodedVideoPath = ''; + assetEntity.duration = '0:00:00.000000'; + + return assetEntity; + }; + + beforeAll(() => { + assetRepositoryMock = { + create: jest.fn(), + getAllByUserId: jest.fn(), + getAllByDeviceId: jest.fn(), + getAssetCountByTimeGroup: jest.fn(), + getById: jest.fn(), + getDetectedObjectsByUserId: jest.fn(), + getLocationsByUserId: jest.fn(), + getSearchPropertiesByUserId: jest.fn(), + }; + + sui = new AssetService(assetRepositoryMock, a); + }); + + it('create an asset', async () => { + const assetEntity = _getAsset(); + + assetRepositoryMock.create.mockImplementation(() => Promise.resolve(assetEntity)); + + const originalPath = + 'upload/3ea54709-e168-42b7-90b0-a0dfe8a7ecbd/original/116766fd-2ef2-52dc-a3ef-149988997291/51c97f95-244f-462d-bdf0-e1dc19913516.jpg'; + const mimeType = 'image/jpeg'; + const createAssetDto = _getCreateAssetDto(); + const result = await sui.createUserAsset(authUser, createAssetDto, originalPath, mimeType); + + expect(result.userId).toEqual(authUser.id); + expect(result.resizePath).toEqual(''); + expect(result.webpPath).toEqual(''); + }); + + it('get assets by device id', async () => { + assetRepositoryMock.getAllByDeviceId.mockImplementation(() => Promise.resolve(['4967046344801'])); + + const deviceId = '116766fd-2ef2-52dc-a3ef-149988997291'; + const result = await sui.getUserAssetsByDeviceId(authUser, deviceId); + + expect(result.length).toEqual(1); + expect(result[0]).toEqual('4967046344801'); + }); +}); diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 9156950a5e..4d32212671 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -1,5 +1,7 @@ +import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; import { BadRequestException, + Inject, Injectable, InternalServerErrorException, Logger, @@ -7,7 +9,7 @@ import { StreamableFile, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { IsNull, Not, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; import { constants, createReadStream, ReadStream, stat } from 'fs'; @@ -25,83 +27,49 @@ import { CreateAssetDto } from './dto/create-asset.dto'; import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto'; import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto'; +import { ASSET_REPOSITORY, IAssetRepository } from './asset-repository'; +import { SearchPropertiesDto } from './dto/search-properties.dto'; +import { + AssetCountByTimeGroupResponseDto, + mapAssetCountByTimeGroupResponse, +} from './response-dto/asset-count-by-time-group-response.dto'; +import { GetAssetCountByTimeGroupDto } from './dto/get-asset-count-by-time-group.dto'; const fileInfo = promisify(stat); @Injectable() export class AssetService { constructor( + @Inject(ASSET_REPOSITORY) + private _assetRepository: IAssetRepository, + @InjectRepository(AssetEntity) private assetRepository: Repository, ) {} - public async updateThumbnailInfo(asset: AssetEntity, thumbnailPath: string): Promise { - const updatedAsset = await this.assetRepository - .createQueryBuilder('assets') - .update(AssetEntity, { ...asset, resizePath: thumbnailPath }) - .where('assets.id = :id', { id: asset.id }) - .returning('*') - .updateEntity(true) - .execute(); - - return updatedAsset.raw[0]; - } - public async createUserAsset( authUser: AuthUserDto, - assetInfo: CreateAssetDto, - path: string, + createAssetDto: CreateAssetDto, + originalPath: string, mimeType: string, - ): Promise { - const asset = new AssetEntity(); - asset.deviceAssetId = assetInfo.deviceAssetId; - asset.userId = authUser.id; - asset.deviceId = assetInfo.deviceId; - asset.type = assetInfo.assetType || AssetType.OTHER; - asset.originalPath = path; - asset.createdAt = assetInfo.createdAt; - asset.modifiedAt = assetInfo.modifiedAt; - asset.isFavorite = assetInfo.isFavorite; - asset.mimeType = mimeType; - asset.duration = assetInfo.duration || null; + ): Promise { + const assetEntity = await this._assetRepository.create(createAssetDto, authUser.id, originalPath, mimeType); - const createdAsset = await this.assetRepository.save(asset); - if (!createdAsset) { - throw new Error('Asset not created'); - } - return createdAsset; + return assetEntity; } public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { - const rows = await this.assetRepository.find({ - where: { - userId: authUser.id, - deviceId: deviceId, - }, - select: ['deviceAssetId'], - }); - - const res: string[] = []; - rows.forEach((v) => res.push(v.deviceAssetId)); - return res; + return this._assetRepository.getAllByDeviceId(authUser.id, deviceId); } public async getAllAssets(authUser: AuthUserDto): Promise { - const assets = await this.assetRepository.find({ - where: { - userId: authUser.id, - resizePath: Not(IsNull()), - }, - relations: ['exifInfo'], - order: { - createdAt: 'DESC', - }, - }); + const assets = await this._assetRepository.getAllByUserId(authUser.id); return assets.map((asset) => mapAsset(asset)); } - public async findAssetOfDevice(deviceId: string, assetId: string): Promise { + // TODO - Refactor this to get asset by its own id + private async findAssetOfDevice(deviceId: string, assetId: string): Promise { const rows = await this.assetRepository.query( 'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."deviceId" = $2', [assetId, deviceId], @@ -117,16 +85,7 @@ export class AssetService { } public async getAssetById(authUser: AuthUserDto, assetId: string): Promise { - const asset = await this.assetRepository.findOne({ - where: { - id: assetId, - }, - relations: ['exifInfo'], - }); - - if (!asset) { - throw new NotFoundException('Asset not found'); - } + const asset = await this._assetRepository.getById(assetId); return mapAsset(asset); } @@ -394,45 +353,35 @@ export class AssetService { async getAssetSearchTerm(authUser: AuthUserDto): Promise { const possibleSearchTerm = new Set(); - // TODO: should use query builder - const rows = await this.assetRepository.query( - ` - SELECT DISTINCT si.tags, si.objects, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country - FROM assets a - LEFT JOIN exif e ON a.id = e."assetId" - LEFT JOIN smart_info si ON a.id = si."assetId" - WHERE a."userId" = $1; - `, - [authUser.id], - ); - rows.forEach((row: { [x: string]: any }) => { + const rows = await this._assetRepository.getSearchPropertiesByUserId(authUser.id); + rows.forEach((row: SearchPropertiesDto) => { // tags - row['tags']?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase())); + row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase())); // objects - row['objects']?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase())); + row.objects?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase())); // asset's tyoe - possibleSearchTerm.add(row['type']?.toLowerCase()); + possibleSearchTerm.add(row.assetType?.toLowerCase() || ''); // image orientation - possibleSearchTerm.add(row['orientation']?.toLowerCase()); + possibleSearchTerm.add(row.orientation?.toLowerCase() || ''); // Lens model - possibleSearchTerm.add(row['lensModel']?.toLowerCase()); + possibleSearchTerm.add(row.lensModel?.toLowerCase() || ''); // Make and model - possibleSearchTerm.add(row['make']?.toLowerCase()); - possibleSearchTerm.add(row['model']?.toLowerCase()); + possibleSearchTerm.add(row.make?.toLowerCase() || ''); + possibleSearchTerm.add(row.model?.toLowerCase() || ''); // Location - possibleSearchTerm.add(row['city']?.toLowerCase()); - possibleSearchTerm.add(row['state']?.toLowerCase()); - possibleSearchTerm.add(row['country']?.toLowerCase()); + possibleSearchTerm.add(row.city?.toLowerCase() || ''); + possibleSearchTerm.add(row.state?.toLowerCase() || ''); + possibleSearchTerm.add(row.country?.toLowerCase() || ''); }); - return Array.from(possibleSearchTerm).filter((x) => x != null); + return Array.from(possibleSearchTerm).filter((x) => x != null && x != ''); } async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto): Promise { @@ -459,33 +408,12 @@ export class AssetService { return searchResults.map((asset) => mapAsset(asset)); } - async getCuratedLocation(authUser: AuthUserDto) { - return await this.assetRepository.query( - ` - SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId" - FROM assets a - LEFT JOIN exif e ON a.id = e."assetId" - WHERE a."userId" = $1 - AND e.city IS NOT NULL - AND a.type = 'IMAGE'; - `, - [authUser.id], - ); + async getCuratedLocation(authUser: AuthUserDto): Promise { + return this._assetRepository.getLocationsByUserId(authUser.id); } async getCuratedObject(authUser: AuthUserDto): Promise { - const curatedObjects: CuratedObjectsResponseDto[] = await this.assetRepository.query( - ` - SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId" - FROM assets a - LEFT JOIN smart_info si ON a.id = si."assetId" - WHERE a."userId" = $1 - AND si.objects IS NOT NULL - `, - [authUser.id], - ); - - return curatedObjects; + return this._assetRepository.getDetectedObjectsByUserId(authUser.id); } async checkDuplicatedAsset( @@ -504,4 +432,16 @@ export class AssetService { return new CheckDuplicateAssetResponseDto(isDuplicated, res?.id); } + + async getAssetCountByTimeGroup( + authUser: AuthUserDto, + getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, + ): Promise { + const result = await this._assetRepository.getAssetCountByTimeGroup( + authUser.id, + getAssetCountByTimeGroupDto.timeGroup, + ); + + return mapAssetCountByTimeGroupResponse(result); + } } diff --git a/server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-group.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-group.dto.ts new file mode 100644 index 0000000000..2862d3c945 --- /dev/null +++ b/server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-group.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; + +export enum TimeGroupEnum { + Day = 'day', + Month = 'month', +} +export class GetAssetCountByTimeGroupDto { + @IsNotEmpty() + @ApiProperty({ + type: String, + enum: TimeGroupEnum, + enumName: 'TimeGroupEnum', + }) + timeGroup!: TimeGroupEnum; +} diff --git a/server/apps/immich/src/api-v1/asset/dto/search-properties.dto.ts b/server/apps/immich/src/api-v1/asset/dto/search-properties.dto.ts new file mode 100644 index 0000000000..669b29b2e3 --- /dev/null +++ b/server/apps/immich/src/api-v1/asset/dto/search-properties.dto.ts @@ -0,0 +1,12 @@ +export class SearchPropertiesDto { + tags?: string[]; + objects?: string[]; + assetType?: string; + orientation?: string; + lensModel?: string; + make?: string; + model?: string; + city?: string; + state?: string; + country?: string; +} diff --git a/server/apps/immich/src/api-v1/asset/response-dto/asset-count-by-time-group-response.dto.ts b/server/apps/immich/src/api-v1/asset/response-dto/asset-count-by-time-group-response.dto.ts new file mode 100644 index 0000000000..2d1d60974e --- /dev/null +++ b/server/apps/immich/src/api-v1/asset/response-dto/asset-count-by-time-group-response.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AssetCountByTimeGroupDto { + @ApiProperty({ type: 'string' }) + timeGroup!: string; + + @ApiProperty({ type: 'integer' }) + count!: number; +} + +export class AssetCountByTimeGroupResponseDto { + groups!: AssetCountByTimeGroupDto[]; + + @ApiProperty({ type: 'integer' }) + totalAssets!: number; +} + +export function mapAssetCountByTimeGroupResponse(result: AssetCountByTimeGroupDto[]): AssetCountByTimeGroupResponseDto { + return { + groups: result, + totalAssets: result.map((group) => group.count).reduce((a, b) => a + b, 0), + }; +} diff --git a/server/apps/immich/src/api-v1/communication/communication.gateway.ts b/server/apps/immich/src/api-v1/communication/communication.gateway.ts index ba36bbcf7d..8cb7928b9e 100644 --- a/server/apps/immich/src/api-v1/communication/communication.gateway.ts +++ b/server/apps/immich/src/api-v1/communication/communication.gateway.ts @@ -5,7 +5,7 @@ import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { UserEntity } from '@app/database/entities/user.entity'; import { Repository } from 'typeorm'; - +import cookieParser from 'cookie'; @WebSocketGateway({ cors: true }) export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect { constructor( @@ -26,8 +26,24 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco async handleConnection(client: Socket) { try { Logger.log(`New websocket connection: ${client.id}`, 'WebsocketConnectionEvent'); + let accessToken = ''; - const accessToken = client.handshake.headers.authorization?.split(' ')[1]; + if (client.handshake.headers.cookie != undefined) { + const cookies = cookieParser.parse(client.handshake.headers.cookie); + if (cookies.immich_access_token) { + accessToken = cookies.immich_access_token; + } else { + client.emit('error', 'unauthorized'); + client.disconnect(); + return; + } + } else if (client.handshake.headers.authorization != undefined) { + accessToken = client.handshake.headers.authorization.split(' ')[1]; + } else { + client.emit('error', 'unauthorized'); + client.disconnect(); + return; + } const res: JwtValidationResult = accessToken ? await this.immichJwtService.validateToken(accessToken) diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 9cc6f3f468..4d4bf4dad6 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/allObjects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/allLocation":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/searchTerm":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"string","nullable":true,"default":null},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"fileSizeInByte":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer"},"diskUseRaw":{"type":"integer"},"diskAvailableRaw":{"type":"integer"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-date":{"get":{"operationId":"getAssetCountByTimeGroup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeGroupDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeGroupResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"string","nullable":true,"default":null},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"fileSizeInByte":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeGroupDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeGroupDto":{"type":"object","properties":{"timeGroup":{"type":"string"},"count":{"type":"integer"}},"required":["timeGroup","count"]},"AssetCountByTimeGroupResponseDto":{"type":"object","properties":{"totalAssets":{"type":"integer"},"groups":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeGroupDto"}}},"required":["totalAssets","groups"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer"},"diskUseRaw":{"type":"integer"},"diskAvailableRaw":{"type":"integer"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}}}}} \ No newline at end of file diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 79ea7f4ad8..2ccc6010c4 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -145,6 +145,44 @@ export interface AlbumResponseDto { */ 'assets': Array; } +/** + * + * @export + * @interface AssetCountByTimeGroupDto + */ +export interface AssetCountByTimeGroupDto { + /** + * + * @type {string} + * @memberof AssetCountByTimeGroupDto + */ + 'timeGroup': string; + /** + * + * @type {number} + * @memberof AssetCountByTimeGroupDto + */ + 'count': number; +} +/** + * + * @export + * @interface AssetCountByTimeGroupResponseDto + */ +export interface AssetCountByTimeGroupResponseDto { + /** + * + * @type {number} + * @memberof AssetCountByTimeGroupResponseDto + */ + 'totalAssets': number; + /** + * + * @type {Array} + * @memberof AssetCountByTimeGroupResponseDto + */ + 'groups': Array; +} /** * * @export @@ -720,6 +758,19 @@ export interface ExifResponseDto { */ 'country'?: string | null; } +/** + * + * @export + * @interface GetAssetCountByTimeGroupDto + */ +export interface GetAssetCountByTimeGroupDto { + /** + * + * @type {TimeGroupEnum} + * @memberof GetAssetCountByTimeGroupDto + */ + 'timeGroup': TimeGroupEnum; +} /** * * @export @@ -996,6 +1047,20 @@ export const ThumbnailFormat = { export type ThumbnailFormat = typeof ThumbnailFormat[keyof typeof ThumbnailFormat]; +/** + * + * @export + * @enum {string} + */ + +export const TimeGroupEnum = { + Day: 'day', + Month: 'month' +} as const; + +export type TimeGroupEnum = typeof TimeGroupEnum[keyof typeof TimeGroupEnum]; + + /** * * @export @@ -2072,13 +2137,52 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * + * @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAssetCountByTimeGroup: async (getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getAssetCountByTimeGroupDto' is not null or undefined + assertParamExists('getAssetCountByTimeGroup', 'getAssetCountByTimeGroupDto', getAssetCountByTimeGroupDto) + const localVarPath = `/asset/count-by-date`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getAssetCountByTimeGroupDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {*} [options] Override http request option. * @throws {RequiredError} */ getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/asset/searchTerm`; + const localVarPath = `/asset/search-terms`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -2153,7 +2257,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @throws {RequiredError} */ getCuratedLocations: async (options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/asset/allLocation`; + const localVarPath = `/asset/curated-locations`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -2186,7 +2290,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @throws {RequiredError} */ getCuratedObjects: async (options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/asset/allObjects`; + const localVarPath = `/asset/curated-objects`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -2456,6 +2560,16 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetById(assetId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {*} [options] Override http request option. @@ -2598,6 +2712,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getAssetById(assetId: string, options?: any): AxiosPromise { return localVarFp.getAssetById(assetId, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: any): AxiosPromise { + return localVarFp.getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {*} [options] Override http request option. @@ -2742,6 +2865,17 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getAssetById(assetId, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AssetApi + */ + public getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {*} [options] Override http request option. diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index 569ed5673d..db2bb6472b 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -1,5 +1,4 @@ import { Socket, io } from 'socket.io-client'; -import { writable } from 'svelte/store'; let websocket: Socket; diff --git a/web/src/routes/+layout.server.ts b/web/src/routes/+layout.server.ts index ad51683594..d3ae614590 100644 --- a/web/src/routes/+layout.server.ts +++ b/web/src/routes/+layout.server.ts @@ -1,4 +1,4 @@ -import { serverApi } from '@api'; +import { serverApi, TimeGroupEnum } from '@api'; import * as cookieParser from 'cookie'; import type { LayoutServerLoad } from './$types'; @@ -21,6 +21,9 @@ export const load: LayoutServerLoad = async ({ request }) => { user: userInfo }; } catch (e) { - console.log('[ERROR] layout.server.ts [LayoutServerLoad]: ', e); + console.error('[ERROR] layout.server.ts [LayoutServerLoad]: ', e); + return { + user: undefined + }; } }; diff --git a/web/src/routes/photos/+page.svelte b/web/src/routes/photos/+page.svelte index 38bf463e52..aca0447edc 100644 --- a/web/src/routes/photos/+page.svelte +++ b/web/src/routes/photos/+page.svelte @@ -20,11 +20,13 @@ import Close from 'svelte-material-icons/Close.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import type { PageData } from './$types'; - import { onMount } from 'svelte'; + + import { onMount, onDestroy } from 'svelte'; import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; + import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket'; export let data: PageData; @@ -193,6 +195,18 @@ console.error('Error deleteSelectedAssetHandler', e); } }; + + onMount(async () => { + openWebsocketConnection(); + + const { data: assets } = await api.assetApi.getAllAssets(); + + setAssetInfo(assets); + }); + + onDestroy(() => { + closeWebsocketConnection(); + });