From 5de8ea162d8f04fb356147294202c2bd37f7dc2e Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 5 Dec 2022 11:56:44 -0600 Subject: [PATCH] feat(server) Tagging system (#1046) --- .gitattributes | 13 + Makefile | 3 + NOTES.md | 9 - mobile/openapi/.openapi-generator/FILES | 20 + mobile/openapi/README.md | Bin 10929 -> 11613 bytes mobile/openapi/doc/AssetApi.md | Bin 27832 -> 27798 bytes mobile/openapi/doc/AssetEntity.md | Bin 0 -> 1169 bytes mobile/openapi/doc/AssetResponseDto.md | Bin 1089 -> 1173 bytes mobile/openapi/doc/CreateTagDto.md | Bin 0 -> 457 bytes mobile/openapi/doc/ExifEntity.md | Bin 0 -> 1282 bytes mobile/openapi/doc/SmartInfoEntity.md | Bin 0 -> 614 bytes mobile/openapi/doc/TagApi.md | Bin 0 -> 4696 bytes mobile/openapi/doc/TagEntity.md | Bin 0 -> 651 bytes mobile/openapi/doc/TagResponseDto.md | Bin 0 -> 485 bytes mobile/openapi/doc/TagTypeEnum.md | Bin 0 -> 377 bytes mobile/openapi/doc/UpdateAssetDto.md | Bin 412 -> 491 bytes mobile/openapi/doc/UpdateTagDto.md | Bin 0 -> 463 bytes mobile/openapi/doc/UserEntity.md | Bin 0 -> 888 bytes mobile/openapi/lib/api.dart | Bin 3901 -> 4224 bytes mobile/openapi/lib/api/asset_api.dart | Bin 33993 -> 33971 bytes mobile/openapi/lib/api/tag_api.dart | Bin 0 -> 8035 bytes mobile/openapi/lib/api_client.dart | Bin 14781 -> 15472 bytes mobile/openapi/lib/api_helper.dart | Bin 3952 -> 4052 bytes .../openapi/lib/model/album_response_dto.dart | Bin 5774 -> 5619 bytes mobile/openapi/lib/model/asset_entity.dart | Bin 0 -> 12382 bytes .../openapi/lib/model/asset_response_dto.dart | Bin 8947 -> 9164 bytes mobile/openapi/lib/model/create_tag_dto.dart | Bin 0 -> 3449 bytes mobile/openapi/lib/model/exif_entity.dart | Bin 0 -> 12600 bytes .../openapi/lib/model/smart_info_entity.dart | Bin 0 -> 4911 bytes mobile/openapi/lib/model/tag_entity.dart | Bin 0 -> 7065 bytes .../openapi/lib/model/tag_response_dto.dart | Bin 0 -> 3661 bytes mobile/openapi/lib/model/tag_type_enum.dart | Bin 0 -> 2692 bytes .../openapi/lib/model/update_asset_dto.dart | Bin 3358 -> 4065 bytes mobile/openapi/lib/model/update_tag_dto.dart | Bin 0 -> 4384 bytes mobile/openapi/lib/model/user_entity.dart | Bin 0 -> 7537 bytes mobile/openapi/test/asset_entity_test.dart | Bin 0 -> 2556 bytes mobile/openapi/test/create_tag_dto_test.dart | Bin 0 -> 650 bytes mobile/openapi/test/exif_entity_test.dart | Bin 0 -> 3063 bytes .../openapi/test/smart_info_entity_test.dart | Bin 0 -> 1012 bytes mobile/openapi/test/tag_api_test.dart | Bin 0 -> 1011 bytes mobile/openapi/test/tag_entity_test.dart | Bin 0 -> 1063 bytes .../openapi/test/tag_response_dto_test.dart | Bin 0 -> 745 bytes mobile/openapi/test/tag_type_enum_test.dart | Bin 0 -> 419 bytes mobile/openapi/test/update_tag_dto_test.dart | Bin 0 -> 659 bytes mobile/openapi/test/user_entity_test.dart | Bin 0 -> 1708 bytes .../immich/src/api-v1/album/album.module.ts | 29 +- .../src/api-v1/asset/asset-repository.ts | 26 +- .../src/api-v1/asset/asset.controller.ts | 8 +- .../immich/src/api-v1/asset/asset.module.ts | 36 +- .../immich/src/api-v1/asset/asset.service.ts | 4 +- .../src/api-v1/asset/dto/update-asset.dto.ts | 22 +- .../asset/response-dto/asset-response.dto.ts | 3 + .../apps/immich/src/api-v1/job/job.module.ts | 20 +- .../src/api-v1/tag/dto/create-tag.dto.ts | 14 + .../src/api-v1/tag/dto/update-tag.dto.ts | 11 + .../tag/response-dto/tag-response.dto.ts | 20 + .../immich/src/api-v1/tag/tag.controller.ts | 44 + .../apps/immich/src/api-v1/tag/tag.module.ts | 18 + .../immich/src/api-v1/tag/tag.repository.ts | 61 + .../immich/src/api-v1/tag/tag.service.spec.ts | 91 + .../apps/immich/src/api-v1/tag/tag.service.ts | 48 + .../src/api-v1/user/user.service.spec.ts | 3 + server/apps/immich/src/app.module.ts | 3 + server/apps/immich/src/main.ts | 2 +- .../immich-jwt/immich-jwt.service.spec.ts | 1 + server/immich-openapi-specs.json | 3976 ++++++++++++++++- .../database/src/entities/asset.entity.ts | 8 +- .../libs/database/src/entities/tag.entity.ts | 45 + .../libs/database/src/entities/user.entity.ts | 6 +- .../1670257571385-CreateTagsTable.ts | 26 + web/src/api/open-api/api.ts | 943 +++- .../asset-viewer/asset-viewer.svelte | 2 +- .../shared-components/status-box.svelte | 4 +- .../shared-components/upload-panel.svelte | 4 +- 74 files changed, 5429 insertions(+), 94 deletions(-) create mode 100644 .gitattributes delete mode 100644 NOTES.md create mode 100644 mobile/openapi/doc/AssetEntity.md create mode 100644 mobile/openapi/doc/CreateTagDto.md create mode 100644 mobile/openapi/doc/ExifEntity.md create mode 100644 mobile/openapi/doc/SmartInfoEntity.md create mode 100644 mobile/openapi/doc/TagApi.md create mode 100644 mobile/openapi/doc/TagEntity.md create mode 100644 mobile/openapi/doc/TagResponseDto.md create mode 100644 mobile/openapi/doc/TagTypeEnum.md create mode 100644 mobile/openapi/doc/UpdateTagDto.md create mode 100644 mobile/openapi/doc/UserEntity.md create mode 100644 mobile/openapi/lib/api/tag_api.dart create mode 100644 mobile/openapi/lib/model/asset_entity.dart create mode 100644 mobile/openapi/lib/model/create_tag_dto.dart create mode 100644 mobile/openapi/lib/model/exif_entity.dart create mode 100644 mobile/openapi/lib/model/smart_info_entity.dart create mode 100644 mobile/openapi/lib/model/tag_entity.dart create mode 100644 mobile/openapi/lib/model/tag_response_dto.dart create mode 100644 mobile/openapi/lib/model/tag_type_enum.dart create mode 100644 mobile/openapi/lib/model/update_tag_dto.dart create mode 100644 mobile/openapi/lib/model/user_entity.dart create mode 100644 mobile/openapi/test/asset_entity_test.dart create mode 100644 mobile/openapi/test/create_tag_dto_test.dart create mode 100644 mobile/openapi/test/exif_entity_test.dart create mode 100644 mobile/openapi/test/smart_info_entity_test.dart create mode 100644 mobile/openapi/test/tag_api_test.dart create mode 100644 mobile/openapi/test/tag_entity_test.dart create mode 100644 mobile/openapi/test/tag_response_dto_test.dart create mode 100644 mobile/openapi/test/tag_type_enum_test.dart create mode 100644 mobile/openapi/test/update_tag_dto_test.dart create mode 100644 mobile/openapi/test/user_entity_test.dart create mode 100644 server/apps/immich/src/api-v1/tag/dto/create-tag.dto.ts create mode 100644 server/apps/immich/src/api-v1/tag/dto/update-tag.dto.ts create mode 100644 server/apps/immich/src/api-v1/tag/response-dto/tag-response.dto.ts create mode 100644 server/apps/immich/src/api-v1/tag/tag.controller.ts create mode 100644 server/apps/immich/src/api-v1/tag/tag.module.ts create mode 100644 server/apps/immich/src/api-v1/tag/tag.repository.ts create mode 100644 server/apps/immich/src/api-v1/tag/tag.service.spec.ts create mode 100644 server/apps/immich/src/api-v1/tag/tag.service.ts create mode 100644 server/libs/database/src/entities/tag.entity.ts create mode 100644 server/libs/database/src/migrations/1670257571385-CreateTagsTable.ts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..b45b801c9c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +mobile/openapi/**/*.md -diff -merge +mobile/openapi/**/*.md linguist-generated=true +mobile/openapi/**/*.dart -diff -merge +mobile/openapi/**/*.dart linguist-generated=true + +web/src/api/open-api/**/*.md -diff -merge +web/src/api/open-api/**/*.md linguist-generated=true + +web/src/api/open-api/**/*.ts -diff -merge +web/src/api/open-api/**/*.ts linguist-generated=true + +mobile/openapi/.openapi-generator/FILES -diff -merge +mobile/openapi/.openapi-generator/FILES linguist-generated=true diff --git a/Makefile b/Makefile index 460f01eb35..34c9619c51 100644 --- a/Makefile +++ b/Makefile @@ -27,3 +27,6 @@ prod-scale: api: cd ./server && npm run api:generate + +attach-server: + docker exec -it docker_immich-server_1 sh \ No newline at end of file diff --git a/NOTES.md b/NOTES.md deleted file mode 100644 index a21726327a..0000000000 --- a/NOTES.md +++ /dev/null @@ -1,9 +0,0 @@ -# TODO - -Server scenario with web - -[ ] 1 user exist without admin right -> make admin on first check - -[ ] 2 users exist without admin right -> ask user to choose which account will be the admin - -[ X ] No users exist -> prompt signup form for Admin \ No newline at end of file diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 3409c3eb15..92f15f288d 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -14,6 +14,7 @@ doc/AssetApi.md doc/AssetCountByTimeBucket.md doc/AssetCountByTimeBucketResponseDto.md doc/AssetCountByUserIdResponseDto.md +doc/AssetEntity.md doc/AssetFileUploadResponseDto.md doc/AssetResponseDto.md doc/AssetTypeEnum.md @@ -25,6 +26,7 @@ doc/CheckExistingAssetsResponseDto.md doc/CreateAlbumDto.md doc/CreateDeviceInfoDto.md doc/CreateProfileImageResponseDto.md +doc/CreateTagDto.md doc/CreateUserDto.md doc/CuratedLocationsResponseDto.md doc/CuratedObjectsResponseDto.md @@ -34,6 +36,7 @@ doc/DeleteAssetStatus.md doc/DeviceInfoApi.md doc/DeviceInfoResponseDto.md doc/DeviceTypeEnum.md +doc/ExifEntity.md doc/ExifResponseDto.md doc/GetAssetByTimeBucketDto.md doc/GetAssetCountByTimeBucketDto.md @@ -58,20 +61,27 @@ doc/ServerPingResponse.md doc/ServerStatsResponseDto.md doc/ServerVersionReponseDto.md doc/SignUpDto.md +doc/SmartInfoEntity.md doc/SmartInfoResponseDto.md doc/SystemConfigApi.md doc/SystemConfigKey.md doc/SystemConfigResponseDto.md doc/SystemConfigResponseItem.md +doc/TagApi.md +doc/TagEntity.md +doc/TagResponseDto.md +doc/TagTypeEnum.md doc/ThumbnailFormat.md doc/TimeGroupEnum.md doc/UpdateAlbumDto.md doc/UpdateAssetDto.md doc/UpdateDeviceInfoDto.md +doc/UpdateTagDto.md doc/UpdateUserDto.md doc/UsageByUserDto.md doc/UserApi.md doc/UserCountResponseDto.md +doc/UserEntity.md doc/UserResponseDto.md doc/ValidateAccessTokenResponseDto.md git_push.sh @@ -84,6 +94,7 @@ lib/api/job_api.dart lib/api/o_auth_api.dart lib/api/server_info_api.dart lib/api/system_config_api.dart +lib/api/tag_api.dart lib/api/user_api.dart lib/api_client.dart lib/api_exception.dart @@ -103,6 +114,7 @@ lib/model/all_job_status_response_dto.dart lib/model/asset_count_by_time_bucket.dart lib/model/asset_count_by_time_bucket_response_dto.dart lib/model/asset_count_by_user_id_response_dto.dart +lib/model/asset_entity.dart lib/model/asset_file_upload_response_dto.dart lib/model/asset_response_dto.dart lib/model/asset_type_enum.dart @@ -113,6 +125,7 @@ lib/model/check_existing_assets_response_dto.dart lib/model/create_album_dto.dart lib/model/create_device_info_dto.dart lib/model/create_profile_image_response_dto.dart +lib/model/create_tag_dto.dart lib/model/create_user_dto.dart lib/model/curated_locations_response_dto.dart lib/model/curated_objects_response_dto.dart @@ -121,6 +134,7 @@ lib/model/delete_asset_response_dto.dart lib/model/delete_asset_status.dart lib/model/device_info_response_dto.dart lib/model/device_type_enum.dart +lib/model/exif_entity.dart lib/model/exif_response_dto.dart lib/model/get_asset_by_time_bucket_dto.dart lib/model/get_asset_count_by_time_bucket_dto.dart @@ -142,18 +156,24 @@ lib/model/server_ping_response.dart lib/model/server_stats_response_dto.dart lib/model/server_version_reponse_dto.dart lib/model/sign_up_dto.dart +lib/model/smart_info_entity.dart lib/model/smart_info_response_dto.dart lib/model/system_config_key.dart lib/model/system_config_response_dto.dart lib/model/system_config_response_item.dart +lib/model/tag_entity.dart +lib/model/tag_response_dto.dart +lib/model/tag_type_enum.dart lib/model/thumbnail_format.dart lib/model/time_group_enum.dart lib/model/update_album_dto.dart lib/model/update_asset_dto.dart lib/model/update_device_info_dto.dart +lib/model/update_tag_dto.dart lib/model/update_user_dto.dart lib/model/usage_by_user_dto.dart lib/model/user_count_response_dto.dart +lib/model/user_entity.dart lib/model/user_response_dto.dart lib/model/validate_access_token_response_dto.dart pubspec.yaml diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index a414e0986992f386d40c572bfc4ecdb2c62b3e43..2974e839ec88658dececc8df981292f31bfa3361 100644 GIT binary patch delta 666 zcmdlOdN*o=fzV`2Zqdn}Lfk@{3N;E^S^=RUT3QPFiN(dKC6oX0%Wa+{^qH+bBr)Bw zAX5vdB3er;xhOTUBvnf*RwE@pSzjL_rw-U{68)w7Eg@4XcD}UP)$2B{W1GL4gku z*&HXI$s`60JeQJusH`(M_<=&3w<;cD6i3nNT9KIs)tIF$#Ug_u8JwF~RN|SJmJbn~ zT%`WaGBF)R3D8ss9~hmQTnf4h(Lj-))Z&8tyy8@Z6M%w~3uQ%QfOJS@L8@zBX)Yo$ hKt(2pYJ5X=cqll0Kzc1T=b^egv^cd0>Z((k&jC~W-4g%+ delta 80 zcmV-W0I&buTCr8IFcOn>4H*}8aAaY0Wnpu3WprYBX=EuNd>|?+P*szn4KlNU67vSL m(HiFkv)&+E0<&QwumQ6*B@G0Va4FoAz$)vL(<_XVgDl$Y8X%Vd diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 9140b2d5e89ded507a634df9642dbca0a7a26866..d6139bd58b00cb439cc6c5fc235e0ef856957ac7 100644 GIT binary patch delta 63 zcmV-F0Kosa*#VZ>0kEzHk?9Z%b#P>1bY)?4b7gds(FPN8I!zZT9bGdg|nY!83(NE9Q*(P delta 77 zcmbPslX1sQ#trM(Sez<7QzoxtljcdP%uLZ#s8P_;3J9J2kV$p(8@B6F5b48-ieSd( XCy5K0AVRGv#gmU{FWo#d^)DL$d?p^b diff --git a/mobile/openapi/doc/AssetEntity.md b/mobile/openapi/doc/AssetEntity.md new file mode 100644 index 0000000000000000000000000000000000000000..8c83a94b03b50dea61302e274f7128956d484a08 GIT binary patch literal 1169 zcma)6-)q}25PsKRA;?1)gVXnI3^JNQpzXq1`eF=Gq_cCYEg9*Ytz&=uPEOr;0cA4= z^WA;=?w8Ip={m5g!$s>1G>c^jKr2f~FG9>_^42LMX%BL6%TDQMRfE`Ww?=sq*mlm7 zyn402e-*oqf{}3bAyNmWjc2DPgdpx!3-Y&oeCgun27>n35jxB0-VrnM$-~A8%JY9= zk!5JIjA>c+Kpt&90LJVwa6k=1mFpy_58%_@qeR+ITvjT)^H`%*%|_8a#qkjE7hGce zfL(XV4_5Oq%@Z1Mmw4KPA}~uzrNTqU3NJA*d{GCkipG+6&NY*Mx7b3Qb1H(~gMJQu zJNSz%`?~uMn)4~&Em||@nekh=;c}>^946R%74J$)5yKM(+%;p!vxFKvz-I5r4c$pk z5q@H|wv`(bTQNTFMNIC5QfY>kum4owVKkgn&P*vOQgs;9xWzypA60!6`4n?`LzD9c zD%Ce6$w}>OAX${95JmBs_ct2v;g~Ztv;NPs{2$1{n$Dr6za)+U&w|f|5Pty5n`PJl literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/AssetResponseDto.md b/mobile/openapi/doc/AssetResponseDto.md index 3e80e12c3f2f250758e2be586104d1a482e7c6dd..aa7d5494cc96087d3d1870ca6c36ee5e81b43cda 100644 GIT binary patch delta 95 zcmX@eF_m+}9Ohsxt&+s_Vl6F&8ii;rEuYNd5}T03^q|z@g8aPVRF{%`J1wnP4OAh$ j+!RfqdXScs)U?FXoDzkSe1&A7h7yJ7*x1RlnU?_o5N{zB delta 11 TcmbQrd5~kn9OlU%n3n0l$l2Q|tvwWy5ZY|UU}0Td(ddDE83O(BJ-LCpU0NX!-`)4n zT>&AY*U?rk7(7>x2{qwKk2M7$iUM{)8=yCYxerkf?|MYh_r1}Hgmp0_g4L~lcXQih z$4pq=XX@;9Ox#RlaS{jZ5I*2^jrpl@8j_7e@U7k+pZ=-k^(piw2J7jE#aH03@KOlz2`d_mAOHXW literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/ExifEntity.md b/mobile/openapi/doc/ExifEntity.md new file mode 100644 index 0000000000000000000000000000000000000000..4de3fc96eee17162b71045a14c774718d742c5b1 GIT binary patch literal 1282 zcma)6O>f&U488kT2*{z00lW9^lrF&*piPS<*bYH}s21%I)rTR`Kr-ydPg0!5X>7E< z*yMX8Me)%iW@^y9##|WzGN0|SoM}QjB*|#R9t{_acEBRaYF@nY3X<)1E4ZZuRc$P> zo7R8d=DG{{0^#OcpvFuu)`ON1T(aN_*gv*9)ZsM+S6HkGjrP{UkY{9_$2&V#mVIU? zX^J9EJuOX_WKmbq@U};|b3pS!Se(yto#6*)uv{|Kr8#B;e_3olFV{xsd(p1yM> z#=eDP7k;!@9s=~`5a~}sjp#qCG`bo_3oosq7M$b%0;7I!M5lW_7ck4eenNA z0p`^8+Qi-Ba`V0bk?y523EObo_MncAc^1fJc#!i7_Ot|UiyeQJ@ZHEp4Tv&XuOIv$ zgOOqJ+L%N~o6T*W-~F0RrcX0pW+Y6}sFS#}(%38x$&J}Ldh#;=Vyyr3WS~WDP}4<< MW5Ex_$4Qd>2TrYnBme*a literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/SmartInfoEntity.md b/mobile/openapi/doc/SmartInfoEntity.md new file mode 100644 index 0000000000000000000000000000000000000000..6b22c3c0049a8448298f654ff647c2e26f821506 GIT binary patch literal 614 zcmbVJ!D<3A5WVLs2KJx^cD=WyV9|r1BDJ?=!E_UC+TBb@#)Ht0?<87k3#HN!2yf=S zHN?`t$Ng^t2Vs~G%hrC)BepWYIBNAu*%8I2^n{W%(BtQd%t~ z)1=%`ux_6LW6r1;4b6W|liJ6#bYUaio%Hvg2DLUjb?68Pp+y%7>ZV~=e10`8{YRPm z$@aCBlhoX4_)<2*3t07f#2?^WW1f`i8W5tcA9x|Dn`PP{blh{(46ADVIGaqDGnN$^ q+e2?0sWjknhoQ`?j3f9`PuBA*wY=EUIPK9o`etz|_@nqp2=M`WdA`&D literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/TagApi.md b/mobile/openapi/doc/TagApi.md new file mode 100644 index 0000000000000000000000000000000000000000..5137a1dba1ce40e3b82d08c1f24f141ccc47ec36 GIT binary patch literal 4696 zcmeHKZBN@U5dQ98aZ)F>wU+ifY)BoA1`;n7gij2C7n{~BwX?+C=z#yeb1sE6V{4HL z>NX-(jW73p_nhqp*i;#j>W=?KV%SSHV~qyvSs}rV6!ZqWkc#+=7%FygaUn(Km>#D# zbI>g58>Nn}*WnqOo1T@iUXsAc(QXbRQ;?}dIQ>n5v%ozoXKY`&k(F=@J5e-1j;|Ay zJ6!fv9%njr+8P#AAN*Eb2K6;NUv&6fXqXlm|xqBW{6QZ+S6I{(X*UHTw&)MZeP`PYp9U5`ycXj$c~ZR@h9DBavYu;` zJ7Ho4@4Y!}s8)Nk{9Lt$pSO!jk#S^#Zg-C4lgwN;g&PL=kcCX;lf;XYdf zk%=+(PnI)wfN$t_G)?_qrHuiX2iB?lY5j~+bN$Zl%Sl?&V|Mso7 zPsKe_Zju>r=pysYkBX=z$}Xmb7b5=N1TmWF9|&GVBRjAls+0n?I#ZIk=YTv?LKbO( z0R7&!#wp_hpB&CNuz+czu^OnhdzGW^f~yz%Iz0NFot}QcN#qBJv+3DctI>uBk{?uw z&K=S_o$gWC>+gpi`qIXeG1@H*1(!ApuyX=sWNNW9@~Vn`i>O~x$=LJGNt4N%d7ng# zeKuj{AV|cP_L-*aJsa?}M8s%$t_ax$)Q%lOqZolpg6zTHl|qZut_8HTEC9th<%aWh z#0`!SkP4?EV(uS?jNcQBy%jODhq!)sKd83+R^Y$2o)b7rg5utlm13gQx(F1}Narvc zWgxA}!C-e7r6dS5#e-Y9HFapd)MtM!uP?m={70Q!9Qo#^zb`RT|DN$*ad_puj)_>* z>o0P)dffol=gc&_@gQmrKVPZX0Z|Y!Z_8IY1V864fK89rnm-?(h=-gbkMOW{Qry>Q zS>A1`q~bWf+}-APQLhw)WMrK&h^J_Bl5#q``BanY_pj=D!Oe%d$_vrk>z!x8#cBCF F&reczy0HKN literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/TagEntity.md b/mobile/openapi/doc/TagEntity.md new file mode 100644 index 0000000000000000000000000000000000000000..3b5bd36c22034960046fc81c445da69ac68af5d4 GIT binary patch literal 651 zcma))QES355Xay1DGvIe23p^@F>vUEpdy=lDg{Gxy0A70$@M|lhu@_-bb~TWq5a?e z^SkR^GjJZQ@}_8=#-`Y*s=riUE&WH z)HLCeVf2`+J!u;_8OUH18`UCw!S3YKV~(*7#*=ZD)y5G!@|%l;H!9En(?Uw4rDU6w zZxoC@_JTEMh-(hzEVnh3>mr8bAT{s+Tja?NTU3llvB#HEu1ut-SqhzJ{VZiQzR^Xi zC)_aKrN)Em8Uk`qI~xhgs^Skx4gJvG`TfjEn5(){gec1w-d&=s)0&~-4ytNg6w}wm xY`$KwH`A`}TC}9nF7ks5dB62If$Qnnc6sMn{s(fj+B;+E7vfy-Pw|Nm;u|Yd#`ypM literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/TagResponseDto.md b/mobile/openapi/doc/TagResponseDto.md new file mode 100644 index 0000000000000000000000000000000000000000..6e26854779ddee802ee5d1b45a15ea4f7438959e GIT binary patch literal 485 zcma)2!D_=W488j+1Ua-ZIK6MDj^r>{OX+$Gftc9K%xcRx>S3^tpPUw!jlt%NdD7E+ z(klR?4LZ8Yqp|1eq{pM3B8JJaXK1ybrSETnWsN~THq zOv#0D28_8w>^Z|$Nqy{jf0q+&`@n$!A?$L1aCQ@Q-MXqa y$7Wma8rCTc4i|3&>3pDi!KCIWrxL!Vw};1n&T_EI1{2W*`X+HT{9#@SAwB^_*pux5 literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/TagTypeEnum.md b/mobile/openapi/doc/TagTypeEnum.md new file mode 100644 index 0000000000000000000000000000000000000000..cede1409067b078539153d2637cbec56d28ec662 GIT binary patch literal 377 zcma)1O^X6C487-91m>U~TJP&gaSp;NvUn3=$~3D*+muc`EcoMXN5PMan@f1f%X@hh za-?9Q)1EDz^v3Krf062N_5f9d-)t<(fv{g=H0{ys37V#{CJ8tnISCH#^RK(uxCj=) zU{a{P(mLg35=R}Zj3>NcG0JUDU7K7K$04H?OJ=C2%EKq5)F%wE-Olo#D2&Sz3Qw++ z-EOG$_@|!Em$mk`vfQ3M1u?}}{c=)$!fu3*>*;#&&)b|=QLvF+kQa%Y;hXsg0IoZG BbT|M2 literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/UpdateAssetDto.md b/mobile/openapi/doc/UpdateAssetDto.md index dc4f1ff4be6b79ae0df46967b3db56565ddd2ce7..f02b6d5ed4ae1bb5c3e420c33a0fe73ff22dd86e 100644 GIT binary patch delta 89 zcmbQk{F-^fO~;bNbkCGxEiHu_1uZR~%;FN8;F6-uymULT5KuHazn~;DKQA#SRv|hi hH7&6;r$nJ7Um-a^ued}ZIyQFVH3cq&nu%YR0|1VhAu0d> delta 14 WcmaFOJcoJ0&B?NivXfmImjM7Peg#AT diff --git a/mobile/openapi/doc/UpdateTagDto.md b/mobile/openapi/doc/UpdateTagDto.md new file mode 100644 index 0000000000000000000000000000000000000000..ad4e551be6bb567a0a664555ba9266003fd12d2b GIT binary patch literal 463 zcma)2v1-FG5Z(0^2OiQ0r0ngK;tYW%gfyLu!NO8qQA;P}%TVaY_v8kec1g8}clYi+ z-m3tUizay;(7I8-Bx}f>%uo{wQC0AS#sa-LnD>xO|852+x~{V(lkg#7CRkkN_ZPKQ zQ7nYTL!r(}hs?`N-c4d}f`d;ueWdc$ZtAm7>nJDEm zWgmwbFm7=6F=Go%%N#wFv|D?VEt=+$!vi7gN>woWiQ4u~>(z0yu6G+9Y-O=O2N%hd kFzOYvnroYu@N>OBZ2x)7B`OzfLLcdu#JAxe^Q92t3l4jZ^8f$< literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/UserEntity.md b/mobile/openapi/doc/UserEntity.md new file mode 100644 index 0000000000000000000000000000000000000000..d342eb48809748bc36b57444b4fe8916f4dfea84 GIT binary patch literal 888 zcma)4T}#6-6n)RH2<*YF(E6?-Ot*)D%Fy{#3cEJh8ccITa)%83@!oV(w^2Vzp*^|h zoO_d;fkdwzcvG~fv@M>a4pT>lwn7XB@(w{sYP9UC(nI|UYb}<`rGh}hv>pS=v;F$T z-s?R1CBoTdqIOR6z{NmDNAUoymhW=0_31Owu@1(QLC5F;i5dC9!;1$f&;P@EoV9b@336WC&p9mIyubVhlt#Nz#3!*)6*DaVG?rH5I#y1T8qm|el0N5%` z8NpKTfX6h14K=fVLw4~2SH|jD%QrR$YI+@0w+1&>-8SG7?`X)A{nbGOX*KR2j;%OB z`kPQ@*@XEEla9L@e*PD&8u4xLd%PWjb?jzj-JOYawSe_5iq}d0g%0#euV7c{GlWEOre~nvaAKAIeY79tt2c?s`K FMgXUeFdhH^ delta 44 zcmV+{0Mq|~A-x{3s{^yd1K$C&!v)0wv#JM60kaedJOh(;45O2K4Zo9I4so;W4wM0u C2@!Sx diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 73811beb63673100e429dac562e19985a251dd9e..c48f8b12426cc4edf738216aa2980865e1b20569 100644 GIT binary patch delta 43 zcmX@v$+WqXX@fxdZfvDoHURrfgol*tmg delta 47 zcmdno$#k-lX@fxdG`qC z?ukJ?bI}wJ0y8_)Gu{35*S&3{QED`xb<*qn`nG-8K5F&aJFvTd(5}Hw2M#+u__MRK zzx($yT4q=N5iw;p*XqBmmF$pT2&u3?l&U}EF|NZzOf*J)jB$*@)Y35@CsLV(J6Jqr zGu=-nrmyhhge%%E*qA;Ors&(C(j;8z#oGr`Xv2i*^N0@QLypRA*9#Aexj@~IrMRGF zX2eAQ_m};_h$&qv(QyXk5M+XaCA?05uS=!UAZA)ak6pA94woeeKP#s+-i|pPx&^e< z|2(V0`g{bNx!+q`5aEzbVv}to@c=_ESPa?Re2075AR~8Ck%7k=n1H=ruM6dF=eP$A z!!FA9wu%`p1wD;Gp_wS5!Hf`2k5DIM9XzYkF-}ls&~-l(=6`|HZH3GrX}M$M84PkG zsHASG+FiR7IsrFQ+0C%_9XF#rW0HL_l$E@&f~;B8Og{i7zBAge4L<~ zb*-H=E!+4j8#YhRUgz#ILKdN#FFZDii;Eig@8;!IezEo5H5u5NtLQk3aF7C#W2J_k;ni%;?&Y<2f5?m2+9TrK=|;3=o0oWAK)YNm9Y zS2dlRYn0C9?_KA*Jx%cgwWgs8z0ARrhd+%BO*K0rW#@aC}x$M zS|VtF9$;cadXb=7xQ-1Hl56q&W@b=lYpdZRSI*xQV(&&sBpQ2uBJguUQHc>C@fPjof>OZqRP-)|3^SP}U}}{xYUbq2^e!X` z9niE3KYy`;2dr`g$3tm6B44~9)vtT!S;0F(Zu=;lJI51iuzg1N^$5wHoW=YLwk)w# zJXUUYP7TT#G|P?-Z9ef(^_1Z(;GGu^wJ}tTlWPfKr+v`wwOvYRT=MAZ!SaHqpEKam zvOEw`&No=d1Aa5@eL&v#>Nz6;)DY7u=Wm zJxZP&_b7C^-H9)*>gkaq*OFTkz^tq;fopR5>t#puO!`<)p5ZTqej{RzPwk_sB@Qwvv{CcdeyU=KWEHv<L*E*>#I+4qhQT8mswxKaF>S2kgRISU$e@g!|@@ z{{;H*51M8GL@w*x8t272HEY^02qMZy1>mjPJgdh2?HczUrlD)^2mVdRy_MYXnsHV7 E3S!eVMgRZ+ literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index e1e6fc55265919826ae358e624175230840a39e8..cf61c2a85dc2b8f61c89330ecffb5d65bf6d2660 100644 GIT binary patch delta 250 zcmdm6{Gnn)mK>XFUP)$2<>W?15pKug;?xo-pHWtIbC%pCX6BH@^vM(DB{+&w6H8J- zoJ1v+$?}SM3q6GjQhw0MJ5L8vp@oPWEHgw2n1>Q5 diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 94e878289ad6d335b0529e41bbc810b01c8e232a..828ff84fcba7988450770a54a8f9eaf4a6eae8c8 100644 GIT binary patch delta 27 hcmew$cSU|f7Y9>f`sDv?;>;mHT874QMwJfS^`6U^tMS7D3I7KHP zVo{p>l0|+pFRSKcYgYToHLU8Bm$J%CKFlg2s*zuwms;eRqL-0aoZ+0GlB%ho%{BQ0 ztNdg^Hfcr;uznXdv&l7V5|SFl8Hq)yDWS!wMa8JY}uI9Y;2bFw{$XYYiT$r52sW`clQ+x9!PAw+E#A2ZC5)BPag=#JZ5SaX#TWzuoPsZeSo(ygU zg<4H(i1;L4vCWdag^ZIA@G5NH#;4E7pHi8Zn46ibkX4+Yr=w6k`5(X7W^sXLW~>rt ogn1biQu9iRD)q_|b4pWn6w-_G%M+7wQmx=Doz3E+<&2D60Bk>K^8f$< delta 483 zcmeyY-KV=@InP-xWTX~pD%8|KFxaoF;E&QJH*&MQ`$77MIEPteTV4Smh=+ zv5HA5C}`xD=cN{Trs!oP7H2r;r=)5sXajAUyp~md@+nqXW}u46{OqEW|FW7*He{2U z?8zoSIgQPN4dR;3^Vl>QC!b_jpZtkkd@?tO;$%Gz)ybh8=O(Li%1?IV)ZARislgO6NQh%)EN)C8(jP^i_khN}T`i&9HUi}Dmo zib_)v;+qff6fyEZm4U-zvLUa+<{&`&DA31i~w6iexLvV diff --git a/mobile/openapi/lib/model/asset_entity.dart b/mobile/openapi/lib/model/asset_entity.dart new file mode 100644 index 0000000000000000000000000000000000000000..b6ca35a1d7b1d766dea0bf5872bd3092f0bc6eec GIT binary patch literal 12382 zcmeHNZExE~68`RAu?~bv#wkjhPY1WMYt%}sToVV06W|~S0xhmAHWI0FDf@J(|NA{N z`%Y3)zVz;JI0LrH-I>|ld7t6FcyaRLg}C~7d-MF~(c97ctJ~4Fcs2fY)ECz`;_c0? zcyn_-e)acLfDz=6X;N0hmxJ$Kp3rM{pBJT^EsAos$X4=PZ1b&>=}fNVTIN;XzGv%A zQC6)O$?7rLso7>*%}V+EHY`~Atue3d9Au9TA12=uF+ z_;Mn|3!MGEthQyYL?UweC?)~ORAjuVxf}pCnCKC%5cZs+T4rjHrTv!CR6b;L$++WI z0PwkNpV6r8bJcDm+m^{qr)w`rxw#}DPW&f1>W*_ChPmoWs4`yD< zq>|}X)wcAyNJ*@FW;69FdBE&bs)i*ddvG)*2-Yp7antV-^c7z$QrF_{wd(;4(nh!0hn z<@X{>!?#F1!U?Sw;fPfS9t)I__%h13Ye6~@B80jSj9)@;trA4TR{Fu9s7pPZbJ7k6 zy_9+Mt|(T*%Qk?#6jrD_cysrgoL85^iZz0?@(f8DDMoO0@$n>V9NZ^Mf47o}k|HlE zc(_~Gtk{(B5+$60S{BoWb2~Wei)HaBA7t5Qkb9X+ zD6vcpc~xvnY=LZ0thcHXi)6JDcgg&-s0!LQ(S{2%TV`b-uSMtn3BDC(TZ$x2HCW3A zu7}q1@_(-X*K8;<-PLBs)**T_V=P8-UY20}OAO?_EY41lt&k=~_=jpzb*aaTY@`{v`&|!5r$-W#9#DSWDU(}Z3fM8 zYYEL6QvBv~IT4UGbYVow_%typA{Lyka-! zLfsCIW`|q08LE1KVTW5VoVLfTu){4F&Jda#Zm4=pv_9QJ+*v@3X@^@dygkGyZoCDD zY{c)r5pGQOTNV1D#}TF*q_LAM9d@#`A4h~?Co%~XqFgQwJNmzc9s}1J0rDp({jfuS z5wwDWXxItg`hh+VJ6`Gg&Up_zR(tymFFowk)wl1x^RVMIw;y=pVaIE2KXAV|x37L& zS`27g^|-7Ugl*a5d@4?E%N@`(K9oq>I;Y_+hn>3C;C!aM!MSXq-A)+1 zKByP+NX~v3+P&4mygf0Tf2{+017kq{ssV{b*4p;7$U^9~jB#oLdn6NKw8_Pq^ZrFG zgl2@X{)3A~7;TGMf3bg23&D%pmxGI1AjYUK>gDRZ62rMSFNL~i+P}v-BvY&p9^7*s zxOMNO8rCK5-;9M|687cb;udJ{qR!>+U)6yy5%+d*T?f^=u$L4$cm|;dO|j%osdiR0 z@Th=p^@OAeF z#V+<@#|1h+6wpjOW01i-LD^joggxE-qZo$5V3ezENsW}G;%3V_*-)#nO3-NL0oPLg zEO(09EC%r{x-R0obB5v7z#Y^R-AbnhG8O3E)84a`dfRt$SAgAjlG;7Cl~pJAlE~p) z+*87Tg$Ctj15)S&mFWCauM8elo|W z8uF;aOz+I}>cD}<64b`Eiy2F(%B{-=o_c3q)Mh?7NzdVC>A8U1KT%WgW^s#z!YIu} zkNAVMl#-vbQ{8B$6{7^_C~^cH(a{OeQ357qYVKfseCIBk3b83^IuFf`4oxlV0SS|> zk4}!Pk086#5I4-Fj_y1Jj!y0~q@y%EX!p#Ez~(wf(Jf$zTu*k65~(9ZXK~5UQ8ErP z!uLtiQ34(`qTsWq1_kOW272tunh4)!MGb82JUzYz44G-On+CG(UwUK<7$Vy&rGZ>u z*#d^hE?a3}*H3LKujtVAWTQc(zJ)qMxD}f`oCgi~hA~+n7?f9J7yi%1hq#N}z;~b# z0ZIq(^~83U2P-t2U)9Z+a|dt6#&bTg4>Z2f=7^j`n8?hH>%eJVT%vXj$lDmL{p1&J zVc(a@=+Pk>_DtVkP;`cbCsXarDZmpoiZHY~dXF5`2bn_19j!N2 z5XmUS_6*hHk-{XQ+U3Yo>q!pKSTR`8GZ)(p0*OyB8Z{x&)0-dQyNymm0!+pKAypm|UB_ zQ z0}Av4U%jQ%6`Q+L)=kw1Y7+jkd?%@V%m`%cP&f%Rb5TZT#;`YYhs7@9k}d<$$e+x(Pl(Jwkl>gRpW9gCE(tLZE;Vo% zHr$(mM&LH%Tv5|Anw_xd=9MIy7Q3rSV|^zo!#x1VfvzV>y6C|+PxlCLec=ILX;Wb; zXR7eDcmh9QsSalR9oKm;z*6l_8nMlBJ7Be=Vo}E~coHrRdAYS*-^r3_YY(Xt73;+? z9DY_dlX0J)iE^*lO}|Fu)E@!-r13}k1mibXKab)d8l0r;^1LnqBfL-J>(R{tJf3gC zyZU%NK7_~fEqFJ#zl`1=SdZrk&+wakA9-rJzOg9~uqbz79-u8kS1^!?AY5$M)9@;q zJ^GZy5S`bc#`%o}znB1A2}7XeebwF!>4M?-s^R1dvgfJ5o$*ET2^9~@K;jejrQ8kiCnW0Q7$;K zX*v6W>4bo%I@4<*O5OwF)qlkp5`!Bm9^c}N5!$Xu0?^RnMlD5`cG8eP-Eu>cpkr03Ubg=;{Ed>WWXM1M!Q!;)ayDwlo)Shu1c=kL*CNOB@DWVby)D^^3r?8IjvXL~BxqUAhZGx2D_nHee86-WWt3 zcMl>mNCQy^-h+q?(m>Ra_aGvJctm?G6HK>1#IsY!(U2hoQIRT%tMYzJMJKCRulq6o zoI}BMuBP<@E>K&`x#*AtbRJ>XI8XpaT@q3w+(f-!$qF5dHZNK8Hc}DV4AQnlV%;FN8ki_($)Zzl5 zM5;?kzMTR{<>r~}Pno0@)YP~XKp?*)Bee)>ge^ot9oH46$uc|~EMVmxyayP0bigvU zDqs-~eoxlP^98y1Ks@zWpw%FT^=2QDHSESfHvu_T3Lz*~>*WBQ<5rZP>s6eer;!CD fqKnia+BFq7e~|vmCZwPM5zqm;tJa#UmWvAj`5R09 delta 49 zcmV-10M7r+NApFnPy@4S15O09)(7AMv(XCF0<*gfwgIz!4@m{H;}oX{v&9_s1_pgQ H3VjL+6onI( diff --git a/mobile/openapi/lib/model/create_tag_dto.dart b/mobile/openapi/lib/model/create_tag_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..0d9a79255c973e4c039de88435fded004299df4a GIT binary patch literal 3449 zcmds3U2oeq6n*!vxG92K!5n$b)8Nc*i^d)L6$7a^U@#1UmT8-r~%rz0t;<)DYQL>};|r zTw>&3g)*$3i#0zt!tk$?#3qpzZYA>M zm!Fe#C5&yg*qtN2Aia^f*r=HQuUoA&6V}ojBZZS=vAl7b?ps7%PX121!sM0&<#J1t zrp%O-4=a^13paNnXcJYbl3OVoyWK{1POPI2Sl?f8Bt-0g%#y>rSQAwI;%8@6z9fkH z?^pVpG={6e3Nx)UhWh@$zsJ&iy7{!lv<<3+h|03mG!W0bZcYef*Iw> zXo{54W)t8rVQxB=CQRd%o}+tV;fNA1y?e9tS~&50!#gsfu#NRlcm^1v}KfE2l59B0W3e#W^oc zpCP=pvKHw8G4%fP=r(S1ZDBq9HGE8 z?(CzBL$k~MW-d{!494}EGc`t19)>7UP0ow6Fq+{MMC<`De}Fin#h7iBXd6V(>X2!B z+8(sZc=z6M84+@<>046bqhjPXLmj-NjMv^)QB~v!e(wEnUmCp?vy3MwEktHzXJ7R^ zaKlJjWDc|F$nsH*XVTqx00s%nHF?J;JHrFZ0(eo;`m9&1Ll78>M9199l24SIX1n?C zJ2QM6%9LHC9}c(zsiomFFC>S<8B#|_dq+p&;-bS<0oX+L3!# z%T?X9Nsg?%%kK4TwQgsP{O?*dz+Pob_B{Qa=EmzSB=V)|Llry#z) z7hm>-ID)Rfm+iW#w8%sy@5EI=Tnj}e<#G(rKp!Hf5U!J=T_`~>wG+hF>78*`YWlt6Hjw5GgXS8bEzS!N%T27u`@ z*(Ze~e-2KYvY?cee%Hr2KVL6zWRv2X*LhaHmDO!}K9#OhT=Mg(*6W6*!L*aV%~zVe zk)Nfx)%;R^Za>JZf#q>i%2#!{UXsf_9Kx~?;zQf0>Q<KZ zy4KE6BYCA=D@-h6{#D9MOHtJ=>;sn&+{CJZSlU3$)Qfsu7GjZolHx|n3P9D= z+j9KGTW~lMi~3G}f)z%PTk^|R7FN8j*A1AE!}1i%wQj{cD@$>c731a!ab}2Cj9kkf zf0!o$mW7Q>eD@s)g@42kTNsE02_3Qx1=!{uV?GuI5X4_&U>q3`+oANpO~8oomJ~C@ zfdug#N(^Vg1+n}!26kf&qBm$UONc)RW^Xafg(L*|=MKchD8l%c7=$4U+vzuMqTq#X z<{@shag12a_SuhEWyV)1cjFvWFsF!++zn2A|Zj; zYYc?wNY=nl+`uCxBBJ*g3fnFzfuFd6mIgMNhq$qCfg5FCx%gGpid)%25YvlS7~du* zw0a?Ond}{t_Dbeb>#VuQq#HuPLTaT6HdMvfu^x)Q7@A$sn+4 zio?f<4vV8FcwGEo6M>`z(uWfUC0YW@q6f{uYgE-_s$`wlL$pL!-t2)&*nAV|;SgJ5N2!UbbvxCWjTlzqAD;kLIaYQ-5 z!1hAW7I55QVKP3*t21XeK8z1Y0&7aV7D*BP}t&E zuu>PO!JR>_u0bM{y3sC+!_3ND0saPjP~w`owKHpGCzUCiX-c(R56G|HWvjm#H-c%n zhhr&~pFqO?v#u-fD|4VT#}f`Q`weJfA~xV(9)2*{vVmMR162&J0g06}rP02@`XRO2 z=PrafCBfLcfvUL}#?C4B%|`3&6<%MABY82&hSLdsZswP7o%!iTMTJTj?-lHGnS*lg6`({?=`n^NRA`hG9; z5K`#hwkq`~;z=p?xkSHW4Q(G=_TIs@01{xIx2oj$R8Lth)ZbSIGKxb2 zwOx6@_4XK2uYSeQNR05iRguG`ddL&GeoY)jDmh`RZVv42B~~!~bGp*R2%xQ+puJZC zWp({3sF|4QX{(xm?G9(0;H9!#Qi1tfs?lg|=z7znib#3DJ0Cpm{)&4~kyQm$0(j-C zg@QjiqSs>H)JrSnib!x(ItRc2Z&9xx*S;Q^BOy9izEj#Bh5Li5RU_wcaZ7i;%{qtE zY(2ws?it2+mCxX|cu^O4aAgSW(E60LQ{rwDd1IB!iZN9+Hwkdo4W>A{fWi&)dz;=9 zWSnQZ-AMF@=3Wd_dKji@g|}~TsGqfT@W0GfP)Na@DO?T{!QTUal=m8s{4L`-+yIE@ z$Aq!thDUOPR^$trf0PAONe$Yr3fNuYI_18G(Y}#*_28~my5e4-IOrGMZ@{mHySvp2 zM8T~@19$a!Zv#1w+_z?+Z#C`EmpEvev-h89E|@UQ^2Kln#)pwYm}SM`8oT}i3js%B z@ZMhW3t$varlBZ50K)YXyl7Y|O;tE2P6groF|n zw?@$a4Gnm|>2%pu&^Bwk5yL_l9)^jw?V%q-&#H=JsFPQ};_cMM{1PsVtjC-Tad1Y0 z3c`J0BMD=KXVtK@c0FT)Pl zsCFwXlJnXgBzc$Yq6V<4xRg){rTIg9m(vw9eh?kun}#n4Ne`Sq;0kJaar0>Z@p1>( zX~kIC?3^kpf7l#RnX;416f=y?mK!@6U0K3dv4YsiM`Z?Q!g63Y7ZnYR%_sCbSv``w zo=?knvU?+6%mlv=6X^JgAM6?Z>O;a?iuYS#Gh8fGi$Gp z6TfMJ$AZ!pK*sGA+wA+%9VPy>8rpY{`9~RJ%uy((#4Q~*ID!nWnbYHDmddhsI&mK( z+p=w<@<7|JO+{QWK>51Og5N4gSEoMLIyn_3P~JG(T|R8mk}veN$Zb>K;m0{}P7R;J zXgPXtX`V@k8>#Wz8#wP~a<8sU=7hO43d3v;b;F%EB@<(%A-kO}S1n5#F`FG)q4gbR zq{!;DVQyg>DHjHXL0=esFqS24A(h{#M}}NVr1qYv3^QPYnR@wxGo?x8O50{A?KHfK7{|g_%8StaT{AA?UTv&ah|AF%y!AFkL`-v-$ zv9QGgAIg6Eknl#rM~G5`{kTvvQ*H)dp`(nbwhj|}@`b_>-WQtU$ZRiX{*@PU79k@X(HDr61;IcCJ$}%NO!feJqM)`%OrT1*&f$Lm|9)ALYFiUR$ literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/smart_info_entity.dart b/mobile/openapi/lib/model/smart_info_entity.dart new file mode 100644 index 0000000000000000000000000000000000000000..cf3e3359586db4160f49918647370ea212b35a8b GIT binary patch literal 4911 zcmds5ZBH9V5dO}u7!Zojs$-ITs+<}UC8Q!vph{?^%0kw1x5k^C@7&#W5F-5dJ~O*} zJ`O`rzf>wx;BI$bc4nS^W-fbsTYG!-`j69-r@tN^AAWj$dia*!9ep?)(AyI_J~^e2 zCvT75{k07-mV8wx>&GvK&tGiutGTFZtEaQtPG_dnBU)7}r;DjB^+H#EklxK=SzEtR zLzS26%1xImKehVrm9bE-)B?Yk%Hprr*1@=So7MBWa$Z$_Y6>_sGo!7jyN%9DQ)xFX z>*@kxey*zN&(Eg$Tv@lZ1$Un68P!W&sihg>-^;D7yj0H7=LI&8s#$#qAO4E2w}|!- z|0nHNwsNGX(w8*ph@FvHi%NpO)=Q8iW6vm6t_gqs|!bClHh4_ z7L;VCKL4U~f6t~EV&R)ULc@`~HPr=~qWu=V$-88?5!MH}zSi5rGuVybkmom2LTi3K^BYiFE@Wu_P=)js_c z)?yHMH&Ps2eLA3LG?I|E?@Fdvkdl^gT-_kwMA?#-wU+Fi*yA1cb$H06$h}SpC#D$h zf?QH*#=9{*;2)QIyvx6Vg+LzT-S`mYqx4x%oPSiyNC^Wfu0Yi$-v?^nySjq&z-lv7 zsgkB&um+mCBYu7JG9W65Ol;2-y)#HM6A9Fsjd)Jkjn6^E%qWvmY=`V7*VL%;uITYY zP`V*qf|Ra1P8vy0e3TRsc9N=iub(I&uHy59-rDk;97>pUuRGaZ=i@bKVbfPY)d0m6%_iqBZH13#Or8g>qRs*ktKA&83cOVB_{yS*&y5oVUtVaBEiL3 zVg!=bGjwO^ZFZG|$J~?|Zi+q+;+kS4nAZj0+_2E4s{=WLz%jxz94!HaYZ{sas1D~z z>x2fwsEwc%kzuZy2DnCh7P=<`6$v}6+{!XvD=(bBP)lyiUZMHRLet{E>MO@=9u<#4 z?(`h&fhHA`$Hdh0l+ShkRTqT0ig!%GGsn5Us*&<@&0QsR%&xf?Yz}cr>%w1$4!>N& z6dI1jRK_iN0fyX;$6$b1pEVbkkCx8+8y^=(I8^yO%W(L0rdTYha=L~oUZ{xRfO?IwYAhamNkM zL&;&h8Q!jRTD-~n>ypR)PDELwZ;BBC1Ap=yaxy!`XpknTjB_<0VY~iz7~0sY+|r{T zt3|n9TbHS8B$x$@Mjk^}d2nk^#6Av4ysw3j78dHaA-Ov)RLL z*K+^XvxGmQ*Xt>0$e@v%&Y4AQBGNnSr`*P}-bhrjaES?p@gjwUIrsLEkAb&Dg5D(; ziA+KfuVvD@u=OPmt!P~s5uF~~q&pJ0#A?SgDYw9m@yS^L7qbyt)J6NWYKbc$w+3^#wc~W1E*!gs_%$QmX*z5(1Tn-s8+fy&84}Cg zg9UG;IcoWuqsmgEMwFh!O1lQ2;$~3j9DJWI)W$-j*hAb*y{&PhYbA{x?VQ8wgDIEV zj6+H8YX;l}!j~|*SNdq30*hGW&)_g+qB!gFVbxxx0uK4jXqSTFTt2dpVrMAzZk6Q+ z1x1=BK~V^G9EdTIU8-aHd#kEYWw|&la9NmLB}w6e)1~oqEb-Ohujx(=o<=(!36JY#KnLfcR}H^jhM}z{y(_ksx%k>2v z{0D&A!~YcM20LjG;SqvIDGXThHGZynNOU+q-yjv*`5HPhIo_IL)+OG*7N+9wjnE@P U%Q$LYrG>_=)Ux;$Un~IqKhnDe2LJ#7 literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/tag_entity.dart b/mobile/openapi/lib/model/tag_entity.dart new file mode 100644 index 0000000000000000000000000000000000000000..d973c3f88103cc87ee9f3164fc7b6464c01c82e7 GIT binary patch literal 7065 zcmds6ZEqX75&rI9K_85hj4JD1J{7u?eQu((&Ry&xkqZ=pAkbRM+KaTSlB*a(&i}p7 z!{P2yTG`H*zMuwTOXSROUY{8fJw%K}O1-Q*Fz^>*R;mNBo-I=Y=)Xd10sXY-##xlW&|!r)FtZCNF!{_iVK; zY*X%_l&UOL%XhO7*9sJ8v7u*Z~64U~7C8c&`Q%Js)n_&}*orOontm8Je$ZPf^>4)e`QQRkSD z=f>Yf$9N^u%`g9&x$>t=swR#;SE8riyoHIu|CHp{elETkYY}y*cUKfk#?9)ouxc>q z-rW6WW{76FF!odp&Y?N8W2To~`k+_s{QkRqQdu11@4xq9ku5|C5+OkCF4JUg_`kbb zw4Pi3@9vg3f}7#f?snuA(!^hP39BFuf}8e~qwwmGmbbFJRQIM-3+)zfiWI7zdwc78 zW*O&ZW}MS@%QA%T7RF`HlXbH+saQ|dA7qVQXs^-%?Nzb7y&hTH-V}q|>lD_kwrmZF zTNd)6>yx2VC-68+2PY^)AH~55t?2o%4Nj;#@C)LvKqG&9aYP0uk;1(Yhrx*`DxAVM z>3Fq1==D#cMNg$$h|gx{sx02SB8OUn5A!V7OEvusYbekb`PauEB{PL&Y&%)nnShdo zNqkZD22Jr>{II`C+x#X!>~D&xep^276GUcvs_6SZsW`(^$;W*Ho=_hpo7N+%7{ZlD z0?@ZXuF`p`q}Mo=0pz~u<;FNQx5X+58T%!m^c(~o;`bPVnIP($B`}15@sK$W-cdcQ zt(l`^c*L}u8BoSei9b{H9>B^JZE8`ZtPfdefd9fD?t>hHE=pAqhaO$85+MkhRO?Df zE`(I@B*{zzB=>WuD-g}#L*rrYHrAlO=n}t-TIn@`zSL+-exYtbpUu{>M+L^~qBQD< zzNZOpW8rs7HCve3XOk*)dFq~}?1Oh^Tfo_OhJ7I~@>lfVzPQ&E2fJoO8T@rO`Rr>xyb)mvg&lcS-ly5vmvcO7rC6&Ek@q|6q`4~nE z1xEE9MqYo=14- zH|a_icJzd9@L5Yf01fFnlbDm(@#1M7X<{yclo$B|2*sQc%)H1R(e79)AksvGrh;E7z8w|ahqky4$_DnPb9$zIF!Yl=gF8H6u&2#0|X3L>oTe= zmV7tJEFM_4B&`l({>Y}}^`Ob4#yL;jLixn9SnO0h(P=867;%al*$#N@j#C3y zS z`oT=J+FC`@z?`m;u~bmJyb`wd=4!ve2Y6$ zSPD%hBKIOO;{&VqA0Dd4Tt7fu$-76UF}BGggyBuxR|yBdwJpBj)|;v@@p3j3!|KYZ zWF}L8E3T^@l$^Ke$xWUbyWDax5{!pe{Yd8zTbb1zu3w-RXq3^ks(LKPm-Mi?dlmCS z{q?Bcevb|#_q1j+RIuoO^XK=&HzS1PkRi4s+cLcE@V&cyGklVZn+JH_+hK3)-@k3p=I7%M46 z6XcVq#m{K^jnJ0Ap1z#m$)rWiqeoWf4vl7IZ1*%t4JZ4$RW~Q?LSL=(zh|_m#s6nV zoa)&B#Zdk>QZX)sWnPKi8$V|3WDbSu5%zO4& zqCcq~GthBMt?u;fGpP>Zh13*Y!8YN>LY8DIG35V9pwFkir*%S9DZ<;G&tME$jR zFnH|f@zG@OXn*~9{_G(81r6kEX60>;7aaNj1CtJ|Evy*1z)d3Gbp<=roDkCZfman*X2A80 ze=bttteio0GCyO81ibgtOJTx!*JFYI!@fauv+&ehgLsZ$Zpbf z)p*Lsjl$r{A39<`gtV<%A{5P_9vFR7e1?ttEvC>DpJ5YYUaNj+_Hpf$ z*d|hx%msC6?>D^v_#EkWZu8GMGDP{wL;?#O5})_g37xq6qVvpcWapaBJotnOXlY7- cXdBq}fCAYX{SQ#+=dEnmW4dS{Ovb-|0qRT4_y7O^ literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/tag_response_dto.dart b/mobile/openapi/lib/model/tag_response_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..d4ed671a285b83f2264e5c43639f9df14f401e41 GIT binary patch literal 3661 zcmds4U2oeq6n*!vxG92K!5n4H(~!(=i^UoG6$7a^U@#1Uk?5GMEUF~cz)17ocMmDa zvg5Vc(|`qtEs^*8o_k22ob*mk==C4d$?@;wtMT>g>G*;!-@G3W=wd=wlPP_eT)esb z>wv`|`6?5}4Nv12r#-x?+d>QYE^3E_G*ayPONQG zRxUB}OQj5}7h=Vqr7-;Mq_J$=*!}9MF02!UOH{@V)j~<*)$LwqxhkYha$VfAm|Kb> z`Q_&%T?%7+J$C0vFG!cN5T%Ow|Gd{rb73t_#qG7UCHs;WPSbsl=!Db1maZ~|B|(L} zquI99oRkkOl`)HN??tdc)Fe!9scfvW!JbvOF6F*;)ClVjM;s;5M`u)VODgkk>}|?= z`=Fog;H2oHjHZMw6RQGUa->r8Gx09I<88Hx+q=U`TkDurn2E7x7TMm zJcOTtPiuGx>+`<1`azVx1|tK?*1Vz85jp+V>VmBX#4J=Ha!Nk&8V%`@`2E9qsCf{X znZC;UbCv`yfH-P55QN{t!|o=c@|$=Fo5o{3sAJ^;sQVW~K%&Sf@PH-dEPWMuB`qzC zUe%Uneu?L1!2*4Z)r@O0=TcQUqZ3fPQ`V1Dr_H623!Y|A1g1(EzcvA~6Xpd5Y{C?> z)EU|y7IsGQrH{ZDxh{H$tf@E$6ZyD+a1N(gCknl&Tj53GR9N?@DN&eE6raN$7q&8z zCtf)IkhBsdaqLboTlj^R6h6zf1$ESlSK3K>apq;}+5yykN9j_gpJhf&!-!pF%B|@} zt~FQuMuHX!T^yr`yXH<|j`K<}LHuo*Maq&*se;KVm%?&@ymwHC!CWYfi&{7$v?ktn z2$w?`i*y-99Q>wI5UW*DuDHpURlMMU`qwr06D!iQo$)#3#rZP?xKh?4A)todewO& z-)Pl2KKM}UEKrwr^y^`IuNd*$>5X1e&a0PAT+LX8jPCtWb8GaSxXGEeX(4he2M-Ne z!c-Y)tK5O&c5Kh)q4*d}7z8K~10O4d_gGmTl<}%`9sFT1qS~;&1GpjDctlylX6X5F zhaol<=Ab*h-!MZBvaUhQ1yYDbmIeOkXojkSL*aX?#9*B|Ao%Bc)q#_W9j<`^F4IKmLgl!G+=act-6_FX8#+Mcf_3 zInABf^p01}y6IkwHzjS8%hS8)&BVUfou2%ln_Q$>Q1rAi4E(Smg737HRokiIr1=#j z<7=eDE$dFXB`;xWa0A`$z;O^_o%+n=I{^#hA`V*&p*=_ddUi)=1aWNa#l(P~HkoXr ziBJq!(S)>|?m~4TA1Z{b#a_eq%&xn;4nn8=_s;Ct z#=JC*6j*!beV%!Se!tW2!}w`F`}z0D!{pm|KDmSY>6b|l?q={Xo5R=H-SqyiD-LODEG<=5i~w@72$0TNvvvYKVL%_AV|;A6xlP zsVu5%vBl3qSp4?VIy7$GW%a~p=Y{sMO3|Upl(a$JU38W!EnS=&y+$#=5juYNOPp+k zb)62n^I$SCh18-@5&pmLbUH~coP)Vo&-aC#=yD4`Isko4e#)Me@Dc=QxdTbpJLNav zHxdeKo-wKv7SAFt&wl$nxt+uC zBn5lT7uT2pz>&uN_;zwZo?c&&zI}R}&%R!e=L^!PD)dFU&@PeZP#EXbDwmv+1Esl8 z3u?%6(Q003!clyov<^LfXo-DXpcep6su#`_FrPRSIxj;smARJs2r_H7p_u6&!HnhO z{!-anas;Wl724qdZKdtrM5# zgiPd4_FirRcx%X5>B`)nrH)xq@+yNaSp!5|DE6B-K+lnK55`Znh2Pia(4euH?J@7CR>HrJ%pv=VuV8rg6b@ev8a867xWxygoaqr;AC^F<<@gl0<( zs%Ex>ikR$~`-n*@m9VHTD$vcDc(tc4D)8MwC6$>#0v=#>>OV^IswqJ`1CsF6a~Ruo zNx!ZF7P{9@`nyIX@UH@HAPx+}dH_FB$#~J$K3<_mqFIU$`Hv^y5nG!}kL1^;rIYbT VJXZ8ajXqz8C*q?64ZSC({TKG^YX$%S literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index 84db0f2a167b53fec13455daf66fb4cb08852ad8..cb7fd1c883736c505e27713a6198b789eea3b93a 100644 GIT binary patch delta 785 zcmZva!D`z;5Qd2jrl^-5aw?@Ahe9ms$cCOOM|B}k2=q`$PYpS&q>;7lcGv8#oQ9(M zAp|}483K8NLY^V_zCxd%vsyVOY4;%Q?#z7i&+Pns{QKGE_noyZxrr=W9OniS$doh| zPS3^x&}wbmyk#q;xkYzbhqHiw@3^stCsuR0cnv7sw2PR2F0SKuKV! z)e^P608^?;eMd-=RyoXJ3NdUA!{$ft zO-D_hd(aq~o1UKf!~Pk~h~LgyDlhJUhaMclx8_wpdQbxjngMrs`q1^t2h~?;|6_mc zIWc@PN}lTd+YD5+8?Tn{l)E*D(S0}O-NGni+gTiicf#U(1MN_oa+~YH!PTF^?qE>Q If;tHP0%kS(SpWb4 delta 101 zcmaDTKTm2y9V16kYGG++QEJNMnT*mbN%{FXll7SECZA@`4Lb9sB&e{+?H#p7u^p>BYy($)jJ#7vuLYF30Ee>h;@kOy?82m|W7k$@%M7 ze;$F1DWB&;+u_sX$T2$o%WOgaa z^ykNEwiMd*dQfMnnp0KFQq(HJ|Fd2%D}*uhvCf5+mtt{lE4uCxox=I|(l)v@B&d{Y znuau2q?}oqOh6pIiI|(nE|p2x&3N~yp?AefUgvyza{~2FlfTmMg%n0oSy=#m2?11V z7_04u%(7~VoR;E?q?wc@h{{K0QxsxW$Rqlo%1lD1BvZ=_AZ5a;sw9qCD?G2HVbL%= zE1F!wt6a`SQ&{>Uibhh@7*#HukmL5v;CO4nNleRXEx$+|JCcPgr3PeqE8bL%h6z~( z$*aa#nv0^KnaDm95QW_=8$h-ulikz~I>z}WLX7BY5_#^)4j2Htzu}-$0Dim%WQ-oi z1cGt>U^OJ35d;4}+AgdA0knfecc4G7kT)73LPTa&Rd5L+c`8lAVKkY2k{JpU*{e@z zI3i!@nJ6L-XxzQ|`qkeNf*ckcJ$wkU_;V6m%|Wb-_^nrgSxgZ;-my}ZmKM??an152 z=k##olx$lfcfwL;(iovP+{w_KmeMHW$Rf@+X9e<|&gd}>oIyuPC-8)R@|1xUw-|yq z0pA@{eTt(9{7vLA?1u6l$LbC5| zb5)9h(oZ;}A>ARrzIoVcZ?3 zu03Pm%t6rSX*5NUu40F&Px`eoZO>~5_5gib&M^Yr^wdp;S~DqEVTxgDRhDAzE~}ig z)gSDY)S1)oV&3L}x2uaFN7fGw)FO=O&ItpDQ=`ocN0Ma1*gH8A`Ps*13m3QoZi3zx_msuKFE`5YYcN9U8lo_OH1-}I(JQosqt*X=*MN*On|Ib-O}hsj0K z2evHT{{=dx%+yuLz2u`?wL`mI5`e)S9lXbM+S+Qcx@qef1reR>g5ACkeTlZr+yw@N z;Qzr3$z*~)1Rzz$4jWeUQkK5I@nA-w*tB}W` z;iEGuaP|to@0E(T^x6%&g|61TqM^rOAu=+)=`z4qik5~qFm%y9$B_8~KE}$wbcF(yp~i&M$9VyRgLv*L{Z3=`;IsJ& z_BP)S0#1)7_d_P!L)SZ<*os|>I*10;-8m_)kG9aG#cVrBX!VGbEss$#AFvWoa#*%w}hXRG@U1m z;(g-hgd{G;K{S34M*uYSjIKoteYNk(`a8h{>$XzhG9I~QID{&{*{zjb;)rX7Kfgls zA9)gRw3Cl_d3>1gml`A?bq-_zXU%@OxIEzFgSimX{b~^k!z?+V=0B1v9EmMbLB7P> z3f$FV{4XTYDL#JXHeb3~>SjMiGj;^`fjN`B&|ynO GkNF#symeXt literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/user_entity.dart b/mobile/openapi/lib/model/user_entity.dart new file mode 100644 index 0000000000000000000000000000000000000000..7862d296edab32d6127286c479c5199e26aed66a GIT binary patch literal 7537 zcmeGhYj4{|^1FYc}VNI^2Z75Q=yNY4B{_mZc zeemfxU+xY#)IcI~cV0X1Ssotl9Uj8#&zBcZ|8aVL`tkMU>05Yr_Selt%PQZ1DfB(D-YmHb`8XarC^dj1`q#r~(_NaU5je*^Rq?17TWCXAv+8866&q2#8%Dl-^;zdycEX3XCw6~lDD^Tw+C>DNB=18 zPFDs5RPqMq4a5~F7AcoA1Z~z6!UPD11iMydriyfAm5+OlhacNSf$T$j%DgT%sv1L+ z8=*?K!JM~vuA5qW{=8&FX~$4&v9s&5QAxMDCc~d?#7h1y?0O8{tm|D_yg_XfOq*+0 z7rBr-ra=^i0~-+l<$mCR%mDFm8&yWyqu_9Abyhop;E{i8oIUNopm7TLw!`~CZ!h%& zocB0j#=^Rd8N-li3=^2RSUCJBx4 zh(XB4q!cD>ts%>ji|apRj-?Q@j6Q(u81pHW)?AbcMQ<8*{`}KDVd`v^Ir!lRB#M8G zj|C1%B8A>^Ov<#|ymWCxk|vn=+y(Jjn|Zj}*JH#voVMLV7dvDe+K@b^N!R(1U7F5iBq=xoTW4sZ3^U@V5`Ue3uFI^y4V6$;|QI&<2v{)|8 z`VCc*?3nExyFqFKE164UgubOF4xMK$jWP_DsA3jS58y|)79C)?73@CT@unYcu%(B~ z(ZG0eDsgZE&)_K9eSe4%Y~O;Wr4dMP{~?LK3k)H~EIx#a_8e|CR_q=uu8+t|7H%jF zQiAWJZ0#Nr&H~e>m7S|1n_%nOL~b^THcVrzW|LsOG_+TU=37)3klBQ-l}0mUlVGQm zV=kMJ$}#7-9yFWy2gK+_5@PmNkitmVAjOf@vM|I#H+2Qt@FRZOoClG<6$ zsBt6en)l;dA1r8}p~6ca$lJqla<=J%u!$IepWL=uLJ~{``!1xyNg+xaAQ(HmI0E+| zOYEfYdS?qjj6sp-p&5*ra#TLZkaiiB9=^#aLiU+7(tBTHdj8pwY(V-x=MLI)#6HrP z13IBcJhP7+os%)*n*$>SML&JmED?lh#JByKtv}m5Il$jDQ~&(av*#fLIPwqJED~UH z59^ZxhC%4TQPey}JAa{-dz#~D;P{UD8bnoqyIL@ytZ_qQ=m>w*fJTWGaE zC(NDW-sb@)RR|3-kelVOYeG@NI$$oP^x(4tgUoUq^#^$(akDOE1XR;$WpZyf%E%Pt30KC;9*;PT;9@p}= zMr~h98vepy+b8#e$l*){QbeOp~|3`%f3= zvngtpXuH1_hdCjIX!;K7*x>j%+RyEH{s9NNvk1I=6j;5JVevjWXbkQ5n{y-)RTW2o zM*k<>4_quSab))%bDWTJQs6pruu@fqN`1l3@=V%3&6RFp?i1K{+aQ5IE+M-jP{w2*wQcoZ+xmyWc(+VLonK?Q14 z8>=(}ie1%@p>*sJOU{5$-xd+FgCDB}QbdTZRCVx+ZAK}7@a+`>3JD{;Dghr!Fq zmV6ysCRbq^4fq(l=Wy{+6zUU~8lS*b;l#~O4m1ZvxglT-lG|r z-iHk3GP#z9F7B|?Ytfxwx%j3sK*T5!5pSI3IQ7qz!Nh#J+1ggVqePr|qV)|WQ$h{g zTu4*x;53d58-6oNZtoE|L1A0@rl~@MMvBNe(Pwzo=W108(`l0GGy!(>>alE12TFo^ zjvzcSmA2Bc{Ke|l$b3xcEZViL`EX*V3aWIiHo8S1bkc1lBd>4jn2nJ+8$48YsouIT z9yB)kj}r^56Szp9JKThe3AV8puIk%hu^GK7X~H>3gYGQfvK*6`F+|G_k5~|uGk1tw zx-#b*p|%9Wh2M@D5tZP4BSyxcV#fNZ-ObRAvTIzT2%!%qjpm;^0EG9w7+w3~9X5Sg zK{`m)huepi<;h#xA;R{j#Zezf;rpTw*|oi6z~ukJN5}$EOD#W1W6U+tm;Mi&=McY; z$ml1bv!kAjdQ!7mw_0bCH_nMOD7q}wPGmiHdQ-1-E;9|=F literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/asset_entity_test.dart b/mobile/openapi/test/asset_entity_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..d34d066122d14c63c12afeb1630d03289054f036 GIT binary patch literal 2556 zcmbW3TW`}a6oB9JD-KWXq_M)D(8dsz+M!BgnOK>G5X#*6Bt0y#gMHGip#688+giq? zTpn_<@}2V?$Bv!J#G6cD`gQ5Q{xrXwf0-`lGdN#d%nxDa!==B3&;D$2{_T~+3i6{A zhR=?V-X42JlVYU}29Y*FLIe4h;Uv$+&PO zQg_aTaaTrTU2NI7dYc`}}An78Y^t%vU-Tv~gw<;_PDMDN^OMm8<;g6MhKe5COg;@4x(eC(%9-?zI$aL^j*bqTgVFyJ6 z4w3LS!`AUApdrb%p}3emSAy5Al6`oH%%G*;*h#fRhluWRm37iI;1CI_P)n3IZoSZ* zWX}VJNJE2yQBJvau%5^vnpDeT1?=M4p}j;at=*Dk&&9^gDsG?PJG7UkUeQuYaoOcN zYQSC+fAteXhH#DHoz2tsp{c=MlDa1{H-bs4R!IZ)lGLMeLzY%amqDq?UXr>e32pFd ztyz~}P=`hIc|b4e{DGos2L*Ju3l-_sSJ;5PMAxa?;*0dP%{J{;-4of%cR`llFGXDG nJ^+i@!dPM?@`Pa{lHC1Gl|MJ|{l`IT5k+`6ze`na@sHkLQmguH literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/create_tag_dto_test.dart b/mobile/openapi/test/create_tag_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..8df20a4ef54cc30f496b0d7b9bcd899c2409d3b6 GIT binary patch literal 650 zcma)3%}>HG6u^LeP#`Q$2*c4l zB3Nxyr-uVCdU8D&!;esqB9pw=n98mNM;l^7NJ3N-oib49-flqTm&Z#Pe+(1r_>U`9a95tK96TVW*%&`BvpK{B)CBKhzxNtaydDC%Nl zqnf98yntg_gWxre>2qka9-X|97|>>Hzg7YZwfCCQk+A7K=2HsFw9#*g8LK#oy?ypK zVvKY-3SKgF;ite(*W`QqClQGXC7XJ0WF4;kIXYmy6X)jIC=><5j0~8AlZA8_%q)S` ziV{(#WjC1Jbhcc#B6VR{6AEP!Phw~O*n!s_bFIP5{N8(A5aI#^D0c|CbbN}X&nE+& zDy$7k;Z1B!8-y24#eixwtEk>TFf&UpYE!;D?F(SBG{bS7KA2~l@){ptCO)+Wytqwy z5UnqpR7h>e2bFD28-$v00}HHxW&K60u&~YK&F_DM-LRF+FmG)XUf{Dd2xTA4k`B={ znV+!|h7)KE4n(Y!L5%*9NbUUd%_b|_#W~IX0cCKiUEHU#5k?INztaUF+|;4@|6=r2 z{LWu>D+IQ_inXo#H*`;mcf4EzLvUCZWMbDXfA8$dfsz>9 zh1ri~{`ow+%D&H**&NOm7ugi%Ib7vSxXkB^vzt!>3&?LJIzBnr z+doJ`scT~$smeN4>4pxWH@&A)Q9~^mp1#z&?X2Ui3~c`3+N-W-<>;Z;PUr?(IXiUn zXL4S|o!?F`tnrM7l`h3lSDIX;`>ke;Hsn=fO)W59p;2GIsA7fAC&|zwj5;iSV@-#) zBZHkz<=YFXM@MfUOrFzlya0k}s6EI?;JOvs!c;bs&Ul?XT3tem+KeaglmH00(gqvQ zF+)Lc@|rWA93_IRo$b5vC`i9XjHa-Wi-Onew5dUt!h+a~&}_T_uLwFPxg4+IPM3EB z7ya54v7tbbrk43UkM!rk{a<19o_MiaeS>?K#fA2KJOnre#@fTUqze0n;Srl&o>$y9 u-2R;GOc&@MH@Ek{sE{pw+<&9+t`=rtw!>S2IKQ1KBkKP=L@TcIIr$4VuRD1F literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/tag_api_test.dart b/mobile/openapi/test/tag_api_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..6554281d5490c8ece42637e72d42ea370572f582 GIT binary patch literal 1011 zcmbV}O;6k~5Qgvh71PsZrG>Pd5Fe|qnrwvxL~6sKC(9(WVcNttwgXyK{P&LIP@%Ho zqX*ma_?da%@nl(&Ww5%c%Du0}W$|rQ6*;WW&Wi=)C0v#jT$K6Q`o{;x0{OX-)}J2i ze>zCQm0G1OijB5nLmeK$pazGHzz%y;k1#W`{v}&H z(gu|@l=0dRUC)l6As(I2!uAM+Pst!e9Pp<=CEJyu+;dcVS%!ZWgfJXB* z!s&#j^W%hp*6P8evwsWC7BE!^HP(Y4ES|7N!JYf<5^9UmWBO%0IG%ew#o}cS(rs1E z0x+743KJ5GkHBa$Ky0)s^Kwjhh}NDcpOf=XnTX4Q49;u(>Mow_mc$?AxZuws5|?$kwpU;W97aYrfr`|9B&?fc#t``?KSt z_s2;nbz^!aRrjoF-O>pRW^hy~YN;dR*AKPsY|s861KU41I%Nm1n0^nMg>JBu)gsH= z6FU)ScbH!G#(6Ye=}HWBt%)Pu9W-mTA*b5jGy?N`H0r}URql~pl1v$4VX^#$4W0Cs z3|gJaa}>H>oc;&#I2S6|lwFB7_D6q*SlOL1YURTheHp>Pfx-?5XJBP6yvFoAXQFALLy0vAyJxOJ$PC+?X+xcch}vmh8VuPvn^bZ1U>AZ zY2NS6n{5;YQ3UhXEWLhAR>^vvB{3|PPss@46jo^l&uP3|yj_vZD<2AO+-x$wodmuV zl~e{e-$QBXrs3N#&V&o--tM9XR=E0Vb_m+bC_x>mt7$1-_}TEzpxd#;Y}2O$jk}D)>~QfQAb>7{Ye|K-yABUW4dH$Ps2I z$Y3}PNLU%w>Ots@PXyrz4rZO*i!(x2APRz@3qq9YF5tEZ+R#wO?O-E{P0w<>lX(}% vBNQo1dG4{a^`j6_G literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/tag_type_enum_test.dart b/mobile/openapi/test/tag_type_enum_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..07a03894669d0091cee5ce3bd357041066a659bf GIT binary patch literal 419 zcmZvYUrWO<6vf~3DXvf5pf0*6*$^DGlYy=v^}(lbX>Yq=o5cK4hU~kWC<9@AxaV@< zcXE<&&Nzq7Yh7JGmizK$QW@KqD!ipS>K7wtg~!!xtW%Fu@B;A3@p}S2#vBXyf)|T|C^n>3E}+ z^QgR#Z7h_XB>Nk8lVyWcXv}p}%lPpO$Wca`D5+^7Vwuh!U z^8LQ=?vf-*5}2*??D}E8nm^C-c?yf=<9q;V2CFQGrz~AAUav^zmG>nYHysUcN0Bc@ zEtTP{QifHc;bZ7zXL-qZ!&@%h;8=@RE8{K_*nHy7GTk|5_yn@&Bsb}iCQG| z4gJ*dk@w)1wPc*7HB1~)+IWgW1Tt`eJD{vi$eDL@D%}&vJw&-5Mh9V!}JX3 z^(PU@YNI;ci@oXDb3A|_qhR&ojF2_>#SsLd38GZEfZKA=h6XZj2OBTi9C)-lS%h{x iL@ni6ngxUXA^z0d&^}r}&a(V7<3#(vC~^#u`{*0{W76#a literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/user_entity_test.dart b/mobile/openapi/test/user_entity_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..de2efd6e008a99ee866cc948e589ed0ff07b8db9 GIT binary patch literal 1708 zcmbW1Pmj|u5XJBL6vL^lR5q|Dw96tY+C`O?20`|K5Xug*n_4(_uqV5!(0+Hu?jKc9 zDNPSaYW2PM8+(#T(bvYz)&37FiCTvg!Qx+Y6Qj<$HmS zZZ0ogUnXIxDr222mev(ZrR5d0ru9-3Qp;Kz8t>MsZmgrD4%AP$_C?cD;pFdDIkp?D z`QD)8CrRfy-1+17(i%@_s8E7|YN@1)c7GJ6m65*C)>JH`6`JDBtD;5wvh%KmGX5Fc0IWMPeRgD8g4s5fQGdPJ`%Y1(#^OiS|^WI38+z-;RyaD0IXXo zgBnyUzm#xuARmsd6Bbp@w#{%50uTNijA0uRW4@A8rUE6{01?g(8-D<;Bs7jU;%E&I zN<3`5=+~xbC_7;<$WLqesQpCL%5?5y ztLp{Wc0qEBbtfRI&WUf(`zPzVTfGyW1M#S-a|=OqPB_1fVNlF#{=z;ZtvW&f8hSxv z+x^P6T72M-Js?j!z+egu ndG-znD-S~HK1ao=^ql+!QO5%p literal 0 HcmV?d00001 diff --git a/server/apps/immich/src/api-v1/album/album.module.ts b/server/apps/immich/src/api-v1/album/album.module.ts index ee589dca25..4df3835ffc 100644 --- a/server/apps/immich/src/api-v1/album/album.module.ts +++ b/server/apps/immich/src/api-v1/album/album.module.ts @@ -1,32 +1,29 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { AlbumService } from './album.service'; import { AlbumController } from './album.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AssetEntity } from '@app/database/entities/asset.entity'; -import { UserEntity } from '@app/database/entities/user.entity'; import { AlbumEntity } from '../../../../../libs/database/src/entities/album.entity'; import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity'; import { UserAlbumEntity } from '@app/database/entities/user-album.entity'; import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository'; -import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository'; import { DownloadModule } from '../../modules/download/download.module'; +import { AssetModule } from '../asset/asset.module'; +import { UserModule } from '../user/user.module'; + +const ALBUM_REPOSITORY_PROVIDER = { + provide: ALBUM_REPOSITORY, + useClass: AlbumRepository, +}; @Module({ imports: [ - TypeOrmModule.forFeature([AssetEntity, UserEntity, AlbumEntity, AssetAlbumEntity, UserAlbumEntity]), + TypeOrmModule.forFeature([AlbumEntity, AssetAlbumEntity, UserAlbumEntity]), DownloadModule, + UserModule, + forwardRef(() => AssetModule), ], controllers: [AlbumController], - providers: [ - AlbumService, - { - provide: ALBUM_REPOSITORY, - useClass: AlbumRepository, - }, - { - provide: ASSET_REPOSITORY, - useClass: AssetRepository, - }, - ], + providers: [AlbumService, ALBUM_REPOSITORY_PROVIDER], + exports: [ALBUM_REPOSITORY_PROVIDER], }) export class AlbumModule {} diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts index 2aa5fc6418..bd8d1ab6ce 100644 --- a/server/apps/immich/src/api-v1/asset/asset-repository.ts +++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts @@ -1,7 +1,7 @@ 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 { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm/repository/Repository'; import { CreateAssetDto } from './dto/create-asset.dto'; @@ -14,6 +14,7 @@ import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto'; import { In } from 'typeorm/find-options/operator/In'; import { UpdateAssetDto } from './dto/update-asset.dto'; +import { ITagRepository, TAG_REPOSITORY } from '../tag/tag.repository'; export interface IAssetRepository { create( @@ -25,7 +26,7 @@ export interface IAssetRepository { checksum?: Buffer, livePhotoAssetEntity?: AssetEntity, ): Promise; - update(asset: AssetEntity, dto: UpdateAssetDto): Promise; + update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise; getAllByUserId(userId: string, skip?: number): Promise; getAllByDeviceId(userId: string, deviceId: string): Promise; getById(assetId: string): Promise; @@ -53,6 +54,8 @@ export class AssetRepository implements IAssetRepository { constructor( @InjectRepository(AssetEntity) private assetRepository: Repository, + + @Inject(TAG_REPOSITORY) private _tagRepository: ITagRepository, ) {} async getAssetWithNoSmartInfo(): Promise { @@ -222,7 +225,7 @@ export class AssetRepository implements IAssetRepository { where: { id: assetId, }, - relations: ['exifInfo'], + relations: ['exifInfo', 'tags'], }); } @@ -237,9 +240,9 @@ export class AssetRepository implements IAssetRepository { .andWhere('asset.resizePath is not NULL') .andWhere('asset.isVisible = true') .leftJoinAndSelect('asset.exifInfo', 'exifInfo') + .leftJoinAndSelect('asset.tags', 'tags') .skip(skip || 0) .orderBy('asset.createdAt', 'DESC'); - return await query.getMany(); } @@ -286,9 +289,14 @@ export class AssetRepository implements IAssetRepository { /** * Update asset */ - async update(asset: AssetEntity, dto: UpdateAssetDto): Promise { + async update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise { asset.isFavorite = dto.isFavorite ?? asset.isFavorite; + if (dto.tagIds) { + const tags = await this._tagRepository.getByIds(userId, dto.tagIds); + asset.tags = tags; + } + return await this.assetRepository.save(asset); } @@ -347,10 +355,10 @@ export class AssetRepository implements IAssetRepository { async countByIdAndUser(assetId: string, userId: string): Promise { return await this.assetRepository.count({ - where: { - id: assetId, - userId - } + where: { + id: assetId, + userId, + }, }); } } 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 83befba8b3..7148376bfb 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -216,14 +216,14 @@ export class AssetController { /** * Update an asset */ - @Put('/assetById/:assetId') - async updateAssetById( + @Put('/:assetId') + async updateAsset( @GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId: string, - @Body() dto: UpdateAssetDto, + @Body(ValidationPipe) dto: UpdateAssetDto, ): Promise { await this.assetService.checkAssetsAccess(authUser, [assetId], true); - return await this.assetService.updateAssetById(assetId, dto); + return await this.assetService.updateAsset(authUser, assetId, dto); } @Delete('/') 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 d6d3b98196..c8501426e2 100644 --- a/server/apps/immich/src/api-v1/asset/asset.module.ts +++ b/server/apps/immich/src/api-v1/asset/asset.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { AssetService } from './asset.service'; import { AssetController } from './asset.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -10,18 +10,25 @@ import { CommunicationModule } from '../communication/communication.module'; import { QueueNameEnum } from '@app/job/constants/queue-name.constant'; import { AssetRepository, ASSET_REPOSITORY } from './asset-repository'; import { DownloadModule } from '../../modules/download/download.module'; -import { ALBUM_REPOSITORY, AlbumRepository } from '../album/album-repository'; -import { AlbumEntity } from '@app/database/entities/album.entity'; -import { UserAlbumEntity } from '@app/database/entities/user-album.entity'; -import { UserEntity } from '@app/database/entities/user.entity'; -import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity'; +import { TagModule } from '../tag/tag.module'; +import { AlbumModule } from '../album/album.module'; +import { UserModule } from '../user/user.module'; + +const ASSET_REPOSITORY_PROVIDER = { + provide: ASSET_REPOSITORY, + useClass: AssetRepository, +}; @Module({ imports: [ + TypeOrmModule.forFeature([AssetEntity]), CommunicationModule, BackgroundTaskModule, DownloadModule, - TypeOrmModule.forFeature([AssetEntity, AlbumEntity, UserAlbumEntity, UserEntity, AssetAlbumEntity]), + UserModule, + AlbumModule, + TagModule, + forwardRef(() => AlbumModule), BullModule.registerQueue({ name: QueueNameEnum.ASSET_UPLOADED, defaultJobOptions: { @@ -40,18 +47,7 @@ import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity'; }), ], controllers: [AssetController], - providers: [ - AssetService, - BackgroundTaskService, - { - provide: ASSET_REPOSITORY, - useClass: AssetRepository, - }, - { - provide: ALBUM_REPOSITORY, - useClass: AlbumRepository, - }, - ], - exports: [AssetService], + providers: [AssetService, BackgroundTaskService, ASSET_REPOSITORY_PROVIDER], + exports: [ASSET_REPOSITORY_PROVIDER], }) export class AssetModule {} 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 6c9d72a0d5..577e08525c 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -231,13 +231,13 @@ export class AssetService { return mapAsset(asset); } - public async updateAssetById(assetId: string, dto: UpdateAssetDto): Promise { + public async updateAsset(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise { const asset = await this._assetRepository.getById(assetId); if (!asset) { throw new BadRequestException('Asset not found'); } - const updatedAsset = await this._assetRepository.update(asset, dto); + const updatedAsset = await this._assetRepository.update(authUser.id, asset, dto); return mapAsset(updatedAsset); } diff --git a/server/apps/immich/src/api-v1/asset/dto/update-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/update-asset.dto.ts index 38122e3262..9c18c50a76 100644 --- a/server/apps/immich/src/api-v1/asset/dto/update-asset.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/update-asset.dto.ts @@ -1,6 +1,24 @@ -import { IsBoolean } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class UpdateAssetDto { + @IsOptional() @IsBoolean() - isFavorite!: boolean; + isFavorite?: boolean; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + @ApiProperty({ + isArray: true, + type: String, + title: 'Array of tag IDs to add to the asset', + example: [ + 'bf973405-3f2a-48d2-a687-2ed4167164be', + 'dd41870b-5d00-46d2-924e-1d8489a0aa0f', + 'fad77c3f-deef-4e7e-9608-14c1aa4e559a', + ], + }) + tagIds?: string[]; } diff --git a/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts b/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts index 09d61aae51..840d47c274 100644 --- a/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts +++ b/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts @@ -1,5 +1,6 @@ import { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; import { ApiProperty } from '@nestjs/swagger'; +import { mapTag, TagResponseDto } from '../../tag/response-dto/tag-response.dto'; import { ExifResponseDto, mapExif } from './exif-response.dto'; import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto'; @@ -23,6 +24,7 @@ export class AssetResponseDto { exifInfo?: ExifResponseDto; smartInfo?: SmartInfoResponseDto; livePhotoVideoId?: string | null; + tags!: TagResponseDto[]; } export function mapAsset(entity: AssetEntity): AssetResponseDto { @@ -44,5 +46,6 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto { exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, livePhotoVideoId: entity.livePhotoVideoId, + tags: entity.tags?.map(mapTag), }; } diff --git a/server/apps/immich/src/api-v1/job/job.module.ts b/server/apps/immich/src/api-v1/job/job.module.ts index 2cb5beb7bf..09ac3885ad 100644 --- a/server/apps/immich/src/api-v1/job/job.module.ts +++ b/server/apps/immich/src/api-v1/job/job.module.ts @@ -5,18 +5,21 @@ import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; import { JwtModule } from '@nestjs/jwt'; import { jwtConfig } from '../../config/jwt.config'; -import { UserEntity } from '@app/database/entities/user.entity'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BullModule } from '@nestjs/bull'; import { QueueNameEnum } from '@app/job'; -import { AssetEntity } from '@app/database/entities/asset.entity'; import { ExifEntity } from '@app/database/entities/exif.entity'; -import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository'; +import { TagModule } from '../tag/tag.module'; +import { AssetModule } from '../asset/asset.module'; +import { UserModule } from '../user/user.module'; @Module({ imports: [ - TypeOrmModule.forFeature([UserEntity, AssetEntity, ExifEntity]), + TypeOrmModule.forFeature([ExifEntity]), ImmichJwtModule, + TagModule, + AssetModule, + UserModule, JwtModule.register(jwtConfig), BullModule.registerQueue( { @@ -70,13 +73,6 @@ import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository'; ), ], controllers: [JobController], - providers: [ - JobService, - ImmichJwtService, - { - provide: ASSET_REPOSITORY, - useClass: AssetRepository, - }, - ], + providers: [JobService, ImmichJwtService], }) export class JobModule {} diff --git a/server/apps/immich/src/api-v1/tag/dto/create-tag.dto.ts b/server/apps/immich/src/api-v1/tag/dto/create-tag.dto.ts new file mode 100644 index 0000000000..3c3859291d --- /dev/null +++ b/server/apps/immich/src/api-v1/tag/dto/create-tag.dto.ts @@ -0,0 +1,14 @@ +import { TagType } from '@app/database/entities/tag.entity'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; + +export class CreateTagDto { + @IsString() + @IsNotEmpty() + name!: string; + + @IsEnum(TagType) + @IsNotEmpty() + @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) + type!: TagType; +} diff --git a/server/apps/immich/src/api-v1/tag/dto/update-tag.dto.ts b/server/apps/immich/src/api-v1/tag/dto/update-tag.dto.ts new file mode 100644 index 0000000000..64632f1f6d --- /dev/null +++ b/server/apps/immich/src/api-v1/tag/dto/update-tag.dto.ts @@ -0,0 +1,11 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class UpdateTagDto { + @IsString() + @IsOptional() + name?: string; + + @IsString() + @IsOptional() + renameTagId?: string; +} diff --git a/server/apps/immich/src/api-v1/tag/response-dto/tag-response.dto.ts b/server/apps/immich/src/api-v1/tag/response-dto/tag-response.dto.ts new file mode 100644 index 0000000000..973b47db6c --- /dev/null +++ b/server/apps/immich/src/api-v1/tag/response-dto/tag-response.dto.ts @@ -0,0 +1,20 @@ +import { TagEntity, TagType } from '@app/database/entities/tag.entity'; +import { ApiProperty } from '@nestjs/swagger'; + +export class TagResponseDto { + @ApiProperty() + id!: string; + + @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) + type!: string; + + name!: string; +} + +export function mapTag(entity: TagEntity): TagResponseDto { + return { + id: entity.id, + type: entity.type, + name: entity.name, + }; +} diff --git a/server/apps/immich/src/api-v1/tag/tag.controller.ts b/server/apps/immich/src/api-v1/tag/tag.controller.ts new file mode 100644 index 0000000000..fb2d79f82d --- /dev/null +++ b/server/apps/immich/src/api-v1/tag/tag.controller.ts @@ -0,0 +1,44 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, ValidationPipe } from '@nestjs/common'; +import { TagService } from './tag.service'; +import { CreateTagDto } from './dto/create-tag.dto'; +import { UpdateTagDto } from './dto/update-tag.dto'; +import { Authenticated } from '../../decorators/authenticated.decorator'; +import { ApiTags } from '@nestjs/swagger'; +import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; +import { TagEntity } from '@app/database/entities/tag.entity'; + +@Authenticated() +@ApiTags('Tag') +@Controller('tag') +export class TagController { + constructor(private readonly tagService: TagService) {} + + @Post() + create(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createTagDto: CreateTagDto): Promise { + return this.tagService.create(authUser, createTagDto); + } + + @Get() + findAll(@GetAuthUser() authUser: AuthUserDto) { + return this.tagService.findAll(authUser); + } + + @Get(':id') + findOne(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string) { + return this.tagService.findOne(authUser, id); + } + + @Patch(':id') + update( + @GetAuthUser() authUser: AuthUserDto, + @Param('id') id: string, + @Body(ValidationPipe) updateTagDto: UpdateTagDto, + ) { + return this.tagService.update(authUser, id, updateTagDto); + } + + @Delete(':id') + delete(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string) { + return this.tagService.remove(authUser, id); + } +} diff --git a/server/apps/immich/src/api-v1/tag/tag.module.ts b/server/apps/immich/src/api-v1/tag/tag.module.ts new file mode 100644 index 0000000000..5a9db11e78 --- /dev/null +++ b/server/apps/immich/src/api-v1/tag/tag.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { TagService } from './tag.service'; +import { TagController } from './tag.controller'; +import { TagEntity } from '@app/database/entities/tag.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TagRepository, TAG_REPOSITORY } from './tag.repository'; + +const TAG_REPOSITORY_PROVIDER = { + provide: TAG_REPOSITORY, + useClass: TagRepository, +}; +@Module({ + imports: [TypeOrmModule.forFeature([TagEntity])], + controllers: [TagController], + providers: [TagService, TAG_REPOSITORY_PROVIDER], + exports: [TAG_REPOSITORY_PROVIDER], +}) +export class TagModule {} diff --git a/server/apps/immich/src/api-v1/tag/tag.repository.ts b/server/apps/immich/src/api-v1/tag/tag.repository.ts new file mode 100644 index 0000000000..a1a383aecd --- /dev/null +++ b/server/apps/immich/src/api-v1/tag/tag.repository.ts @@ -0,0 +1,61 @@ +import { TagEntity, TagType } from '@app/database/entities/tag.entity'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { UpdateTagDto } from './dto/update-tag.dto'; + +export interface ITagRepository { + create(userId: string, tagType: TagType, tagName: string): Promise; + getByIds(userId: string, tagIds: string[]): Promise; + getById(tagId: string, userId: string): Promise; + getByUserId(userId: string): Promise; + update(tag: TagEntity, updateTagDto: UpdateTagDto): Promise; + remove(tag: TagEntity): Promise; +} + +export const TAG_REPOSITORY = 'TAG_REPOSITORY'; + +@Injectable() +export class TagRepository implements ITagRepository { + constructor( + @InjectRepository(TagEntity) + private tagRepository: Repository, + ) {} + + async create(userId: string, tagType: TagType, tagName: string): Promise { + const tag = new TagEntity(); + tag.name = tagName; + tag.type = tagType; + tag.userId = userId; + + return this.tagRepository.save(tag); + } + + async getById(tagId: string, userId: string): Promise { + return await this.tagRepository.findOne({ where: { id: tagId, userId }, relations: ['user'] }); + } + + async getByIds(userId: string, tagIds: string[]): Promise { + return await this.tagRepository.find({ + where: { id: In(tagIds), userId }, + relations: { + user: true, + }, + }); + } + + async getByUserId(userId: string): Promise { + return await this.tagRepository.find({ where: { userId } }); + } + + async update(tag: TagEntity, updateTagDto: UpdateTagDto): Promise { + tag.name = updateTagDto.name ?? tag.name; + tag.renameTagId = updateTagDto.renameTagId ?? tag.renameTagId; + + return this.tagRepository.save(tag); + } + + async remove(tag: TagEntity): Promise { + return await this.tagRepository.remove(tag); + } +} diff --git a/server/apps/immich/src/api-v1/tag/tag.service.spec.ts b/server/apps/immich/src/api-v1/tag/tag.service.spec.ts new file mode 100644 index 0000000000..3ab1d3034a --- /dev/null +++ b/server/apps/immich/src/api-v1/tag/tag.service.spec.ts @@ -0,0 +1,91 @@ +import { TagEntity, TagType } from '@app/database/entities/tag.entity'; +import { UserEntity } from '@app/database/entities/user.entity'; +import { AuthUserDto } from '../../decorators/auth-user.decorator'; +import { ITagRepository } from './tag.repository'; +import { TagService } from './tag.service'; + +describe('TagService', () => { + let sut: TagService; + let tagRepositoryMock: jest.Mocked; + + const user1AuthUser: AuthUserDto = Object.freeze({ + id: '1111', + email: 'testuser@email.com', + }); + + const user1: UserEntity = Object.freeze({ + id: '1111', + firstName: 'Alex', + lastName: 'Tran', + isAdmin: true, + email: 'testuser@email.com', + profileImagePath: '', + shouldChangePassword: true, + createdAt: '2022-12-02T19:29:23.603Z', + deletedAt: undefined, + tags: [], + oauthId: 'oauth-id-1', + }); + + // const user2: UserEntity = Object.freeze({ + // id: '2222', + // firstName: 'Alex', + // lastName: 'Tran', + // isAdmin: true, + // email: 'testuser2@email.com', + // profileImagePath: '', + // shouldChangePassword: true, + // createdAt: '2022-12-02T19:29:23.603Z', + // deletedAt: undefined, + // tags: [], + // oauthId: 'oauth-id-2', + // }); + + const user1Tag1: TagEntity = Object.freeze({ + name: 'user 1 tag 1', + type: TagType.CUSTOM, + userId: user1.id, + user: user1, + renameTagId: '', + id: 'user1-tag-1-id', + assets: [], + }); + + // const user1Tag2: TagEntity = Object.freeze({ + // name: 'user 1 tag 2', + // type: TagType.CUSTOM, + // userId: user1.id, + // user: user1, + // renameTagId: '', + // id: 'user1-tag-2-id', + // assets: [], + // }); + + beforeAll(() => { + tagRepositoryMock = { + create: jest.fn(), + getByIds: jest.fn(), + getById: jest.fn(), + getByUserId: jest.fn(), + remove: jest.fn(), + update: jest.fn(), + }; + + sut = new TagService(tagRepositoryMock); + }); + + it('creates tag', async () => { + const createTagDto = { + name: 'user 1 tag 1', + type: TagType.CUSTOM, + }; + + tagRepositoryMock.create.mockResolvedValue(user1Tag1); + + const result = await sut.create(user1AuthUser, createTagDto); + + expect(result.userId).toEqual(user1AuthUser.id); + expect(result.name).toEqual(createTagDto.name); + expect(result.type).toEqual(createTagDto.type); + }); +}); diff --git a/server/apps/immich/src/api-v1/tag/tag.service.ts b/server/apps/immich/src/api-v1/tag/tag.service.ts new file mode 100644 index 0000000000..e14e978801 --- /dev/null +++ b/server/apps/immich/src/api-v1/tag/tag.service.ts @@ -0,0 +1,48 @@ +import { TagEntity } from '@app/database/entities/tag.entity'; +import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; +import { AuthUserDto } from '../../decorators/auth-user.decorator'; +import { CreateTagDto } from './dto/create-tag.dto'; +import { UpdateTagDto } from './dto/update-tag.dto'; +import { ITagRepository, TAG_REPOSITORY } from './tag.repository'; + +@Injectable() +export class TagService { + readonly logger = new Logger(TagService.name); + + constructor(@Inject(TAG_REPOSITORY) private _tagRepository: ITagRepository) {} + + async create(authUser: AuthUserDto, createTagDto: CreateTagDto) { + try { + return await this._tagRepository.create(authUser.id, createTagDto.type, createTagDto.name); + } catch (e: any) { + this.logger.error(e, e.stack); + throw new BadRequestException(`Failed to create tag: ${e.detail}`); + } + } + + async findAll(authUser: AuthUserDto) { + return await this._tagRepository.getByUserId(authUser.id); + } + + async findOne(authUser: AuthUserDto, id: string): Promise { + const tag = await this._tagRepository.getById(id, authUser.id); + + if (!tag) { + throw new BadRequestException('Tag not found'); + } + + return tag; + } + + async update(authUser: AuthUserDto, id: string, updateTagDto: UpdateTagDto) { + const tag = await this.findOne(authUser, id); + + return this._tagRepository.update(tag, updateTagDto); + } + + async remove(authUser: AuthUserDto, id: string) { + const tag = await this.findOne(authUser, id); + + return this._tagRepository.remove(tag); + } +} diff --git a/server/apps/immich/src/api-v1/user/user.service.spec.ts b/server/apps/immich/src/api-v1/user/user.service.spec.ts index 49d82938ab..126d925607 100644 --- a/server/apps/immich/src/api-v1/user/user.service.spec.ts +++ b/server/apps/immich/src/api-v1/user/user.service.spec.ts @@ -31,6 +31,7 @@ describe('UserService', () => { shouldChangePassword: false, profileImagePath: '', createdAt: '2021-01-01', + tags: [], }); const immichUser: UserEntity = Object.freeze({ @@ -45,6 +46,7 @@ describe('UserService', () => { shouldChangePassword: false, profileImagePath: '', createdAt: '2021-01-01', + tags: [], }); const updatedImmichUser: UserEntity = Object.freeze({ @@ -59,6 +61,7 @@ describe('UserService', () => { shouldChangePassword: true, profileImagePath: '', createdAt: '2021-01-01', + tags: [], }); beforeAll(() => { diff --git a/server/apps/immich/src/app.module.ts b/server/apps/immich/src/app.module.ts index 487847bbc5..25ed0ab358 100644 --- a/server/apps/immich/src/app.module.ts +++ b/server/apps/immich/src/app.module.ts @@ -18,6 +18,7 @@ import { DatabaseModule } from '@app/database'; import { JobModule } from './api-v1/job/job.module'; import { SystemConfigModule } from './api-v1/system-config/system-config.module'; import { OAuthModule } from './api-v1/oauth/oauth.module'; +import { TagModule } from './api-v1/tag/tag.module'; @Module({ imports: [ @@ -63,6 +64,8 @@ import { OAuthModule } from './api-v1/oauth/oauth.module'; JobModule, SystemConfigModule, + + TagModule, ], controllers: [AppController], providers: [], diff --git a/server/apps/immich/src/main.ts b/server/apps/immich/src/main.ts index cd1cd22d23..3e5e3fb2ae 100644 --- a/server/apps/immich/src/main.ts +++ b/server/apps/immich/src/main.ts @@ -55,7 +55,7 @@ async function bootstrap() { if (process.env.NODE_ENV == 'development') { // Generate API Documentation only in development mode const outputPath = path.resolve(process.cwd(), 'immich-openapi-specs.json'); - writeFileSync(outputPath, JSON.stringify(apiDocument), { encoding: 'utf8' }); + writeFileSync(outputPath, JSON.stringify(apiDocument, null, 2), { encoding: 'utf8' }); Logger.log( `Running Immich Server in DEVELOPMENT environment - version ${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`, 'ImmichServer', diff --git a/server/apps/immich/src/modules/immich-jwt/immich-jwt.service.spec.ts b/server/apps/immich/src/modules/immich-jwt/immich-jwt.service.spec.ts index 9936f9de35..7edf313997 100644 --- a/server/apps/immich/src/modules/immich-jwt/immich-jwt.service.spec.ts +++ b/server/apps/immich/src/modules/immich-jwt/immich-jwt.service.spec.ts @@ -56,6 +56,7 @@ describe('ImmichJwtService', () => { profileImagePath: '', shouldChangePassword: false, createdAt: 'today', + tags: [], }; const dto: LoginResponseDto = { diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index bee972c82f..7ff2daa56f 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1 +1,3975 @@ -{"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/{userId}":{"delete":{"operationId":"deleteUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/{userId}/restore":{"post":{"operationId":"restoreUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/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/{assetId}":{"get":{"operationId":"downloadFile","parameters":[{"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"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download-library":{"get":{"operationId":"downloadLibrary","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file/{assetId}":{"get":{"operationId":"serveFile","parameters":[{"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"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"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-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[{"name":"if-none-match","in":"header","description":"ETag of data already cached on the client","required":false,"schema":{"type":"string"}}],"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/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"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":[]}]},"put":{"operationId":"updateAssetById","summary":"","description":"Update an asset","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAssetDto"}}}},"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":[]}]}},"/asset/exist":{"post":{"operationId":"checkExistingAssets","summary":"","description":"Checks if multiple assets exist on the server and returns all existing - used by background backup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsResponseDto"}}}}},"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"]}},"/oauth/config":{"post":{"operationId":"generateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigResponseDto"}}}}},"tags":["OAuth"]}},"/oauth/callback":{"post":{"operationId":"callback","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthCallbackDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["OAuth"]}},"/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"]}},"/server-info/stats":{"get":{"operationId":"getStats","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerStatsResponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/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"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"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/AddAssetsResponseDto"}}}}},"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":[]}]}},"/album/{albumId}/download":{"get":{"operationId":"downloadArchive","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/system-config":{"get":{"operationId":"getConfig","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]},"put":{"operationId":"updateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSystemConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"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"},"deletedAt":{"format":"date-time","type":"string"}},"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":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"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},"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"},"livePhotoVideoId":{"type":"string","nullable":true}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"audio":{"type":"integer","default":0},"photos":{"type":"integer","default":0},"videos":{"type":"integer","default":0},"other":{"type":"integer","default":0},"total":{"type":"integer","default":0}},"required":["audio","photos","videos","other","total"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"UpdateAssetDto":{"type":"object","properties":{"isFavorite":{"type":"boolean"}},"required":["isFavorite"]},"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"]},"CheckExistingAssetsDto":{"type":"object","properties":{"deviceAssetIds":{"type":"array","items":{"type":"string"}},"deviceId":{"type":"string"}},"required":["deviceAssetIds","deviceId"]},"CheckExistingAssetsResponseDto":{"type":"object","properties":{"existingIds":{"type":"array","items":{"type":"string"}}},"required":["existingIds"]},"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},"redirectUri":{"type":"string","readOnly":true}},"required":["successful","redirectUri"]},"OAuthConfigDto":{"type":"object","properties":{"redirectUri":{"type":"string"}},"required":["redirectUri"]},"OAuthConfigResponseDto":{"type":"object","properties":{"enabled":{"type":"boolean","readOnly":true},"url":{"type":"string","readOnly":true},"buttonText":{"type":"string","readOnly":true}},"required":["enabled"]},"OAuthCallbackDto":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]},"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","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"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"]},"UsageByUserDto":{"type":"object","properties":{"userId":{"type":"string"},"videos":{"type":"integer"},"photos":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"}},"required":["userId","videos","photos","usageRaw","usage"]},"ServerStatsResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"},"objects":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"},"usageByUser":{"title":"Array of usage for each user","example":[{"photos":1,"videos":1,"diskUsageRaw":1}],"type":"array","items":{"$ref":"#/components/schemas/UsageByUserDto"}}},"required":["photos","videos","objects","usageRaw","usage","usageByUser"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"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"]},"AddAssetsResponseDto":{"type":"object","properties":{"successfullyAdded":{"type":"integer"},"alreadyInAlbum":{"type":"array","items":{"type":"string"}},"album":{"$ref":"#/components/schemas/AlbumResponseDto"}},"required":["successfullyAdded","alreadyInAlbum"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"delayed":{"type":"integer"},"waiting":{"type":"integer"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]},"SystemConfigKey":{"type":"string","enum":["ffmpeg_crf","ffmpeg_preset","ffmpeg_target_video_codec","ffmpeg_target_audio_codec","ffmpeg_target_scaling"]},"SystemConfigResponseItem":{"type":"object","properties":{"name":{"type":"string"},"key":{"$ref":"#/components/schemas/SystemConfigKey"},"value":{"type":"string"},"defaultValue":{"type":"string"}},"required":["name","key","value","defaultValue"]},"SystemConfigResponseDto":{"type":"object","properties":{"config":{"type":"array","items":{"$ref":"#/components/schemas/SystemConfigResponseItem"}}},"required":["config"]},"UpdateSystemConfigDto":{"type":"object","properties":{}}}}} \ 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/{userId}": { + "delete": { + "operationId": "deleteUser", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponseDto" + } + } + } + } + }, + "tags": [ + "User" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/user/{userId}/restore": { + "post": { + "operationId": "restoreUser", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponseDto" + } + } + } + } + }, + "tags": [ + "User" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/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/{assetId}": { + "get": { + "operationId": "downloadFile", + "parameters": [ + { + "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" + } + }, + { + "name": "assetId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Asset" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/asset/download-library": { + "get": { + "operationId": "downloadLibrary", + "parameters": [ + { + "name": "skip", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Asset" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/asset/file/{assetId}": { + "get": { + "operationId": "serveFile", + "parameters": [ + { + "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" + } + }, + { + "name": "assetId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "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-time-bucket": { + "post": { + "operationId": "getAssetCountByTimeBucket", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAssetCountByTimeBucketDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetCountByTimeBucketResponseDto" + } + } + } + } + }, + "tags": [ + "Asset" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/asset/count-by-user-id": { + "get": { + "operationId": "getAssetCountByUserId", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetCountByUserIdResponseDto" + } + } + } + } + }, + "tags": [ + "Asset" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/asset": { + "get": { + "operationId": "getAllAssets", + "summary": "", + "description": "Get all AssetEntity belong to the user", + "parameters": [ + { + "name": "if-none-match", + "in": "header", + "description": "ETag of data already cached on the client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "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/time-bucket": { + "post": { + "operationId": "getAssetByTimeBucket", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAssetByTimeBucketDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + } + } + } + } + } + }, + "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/{assetId}": { + "put": { + "operationId": "updateAsset", + "summary": "", + "description": "Update an asset", + "parameters": [ + { + "name": "assetId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAssetDto" + } + } + } + }, + "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": [] + } + ] + } + }, + "/asset/exist": { + "post": { + "operationId": "checkExistingAssets", + "summary": "", + "description": "Checks if multiple assets exist on the server and returns all existing - used by background backup", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckExistingAssetsDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckExistingAssetsResponseDto" + } + } + } + } + }, + "tags": [ + "Asset" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/album/count-by-user-id": { + "get": { + "operationId": "getAlbumCountByUserId", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlbumCountResponseDto" + } + } + } + } + }, + "tags": [ + "Album" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/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" + } + }, + { + "name": "assetId", + "required": false, + "in": "query", + "description": "Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums", + "schema": { + "type": "string" + } + } + ], + "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/AddAssetsResponseDto" + } + } + } + } + }, + "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": [] + } + ] + } + }, + "/album/{albumId}/download": { + "get": { + "operationId": "downloadArchive", + "parameters": [ + { + "name": "albumId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Album" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/tag": { + "post": { + "operationId": "create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTagDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagEntity" + } + } + } + } + }, + "tags": [ + "Tag" + ] + }, + "get": { + "operationId": "findAll", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagEntity" + } + } + } + } + } + }, + "tags": [ + "Tag" + ] + } + }, + "/tag/{id}": { + "get": { + "operationId": "findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagEntity" + } + } + } + } + }, + "tags": [ + "Tag" + ] + }, + "patch": { + "operationId": "update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTagDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Tag" + ] + }, + "delete": { + "operationId": "delete", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagEntity" + } + } + } + } + }, + "tags": [ + "Tag" + ] + } + }, + "/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" + ] + } + }, + "/oauth/config": { + "post": { + "operationId": "generateConfig", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthConfigDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthConfigResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth" + ] + } + }, + "/oauth/callback": { + "post": { + "operationId": "callback", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthCallbackDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth" + ] + } + }, + "/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" + ] + } + }, + "/server-info/stats": { + "get": { + "operationId": "getStats", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerStatsResponseDto" + } + } + } + } + }, + "tags": [ + "Server Info" + ] + } + }, + "/jobs": { + "get": { + "operationId": "getAllJobsStatus", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllJobStatusResponseDto" + } + } + } + } + }, + "tags": [ + "Job" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/jobs/{jobId}": { + "get": { + "operationId": "getJobStatus", + "parameters": [ + { + "name": "jobId", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/JobId" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobStatusResponseDto" + } + } + } + } + }, + "tags": [ + "Job" + ], + "security": [ + { + "bearer": [] + } + ] + }, + "put": { + "operationId": "sendJobCommand", + "parameters": [ + { + "name": "jobId", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/JobId" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobCommandDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "number" + } + } + } + } + }, + "tags": [ + "Job" + ], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/system-config": { + "get": { + "operationId": "getConfig", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemConfigResponseDto" + } + } + } + } + }, + "tags": [ + "System Config" + ], + "security": [ + { + "bearer": [] + } + ] + }, + "put": { + "operationId": "updateConfig", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSystemConfigDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemConfigResponseDto" + } + } + } + } + }, + "tags": [ + "System Config" + ], + "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" + }, + "deletedAt": { + "format": "date-time", + "type": "string" + } + }, + "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": "integer", + "nullable": true, + "default": null, + "format": "int64" + }, + "fileSizeInByte": { + "type": "integer", + "nullable": true, + "default": null, + "format": "int64" + }, + "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 + }, + "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" + } + } + } + }, + "TagTypeEnum": { + "type": "string", + "enum": [ + "OBJECT", + "FACE", + "CUSTOM" + ] + }, + "TagResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/TagTypeEnum" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "type", + "name" + ] + }, + "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" + }, + "livePhotoVideoId": { + "type": "string", + "nullable": true + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagResponseDto" + } + } + }, + "required": [ + "type", + "id", + "deviceAssetId", + "ownerId", + "deviceId", + "originalPath", + "resizePath", + "createdAt", + "modifiedAt", + "isFavorite", + "mimeType", + "duration", + "webpPath", + "tags" + ] + }, + "TimeGroupEnum": { + "type": "string", + "enum": [ + "day", + "month" + ] + }, + "GetAssetCountByTimeBucketDto": { + "type": "object", + "properties": { + "timeGroup": { + "$ref": "#/components/schemas/TimeGroupEnum" + } + }, + "required": [ + "timeGroup" + ] + }, + "AssetCountByTimeBucket": { + "type": "object", + "properties": { + "timeBucket": { + "type": "string" + }, + "count": { + "type": "integer" + } + }, + "required": [ + "timeBucket", + "count" + ] + }, + "AssetCountByTimeBucketResponseDto": { + "type": "object", + "properties": { + "totalCount": { + "type": "integer" + }, + "buckets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssetCountByTimeBucket" + } + } + }, + "required": [ + "totalCount", + "buckets" + ] + }, + "AssetCountByUserIdResponseDto": { + "type": "object", + "properties": { + "audio": { + "type": "integer", + "default": 0 + }, + "photos": { + "type": "integer", + "default": 0 + }, + "videos": { + "type": "integer", + "default": 0 + }, + "other": { + "type": "integer", + "default": 0 + }, + "total": { + "type": "integer", + "default": 0 + } + }, + "required": [ + "audio", + "photos", + "videos", + "other", + "total" + ] + }, + "GetAssetByTimeBucketDto": { + "type": "object", + "properties": { + "timeBucket": { + "title": "Array of date time buckets", + "example": [ + "2015-06-01T00:00:00.000Z", + "2016-02-01T00:00:00.000Z", + "2016-03-01T00:00:00.000Z" + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "timeBucket" + ] + }, + "UpdateAssetDto": { + "type": "object", + "properties": { + "tagIds": { + "title": "Array of tag IDs to add to the asset", + "example": [ + "bf973405-3f2a-48d2-a687-2ed4167164be", + "dd41870b-5d00-46d2-924e-1d8489a0aa0f", + "fad77c3f-deef-4e7e-9608-14c1aa4e559a" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "isFavorite": { + "type": "boolean" + } + } + }, + "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" + ] + }, + "CheckExistingAssetsDto": { + "type": "object", + "properties": { + "deviceAssetIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "deviceId": { + "type": "string" + } + }, + "required": [ + "deviceAssetIds", + "deviceId" + ] + }, + "CheckExistingAssetsResponseDto": { + "type": "object", + "properties": { + "existingIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "existingIds" + ] + }, + "AlbumCountResponseDto": { + "type": "object", + "properties": { + "owned": { + "type": "integer" + }, + "shared": { + "type": "integer" + }, + "sharing": { + "type": "integer" + } + }, + "required": [ + "owned", + "shared", + "sharing" + ] + }, + "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" + ] + }, + "AddAssetsResponseDto": { + "type": "object", + "properties": { + "successfullyAdded": { + "type": "integer" + }, + "alreadyInAlbum": { + "type": "array", + "items": { + "type": "string" + } + }, + "album": { + "$ref": "#/components/schemas/AlbumResponseDto" + } + }, + "required": [ + "successfullyAdded", + "alreadyInAlbum" + ] + }, + "RemoveAssetsDto": { + "type": "object", + "properties": { + "assetIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "assetIds" + ] + }, + "UpdateAlbumDto": { + "type": "object", + "properties": { + "albumName": { + "type": "string" + }, + "albumThumbnailAssetId": { + "type": "string" + } + } + }, + "CreateTagDto": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/TagTypeEnum" + }, + "name": { + "type": "string" + } + }, + "required": [ + "type", + "name" + ] + }, + "ExifEntity": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "description": { + "type": "string", + "description": "General info" + }, + "exifImageWidth": { + "type": "number", + "nullable": true + }, + "exifImageHeight": { + "type": "number", + "nullable": true + }, + "fileSizeInByte": { + "type": "number", + "nullable": true + }, + "orientation": { + "type": "string", + "nullable": true + }, + "dateTimeOriginal": { + "format": "date-time", + "type": "string", + "nullable": true + }, + "modifyDate": { + "format": "date-time", + "type": "string", + "nullable": true + }, + "latitude": { + "type": "number", + "nullable": true + }, + "longitude": { + "type": "number", + "nullable": true + }, + "city": { + "type": "string", + "nullable": true + }, + "state": { + "type": "string", + "nullable": true + }, + "country": { + "type": "string", + "nullable": true + }, + "make": { + "type": "string", + "nullable": true, + "description": "Image info" + }, + "model": { + "type": "string", + "nullable": true + }, + "imageName": { + "type": "string", + "nullable": true + }, + "lensModel": { + "type": "string", + "nullable": true + }, + "fNumber": { + "type": "number", + "nullable": true + }, + "focalLength": { + "type": "number", + "nullable": true + }, + "iso": { + "type": "number", + "nullable": true + }, + "exposureTime": { + "type": "number", + "nullable": true + }, + "fps": { + "type": "number", + "nullable": true, + "description": "Video info" + }, + "asset": { + "$ref": "#/components/schemas/AssetEntity" + }, + "exifTextSearchableColumn": { + "type": "string" + } + }, + "required": [ + "id", + "assetId", + "description", + "exifImageWidth", + "exifImageHeight", + "fileSizeInByte", + "orientation", + "dateTimeOriginal", + "modifyDate", + "latitude", + "longitude", + "city", + "state", + "country", + "make", + "model", + "imageName", + "lensModel", + "fNumber", + "focalLength", + "iso", + "exposureTime", + "exifTextSearchableColumn" + ] + }, + "SmartInfoEntity": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "tags": { + "nullable": true, + "type": "array", + "items": { + "type": "string" + } + }, + "objects": { + "nullable": true, + "type": "array", + "items": { + "type": "string" + } + }, + "asset": { + "$ref": "#/components/schemas/AssetEntity" + } + }, + "required": [ + "id", + "assetId", + "tags", + "objects" + ] + }, + "UserEntity": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "isAdmin": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "salt": { + "type": "string" + }, + "oauthId": { + "type": "string" + }, + "profileImagePath": { + "type": "string" + }, + "shouldChangePassword": { + "type": "boolean" + }, + "createdAt": { + "type": "string" + }, + "deletedAt": { + "format": "date-time", + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagEntity" + } + } + }, + "required": [ + "id", + "firstName", + "lastName", + "isAdmin", + "email", + "oauthId", + "profileImagePath", + "shouldChangePassword", + "createdAt", + "tags" + ] + }, + "TagEntity": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "enum": [ + "OBJECT", + "FACE", + "CUSTOM" + ], + "type": "string" + }, + "name": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "renameTagId": { + "type": "string" + }, + "assets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssetEntity" + } + }, + "user": { + "$ref": "#/components/schemas/UserEntity" + } + }, + "required": [ + "id", + "type", + "name", + "userId", + "renameTagId", + "assets", + "user" + ] + }, + "AssetEntity": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "deviceAssetId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "deviceId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "IMAGE", + "VIDEO", + "AUDIO", + "OTHER" + ] + }, + "originalPath": { + "type": "string" + }, + "resizePath": { + "type": "string", + "nullable": true + }, + "webpPath": { + "type": "string", + "nullable": true + }, + "encodedVideoPath": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "isFavorite": { + "type": "boolean" + }, + "mimeType": { + "type": "string", + "nullable": true + }, + "checksum": { + "type": "object", + "nullable": true + }, + "duration": { + "type": "string", + "nullable": true + }, + "isVisible": { + "type": "boolean" + }, + "livePhotoVideoId": { + "type": "string", + "nullable": true + }, + "exifInfo": { + "$ref": "#/components/schemas/ExifEntity" + }, + "smartInfo": { + "$ref": "#/components/schemas/SmartInfoEntity" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagEntity" + } + } + }, + "required": [ + "id", + "deviceAssetId", + "userId", + "deviceId", + "type", + "originalPath", + "resizePath", + "webpPath", + "encodedVideoPath", + "createdAt", + "modifiedAt", + "isFavorite", + "mimeType", + "duration", + "isVisible", + "livePhotoVideoId", + "tags" + ] + }, + "UpdateTagDto": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "renameTagId": { + "type": "string" + } + } + }, + "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 + }, + "redirectUri": { + "type": "string", + "readOnly": true + } + }, + "required": [ + "successful", + "redirectUri" + ] + }, + "OAuthConfigDto": { + "type": "object", + "properties": { + "redirectUri": { + "type": "string" + } + }, + "required": [ + "redirectUri" + ] + }, + "OAuthConfigResponseDto": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "readOnly": true + }, + "url": { + "type": "string", + "readOnly": true + }, + "buttonText": { + "type": "string", + "readOnly": true + } + }, + "required": [ + "enabled" + ] + }, + "OAuthCallbackDto": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "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", + "format": "int64" + }, + "diskUseRaw": { + "type": "integer", + "format": "int64" + }, + "diskAvailableRaw": { + "type": "integer", + "format": "int64" + }, + "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" + ] + }, + "UsageByUserDto": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "videos": { + "type": "integer" + }, + "photos": { + "type": "integer" + }, + "usageRaw": { + "type": "integer", + "format": "int64" + }, + "usage": { + "type": "string" + } + }, + "required": [ + "userId", + "videos", + "photos", + "usageRaw", + "usage" + ] + }, + "ServerStatsResponseDto": { + "type": "object", + "properties": { + "photos": { + "type": "integer" + }, + "videos": { + "type": "integer" + }, + "objects": { + "type": "integer" + }, + "usageRaw": { + "type": "integer", + "format": "int64" + }, + "usage": { + "type": "string" + }, + "usageByUser": { + "title": "Array of usage for each user", + "example": [ + { + "photos": 1, + "videos": 1, + "diskUsageRaw": 1 + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/UsageByUserDto" + } + } + }, + "required": [ + "photos", + "videos", + "objects", + "usageRaw", + "usage", + "usageByUser" + ] + }, + "JobCounts": { + "type": "object", + "properties": { + "active": { + "type": "integer" + }, + "completed": { + "type": "integer" + }, + "failed": { + "type": "integer" + }, + "delayed": { + "type": "integer" + }, + "waiting": { + "type": "integer" + } + }, + "required": [ + "active", + "completed", + "failed", + "delayed", + "waiting" + ] + }, + "AllJobStatusResponseDto": { + "type": "object", + "properties": { + "thumbnailGenerationQueueCount": { + "$ref": "#/components/schemas/JobCounts" + }, + "metadataExtractionQueueCount": { + "$ref": "#/components/schemas/JobCounts" + }, + "videoConversionQueueCount": { + "$ref": "#/components/schemas/JobCounts" + }, + "machineLearningQueueCount": { + "$ref": "#/components/schemas/JobCounts" + }, + "isThumbnailGenerationActive": { + "type": "boolean" + }, + "isMetadataExtractionActive": { + "type": "boolean" + }, + "isVideoConversionActive": { + "type": "boolean" + }, + "isMachineLearningActive": { + "type": "boolean" + } + }, + "required": [ + "thumbnailGenerationQueueCount", + "metadataExtractionQueueCount", + "videoConversionQueueCount", + "machineLearningQueueCount", + "isThumbnailGenerationActive", + "isMetadataExtractionActive", + "isVideoConversionActive", + "isMachineLearningActive" + ] + }, + "JobId": { + "type": "string", + "enum": [ + "thumbnail-generation", + "metadata-extraction", + "video-conversion", + "machine-learning" + ] + }, + "JobStatusResponseDto": { + "type": "object", + "properties": { + "isActive": { + "type": "boolean" + }, + "queueCount": { + "type": "object" + } + }, + "required": [ + "isActive", + "queueCount" + ] + }, + "JobCommand": { + "type": "string", + "enum": [ + "start", + "stop" + ] + }, + "JobCommandDto": { + "type": "object", + "properties": { + "command": { + "$ref": "#/components/schemas/JobCommand" + } + }, + "required": [ + "command" + ] + }, + "SystemConfigKey": { + "type": "string", + "enum": [ + "ffmpeg_crf", + "ffmpeg_preset", + "ffmpeg_target_video_codec", + "ffmpeg_target_audio_codec", + "ffmpeg_target_scaling" + ] + }, + "SystemConfigResponseItem": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "key": { + "$ref": "#/components/schemas/SystemConfigKey" + }, + "value": { + "type": "string" + }, + "defaultValue": { + "type": "string" + } + }, + "required": [ + "name", + "key", + "value", + "defaultValue" + ] + }, + "SystemConfigResponseDto": { + "type": "object", + "properties": { + "config": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SystemConfigResponseItem" + } + } + }, + "required": [ + "config" + ] + }, + "UpdateSystemConfigDto": { + "type": "object", + "properties": {} + } + } + } +} \ No newline at end of file diff --git a/server/libs/database/src/entities/asset.entity.ts b/server/libs/database/src/entities/asset.entity.ts index 02c265e677..6ff49747ef 100644 --- a/server/libs/database/src/entities/asset.entity.ts +++ b/server/libs/database/src/entities/asset.entity.ts @@ -1,6 +1,7 @@ -import { Column, Entity, Index, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { Column, Entity, Index, JoinTable, ManyToMany, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; import { ExifEntity } from './exif.entity'; import { SmartInfoEntity } from './smart-info.entity'; +import { TagEntity } from './tag.entity'; @Entity('assets') @Unique('UQ_userid_checksum', ['userId', 'checksum']) @@ -62,6 +63,11 @@ export class AssetEntity { @OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset) smartInfo?: SmartInfoEntity; + + // https://github.com/typeorm/typeorm/blob/master/docs/many-to-many-relations.md + @ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true }) + @JoinTable({ name: 'tag_asset' }) + tags!: TagEntity[]; } export enum AssetType { diff --git a/server/libs/database/src/entities/tag.entity.ts b/server/libs/database/src/entities/tag.entity.ts new file mode 100644 index 0000000000..b3564337a6 --- /dev/null +++ b/server/libs/database/src/entities/tag.entity.ts @@ -0,0 +1,45 @@ +import { Column, Entity, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { AssetEntity } from './asset.entity'; +import { UserEntity } from './user.entity'; + +@Entity('tags') +@Unique('UQ_tag_name_userId', ['name', 'userId']) +export class TagEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column() + type!: TagType; + + @Column() + name!: string; + + @Column() + userId!: string; + + @Column({ type: 'uuid', comment: 'The new renamed tagId', nullable: true }) + renameTagId!: string; + + @ManyToMany(() => AssetEntity, (asset) => asset.tags) + assets!: AssetEntity[]; + + @ManyToOne(() => UserEntity, (user) => user.tags) + user!: UserEntity; +} + +export enum TagType { + /** + * Tag that is detected by the ML model for object detection will use this type + */ + OBJECT = 'OBJECT', + + /** + * Face that is detected by the ML model for facial detection (TBD/NOT YET IMPLEMENTED) will use this type + */ + FACE = 'FACE', + + /** + * Tag that is created by the user will use this type + */ + CUSTOM = 'CUSTOM', +} diff --git a/server/libs/database/src/entities/user.entity.ts b/server/libs/database/src/entities/user.entity.ts index c114101c64..13bfc3d2e0 100644 --- a/server/libs/database/src/entities/user.entity.ts +++ b/server/libs/database/src/entities/user.entity.ts @@ -1,4 +1,5 @@ -import { Column, CreateDateColumn, DeleteDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, CreateDateColumn, DeleteDateColumn, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { TagEntity } from './tag.entity'; @Entity('users') export class UserEntity { @@ -37,4 +38,7 @@ export class UserEntity { @DeleteDateColumn() deletedAt?: Date; + + @OneToMany(() => TagEntity, (tag) => tag.user) + tags!: TagEntity[]; } diff --git a/server/libs/database/src/migrations/1670257571385-CreateTagsTable.ts b/server/libs/database/src/migrations/1670257571385-CreateTagsTable.ts new file mode 100644 index 0000000000..0585aecc8c --- /dev/null +++ b/server/libs/database/src/migrations/1670257571385-CreateTagsTable.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateTagsTable1670257571385 implements MigrationInterface { + name = 'CreateTagsTable1670257571385' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "tags" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying NOT NULL, "name" character varying NOT NULL, "userId" uuid NOT NULL, "renameTagId" uuid, CONSTRAINT "UQ_tag_name_userId" UNIQUE ("name", "userId"), CONSTRAINT "PK_e7dc17249a1148a1970748eda99" PRIMARY KEY ("id")); COMMENT ON COLUMN "tags"."renameTagId" IS 'The new renamed tagId'`); + await queryRunner.query(`CREATE TABLE "tag_asset" ("assetsId" uuid NOT NULL, "tagsId" uuid NOT NULL, CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" PRIMARY KEY ("assetsId", "tagsId"))`); + await queryRunner.query(`CREATE INDEX "IDX_f8e8a9e893cb5c54907f1b798e" ON "tag_asset" ("assetsId") `); + await queryRunner.query(`CREATE INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4" ON "tag_asset" ("tagsId") `); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`); + await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`); + await queryRunner.query(`DROP INDEX "public"."IDX_e99f31ea4cdf3a2c35c7287eb4"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f8e8a9e893cb5c54907f1b798e"`); + await queryRunner.query(`DROP TABLE "tag_asset"`); + await queryRunner.query(`DROP TABLE "tags"`); + } + +} diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 233800bdfe..c9874b6811 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -325,6 +325,143 @@ export interface AssetCountByUserIdResponseDto { */ 'total': number; } +/** + * + * @export + * @interface AssetEntity + */ +export interface AssetEntity { + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'id': string; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'deviceAssetId': string; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'userId': string; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'deviceId': string; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'type': AssetEntityTypeEnum; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'originalPath': string; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'resizePath': string | null; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'webpPath': string | null; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'encodedVideoPath': string; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'createdAt': string; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'modifiedAt': string; + /** + * + * @type {boolean} + * @memberof AssetEntity + */ + 'isFavorite': boolean; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'mimeType': string | null; + /** + * + * @type {object} + * @memberof AssetEntity + */ + 'checksum'?: object | null; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'duration': string | null; + /** + * + * @type {boolean} + * @memberof AssetEntity + */ + 'isVisible': boolean; + /** + * + * @type {string} + * @memberof AssetEntity + */ + 'livePhotoVideoId': string | null; + /** + * + * @type {ExifEntity} + * @memberof AssetEntity + */ + 'exifInfo'?: ExifEntity; + /** + * + * @type {SmartInfoEntity} + * @memberof AssetEntity + */ + 'smartInfo'?: SmartInfoEntity; + /** + * + * @type {Array} + * @memberof AssetEntity + */ + 'tags': Array; +} + +export const AssetEntityTypeEnum = { + Image: 'IMAGE', + Video: 'VIDEO', + Audio: 'AUDIO', + Other: 'OTHER' +} as const; + +export type AssetEntityTypeEnum = typeof AssetEntityTypeEnum[keyof typeof AssetEntityTypeEnum]; + /** * * @export @@ -446,6 +583,12 @@ export interface AssetResponseDto { * @memberof AssetResponseDto */ 'livePhotoVideoId'?: string | null; + /** + * + * @type {Array} + * @memberof AssetResponseDto + */ + 'tags': Array; } /** * @@ -602,6 +745,25 @@ export interface CreateProfileImageResponseDto { */ 'profileImagePath': string; } +/** + * + * @export + * @interface CreateTagDto + */ +export interface CreateTagDto { + /** + * + * @type {TagTypeEnum} + * @memberof CreateTagDto + */ + 'type': TagTypeEnum; + /** + * + * @type {string} + * @memberof CreateTagDto + */ + 'name': string; +} /** * * @export @@ -811,6 +973,163 @@ export const DeviceTypeEnum = { export type DeviceTypeEnum = typeof DeviceTypeEnum[keyof typeof DeviceTypeEnum]; +/** + * + * @export + * @interface ExifEntity + */ +export interface ExifEntity { + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'id': string; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'assetId': string; + /** + * General info + * @type {string} + * @memberof ExifEntity + */ + 'description': string; + /** + * + * @type {number} + * @memberof ExifEntity + */ + 'exifImageWidth': number | null; + /** + * + * @type {number} + * @memberof ExifEntity + */ + 'exifImageHeight': number | null; + /** + * + * @type {number} + * @memberof ExifEntity + */ + 'fileSizeInByte': number | null; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'orientation': string | null; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'dateTimeOriginal': string | null; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'modifyDate': string | null; + /** + * + * @type {number} + * @memberof ExifEntity + */ + 'latitude': number | null; + /** + * + * @type {number} + * @memberof ExifEntity + */ + 'longitude': number | null; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'city': string | null; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'state': string | null; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'country': string | null; + /** + * Image info + * @type {string} + * @memberof ExifEntity + */ + 'make': string | null; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'model': string | null; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'imageName': string | null; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'lensModel': string | null; + /** + * + * @type {number} + * @memberof ExifEntity + */ + 'fNumber': number | null; + /** + * + * @type {number} + * @memberof ExifEntity + */ + 'focalLength': number | null; + /** + * + * @type {number} + * @memberof ExifEntity + */ + 'iso': number | null; + /** + * + * @type {number} + * @memberof ExifEntity + */ + 'exposureTime': number | null; + /** + * Video info + * @type {number} + * @memberof ExifEntity + */ + 'fps'?: number | null; + /** + * + * @type {AssetEntity} + * @memberof ExifEntity + */ + 'asset'?: AssetEntity; + /** + * + * @type {string} + * @memberof ExifEntity + */ + 'exifTextSearchableColumn': string; +} /** * * @export @@ -1400,6 +1719,43 @@ export interface SignUpDto { */ 'lastName': string; } +/** + * + * @export + * @interface SmartInfoEntity + */ +export interface SmartInfoEntity { + /** + * + * @type {string} + * @memberof SmartInfoEntity + */ + 'id': string; + /** + * + * @type {string} + * @memberof SmartInfoEntity + */ + 'assetId': string; + /** + * + * @type {Array} + * @memberof SmartInfoEntity + */ + 'tags': Array | null; + /** + * + * @type {Array} + * @memberof SmartInfoEntity + */ + 'objects': Array | null; + /** + * + * @type {AssetEntity} + * @memberof SmartInfoEntity + */ + 'asset'?: AssetEntity; +} /** * * @export @@ -1486,6 +1842,104 @@ export interface SystemConfigResponseItem { */ 'defaultValue': string; } +/** + * + * @export + * @interface TagEntity + */ +export interface TagEntity { + /** + * + * @type {string} + * @memberof TagEntity + */ + 'id': string; + /** + * + * @type {string} + * @memberof TagEntity + */ + 'type': TagEntityTypeEnum; + /** + * + * @type {string} + * @memberof TagEntity + */ + 'name': string; + /** + * + * @type {string} + * @memberof TagEntity + */ + 'userId': string; + /** + * + * @type {string} + * @memberof TagEntity + */ + 'renameTagId': string; + /** + * + * @type {Array} + * @memberof TagEntity + */ + 'assets': Array; + /** + * + * @type {UserEntity} + * @memberof TagEntity + */ + 'user': UserEntity; +} + +export const TagEntityTypeEnum = { + Object: 'OBJECT', + Face: 'FACE', + Custom: 'CUSTOM' +} as const; + +export type TagEntityTypeEnum = typeof TagEntityTypeEnum[keyof typeof TagEntityTypeEnum]; + +/** + * + * @export + * @interface TagResponseDto + */ +export interface TagResponseDto { + /** + * + * @type {string} + * @memberof TagResponseDto + */ + 'id': string; + /** + * + * @type {TagTypeEnum} + * @memberof TagResponseDto + */ + 'type': TagTypeEnum; + /** + * + * @type {string} + * @memberof TagResponseDto + */ + 'name': string; +} +/** + * + * @export + * @enum {string} + */ + +export const TagTypeEnum = { + Object: 'OBJECT', + Face: 'FACE', + Custom: 'CUSTOM' +} as const; + +export type TagTypeEnum = typeof TagTypeEnum[keyof typeof TagTypeEnum]; + + /** * * @export @@ -1539,12 +1993,18 @@ export interface UpdateAlbumDto { * @interface UpdateAssetDto */ export interface UpdateAssetDto { + /** + * + * @type {Array} + * @memberof UpdateAssetDto + */ + 'tagIds'?: Array; /** * * @type {boolean} * @memberof UpdateAssetDto */ - 'isFavorite': boolean; + 'isFavorite'?: boolean; } /** * @@ -1571,6 +2031,25 @@ export interface UpdateDeviceInfoDto { */ 'isAutoBackup'?: boolean; } +/** + * + * @export + * @interface UpdateTagDto + */ +export interface UpdateTagDto { + /** + * + * @type {string} + * @memberof UpdateTagDto + */ + 'name'?: string; + /** + * + * @type {string} + * @memberof UpdateTagDto + */ + 'renameTagId'?: string; +} /** * * @export @@ -1670,6 +2149,91 @@ export interface UserCountResponseDto { */ 'userCount': number; } +/** + * + * @export + * @interface UserEntity + */ +export interface UserEntity { + /** + * + * @type {string} + * @memberof UserEntity + */ + 'id': string; + /** + * + * @type {string} + * @memberof UserEntity + */ + 'firstName': string; + /** + * + * @type {string} + * @memberof UserEntity + */ + 'lastName': string; + /** + * + * @type {boolean} + * @memberof UserEntity + */ + 'isAdmin': boolean; + /** + * + * @type {string} + * @memberof UserEntity + */ + 'email': string; + /** + * + * @type {string} + * @memberof UserEntity + */ + 'password'?: string; + /** + * + * @type {string} + * @memberof UserEntity + */ + 'salt'?: string; + /** + * + * @type {string} + * @memberof UserEntity + */ + 'oauthId': string; + /** + * + * @type {string} + * @memberof UserEntity + */ + 'profileImagePath': string; + /** + * + * @type {boolean} + * @memberof UserEntity + */ + 'shouldChangePassword': boolean; + /** + * + * @type {string} + * @memberof UserEntity + */ + 'createdAt': string; + /** + * + * @type {string} + * @memberof UserEntity + */ + 'deletedAt'?: string; + /** + * + * @type {Array} + * @memberof UserEntity + */ + 'tags': Array; +} /** * * @export @@ -3246,12 +3810,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - updateAssetById: async (assetId: string, updateAssetDto: UpdateAssetDto, options: AxiosRequestConfig = {}): Promise => { + updateAsset: async (assetId: string, updateAssetDto: UpdateAssetDto, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'assetId' is not null or undefined - assertParamExists('updateAssetById', 'assetId', assetId) + assertParamExists('updateAsset', 'assetId', assetId) // verify required parameter 'updateAssetDto' is not null or undefined - assertParamExists('updateAssetById', 'updateAssetDto', updateAssetDto) - const localVarPath = `/asset/assetById/{assetId}` + assertParamExists('updateAsset', 'updateAssetDto', updateAssetDto) + const localVarPath = `/asset/{assetId}` .replace(`{${"assetId"}}`, encodeURIComponent(String(assetId))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -3520,8 +4084,8 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async updateAssetById(assetId: string, updateAssetDto: UpdateAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssetById(assetId, updateAssetDto, options); + async updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(assetId, updateAssetDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -3711,8 +4275,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - updateAssetById(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise { - return localVarFp.updateAssetById(assetId, updateAssetDto, options).then((request) => request(axios, basePath)); + updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise { + return localVarFp.updateAsset(assetId, updateAssetDto, options).then((request) => request(axios, basePath)); }, /** * @@ -3935,8 +4499,8 @@ export class AssetApi extends BaseAPI { * @throws {RequiredError} * @memberof AssetApi */ - public updateAssetById(assetId: string, updateAssetDto: UpdateAssetDto, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).updateAssetById(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath)); + public updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).updateAsset(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath)); } /** @@ -5250,6 +5814,363 @@ export class SystemConfigApi extends BaseAPI { } +/** + * TagApi - axios parameter creator + * @export + */ +export const TagApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + _delete: async (id: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('_delete', 'id', id) + const localVarPath = `/tag/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // 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: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {CreateTagDto} createTagDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + create: async (createTagDto: CreateTagDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createTagDto' is not null or undefined + assertParamExists('create', 'createTagDto', createTagDto) + const localVarPath = `/tag`; + // 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: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createTagDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + findAll: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/tag`; + // 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; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + findOne: async (id: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('findOne', 'id', id) + const localVarPath = `/tag/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // 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; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {UpdateTagDto} updateTagDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + update: async (id: string, updateTagDto: UpdateTagDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('update', 'id', id) + // verify required parameter 'updateTagDto' is not null or undefined + assertParamExists('update', 'updateTagDto', updateTagDto) + const localVarPath = `/tag/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // 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: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateTagDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * TagApi - functional programming interface + * @export + */ +export const TagApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = TagApiAxiosParamCreator(configuration) + return { + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async _delete(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator._delete(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {CreateTagDto} createTagDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async create(createTagDto: CreateTagDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.create(createTagDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async findAll(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.findAll(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async findOne(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.findOne(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} id + * @param {UpdateTagDto} updateTagDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async update(id: string, updateTagDto: UpdateTagDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.update(id, updateTagDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * TagApi - factory interface + * @export + */ +export const TagApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = TagApiFp(configuration) + return { + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + _delete(id: string, options?: any): AxiosPromise { + return localVarFp._delete(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {CreateTagDto} createTagDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + create(createTagDto: CreateTagDto, options?: any): AxiosPromise { + return localVarFp.create(createTagDto, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + findAll(options?: any): AxiosPromise> { + return localVarFp.findAll(options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + findOne(id: string, options?: any): AxiosPromise { + return localVarFp.findOne(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {string} id + * @param {UpdateTagDto} updateTagDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + update(id: string, updateTagDto: UpdateTagDto, options?: any): AxiosPromise { + return localVarFp.update(id, updateTagDto, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * TagApi - object-oriented interface + * @export + * @class TagApi + * @extends {BaseAPI} + */ +export class TagApi extends BaseAPI { + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TagApi + */ + public _delete(id: string, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration)._delete(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {CreateTagDto} createTagDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TagApi + */ + public create(createTagDto: CreateTagDto, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).create(createTagDto, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TagApi + */ + public findAll(options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).findAll(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TagApi + */ + public findOne(id: string, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).findOne(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {string} id + * @param {UpdateTagDto} updateTagDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TagApi + */ + public update(id: string, updateTagDto: UpdateTagDto, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).update(id, updateTagDto, options).then((request) => request(this.axios, this.basePath)); + } +} + + /** * UserApi - axios parameter creator * @export diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index cd46776e9b..17f58a64f3 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -183,7 +183,7 @@ }; const toggleFavorite = async () => { - const { data } = await api.assetApi.updateAssetById(asset.id, { + const { data } = await api.assetApi.updateAsset(asset.id, { isFavorite: !asset.isFavorite }); diff --git a/web/src/lib/components/shared-components/status-box.svelte b/web/src/lib/components/shared-components/status-box.svelte index b8d5c5feb7..a7271b469d 100644 --- a/web/src/lib/components/shared-components/status-box.svelte +++ b/web/src/lib/components/shared-components/status-box.svelte @@ -62,7 +62,9 @@ style={`width: ${getStorageUsagePercentage()}%`} /> -

{asByteUnitString(serverInfo?.diskUseRaw)} of {asByteUnitString(serverInfo?.diskSizeRaw)} used

+

+ {asByteUnitString(serverInfo?.diskUseRaw)} of {asByteUnitString(serverInfo?.diskSizeRaw)} used +

{:else}
diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index e393f61dd1..c8eb90246a 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -115,9 +115,7 @@