diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 2baf6e9ae5..20da0c5201 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -177,13 +177,9 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- - name: Any jobs failed?
- if: ${{ contains(needs.*.result, 'failure') }}
- run: exit 1
- - name: All jobs passed or skipped
- if: ${{ !(contains(needs.*.result, 'failure')) }}
- # zizmor: ignore[template-injection]
- run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
+ - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
+ with:
+ needs: ${{ toJSON(needs) }}
success-check-ml:
name: Docker Build & Push ML Success
@@ -192,10 +188,6 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- - name: Any jobs failed?
- if: ${{ contains(needs.*.result, 'failure') }}
- run: exit 1
- - name: All jobs passed or skipped
- if: ${{ !(contains(needs.*.result, 'failure')) }}
- # zizmor: ignore[template-injection]
- run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
+ - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
+ with:
+ needs: ${{ toJSON(needs) }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a0e8f51b06..186eb07761 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -479,13 +479,9 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- - name: Any jobs failed?
- if: ${{ contains(needs.*.result, 'failure') }}
- run: exit 1
- - name: All jobs passed or skipped
- if: ${{ !(contains(needs.*.result, 'failure')) }}
- # zizmor: ignore[template-injection]
- run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
+ - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
+ with:
+ needs: ${{ toJSON(needs) }}
mobile-unit-tests:
name: Unit Test Mobile
diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml
index b762ac636c..0758ef4dc9 100644
--- a/.github/workflows/weblate-lock.yml
+++ b/.github/workflows/weblate-lock.yml
@@ -52,10 +52,6 @@ jobs:
permissions: {}
if: always()
steps:
- - name: Any jobs failed?
- if: ${{ contains(needs.*.result, 'failure') }}
- run: exit 1
- - name: All jobs passed or skipped
- if: ${{ !(contains(needs.*.result, 'failure')) }}
- # zizmor: ignore[template-injection]
- run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
+ - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
+ with:
+ needs: ${{ toJSON(needs) }}
diff --git a/i18n/en.json b/i18n/en.json
index 849f96e4a5..2fbce6fbb7 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -463,6 +463,7 @@
"assets_added_count": "Added {count, plural, one {# asset} other {# assets}}",
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
"assets_added_to_name_count": "Added {count, plural, one {# asset} other {# assets}} to {hasName, select, true {{name}} other {new album}}",
+ "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
"assets_count": "{count, plural, one {# asset} other {# assets}}",
"assets_deleted_permanently": "{count} asset(s) deleted permanently",
"assets_deleted_permanently_from_server": "{count} asset(s) deleted permanently from the Immich server",
diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile
index a462b5e696..1f4d7aa635 100644
--- a/machine-learning/Dockerfile
+++ b/machine-learning/Dockerfile
@@ -1,6 +1,6 @@
ARG DEVICE=cpu
-FROM python:3.11-bookworm@sha256:ab60e444e04215a62671149f24c59cc2893b49cb5dad26f9d139077a86be760e AS builder-cpu
+FROM python:3.11-bookworm@sha256:d2621a9f74d31a8a60af19f97b09cc3ac54382c8680b6544018713a12ef6c048 AS builder-cpu
FROM builder-cpu AS builder-openvino
@@ -59,7 +59,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
RUN apt-get update && apt-get install -y --no-install-recommends g++
-COPY --from=ghcr.io/astral-sh/uv:latest@sha256:4a6c9444b126bd325fba904bff796bf91fb777bf6148d60109c4cb1de2ffc497 /uv /uvx /bin/
+COPY --from=ghcr.io/astral-sh/uv:latest@sha256:4faec156e35a5f345d57804d8858c6ba1cf6352ce5f4bffc11b7fdebdef46a38 /uv /uvx /bin/
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
@@ -68,11 +68,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \
uv pip install /opt/onnxruntime_rocm-*.whl; \
fi
-FROM python:3.11-slim-bookworm@sha256:97ef3198ec8c78690587167bb6a4905d00ffe053900687bdae93ad667e507cbb AS prod-cpu
+FROM python:3.11-slim-bookworm@sha256:7a3ed1226224bcc1fe5443262363d42f48cf832a540c1836ba8ccbeaadf8637c AS prod-cpu
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
-FROM python:3.11-slim-bookworm@sha256:97ef3198ec8c78690587167bb6a4905d00ffe053900687bdae93ad667e507cbb AS prod-openvino
+FROM python:3.11-slim-bookworm@sha256:7a3ed1226224bcc1fe5443262363d42f48cf832a540c1836ba8ccbeaadf8637c AS prod-openvino
RUN apt-get update && \
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock
index 22c6fad153..16765759e3 100644
--- a/machine-learning/uv.lock
+++ b/machine-learning/uv.lock
@@ -346,11 +346,11 @@ wheels = [
[[package]]
name = "configargparse"
-version = "1.7"
+version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/70/8a/73f1008adfad01cb923255b924b1528727b8270e67cb4ef41eabdc7d783e/ConfigArgParse-1.7.tar.gz", hash = "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1", size = 43817, upload-time = "2023-07-23T16:20:04.95Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size = 43958, upload-time = "2025-05-23T14:26:17.369Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6f/b3/b4ac838711fd74a2b4e6f746703cf9dd2cf5462d17dac07e349234e21b97/ConfigArgParse-1.7-py3-none-any.whl", hash = "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b", size = 25489, upload-time = "2023-07-23T16:20:03.27Z" },
+ { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" },
]
[[package]]
@@ -900,7 +900,7 @@ wheels = [
[[package]]
name = "huggingface-hub"
-version = "0.32.2"
+version = "0.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -912,9 +912,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d0/76/44f7025d1b3f29336aeb7324a57dd7c19f7c69f6612b7637b39ac7c17302/huggingface_hub-0.32.2.tar.gz", hash = "sha256:64a288b1eadad6b60bbfd50f0e52fd6cfa2ef77ab13c3e8a834a038ae929de54", size = 422847, upload-time = "2025-05-27T09:23:00.306Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/60/c8/4f7d270285c46324fd66f62159eb16739aa5696f422dba57678a8c6b78e9/huggingface_hub-0.32.4.tar.gz", hash = "sha256:f61d45cd338736f59fb0e97550b74c24ee771bcc92c05ae0766b9116abe720be", size = 424494, upload-time = "2025-06-03T09:59:46.105Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/32/30/532fe57467a6cc7ff2e39f088db1cb6d6bf522f724a4a5c7beda1282d5a6/huggingface_hub-0.32.2-py3-none-any.whl", hash = "sha256:f8fcf14603237eadf96dbe577d30b330f8c27b4a0a31e8f6c94fdc25e021fdb8", size = 509968, upload-time = "2025-05-27T09:22:57.967Z" },
+ { url = "https://files.pythonhosted.org/packages/67/8b/222140f3cfb6f17b0dd8c4b9a0b36bd4ebefe9fb0098ba35d6960abcda0f/huggingface_hub-0.32.4-py3-none-any.whl", hash = "sha256:37abf8826b38d971f60d3625229221c36e53fe58060286db9baf619cfbf39767", size = 512101, upload-time = "2025-06-03T09:59:44.099Z" },
]
[[package]]
@@ -1225,7 +1225,7 @@ wheels = [
[[package]]
name = "locust"
-version = "2.37.5"
+version = "2.37.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "configargparse" },
@@ -1245,25 +1245,26 @@ dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "werkzeug" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d2/d1/60d5fddac2baa47314c091636868b50178a38fc71ce39d68afd847448028/locust-2.37.5.tar.gz", hash = "sha256:c90824c4cb6a01cdede220684c7c8381253fcca47fc689dbca4f6c46d740c99f", size = 2252000, upload-time = "2025-05-22T08:54:58.676Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/90/05/2bfdf19756c6a12f6f9513f75340ecf0595d83cab4d9fc91162225908e3d/locust-2.37.9.tar.gz", hash = "sha256:e43673b594ec5ecde4f9ba6e0d5c66c00d7c0ae93591951abe83e8d186c67175", size = 2252507, upload-time = "2025-06-05T09:26:58.186Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e0/a0/32a51fb48f96b0de6bb6ea7308f68b7ae1bae53e6b975672f8c4ef7f8c08/locust-2.37.5-py3-none-any.whl", hash = "sha256:9922a2718b42f1c57a05c822e47b66555b3c61292694ec5edaf7a166fac6d112", size = 2268626, upload-time = "2025-05-22T08:54:55.938Z" },
+ { url = "https://files.pythonhosted.org/packages/33/1c/0ece4176231c578e819d970ec08d124492833e50aafd171c582bcc414446/locust-2.37.9-py3-none-any.whl", hash = "sha256:e17da439f3a252d1fb6d4c34daf00d7e8b87e99d833a32e8a79f4f8ebb07767d", size = 2269084, upload-time = "2025-06-05T09:26:56.257Z" },
]
[[package]]
name = "locust-cloud"
-version = "1.21.8"
+version = "1.23.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "configargparse" },
{ name = "gevent" },
{ name = "platformdirs" },
+ { name = "python-engineio" },
{ name = "python-socketio", extra = ["client"] },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/09/d4/64a169b4831d26ab9dceacb192ea30c749501d87b4958e628cf1f7ef45c4/locust_cloud-1.21.8.tar.gz", hash = "sha256:e8bde0da013c8731a45cc834cdf9fec2fc21738a5f2807d93c2c5eeb3008a80e", size = 450414, upload-time = "2025-05-22T08:30:27.458Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/7c/d9cbbd051490aeedfbd6ddda8ad48f77dd848ee490f6ebd166d20db5911e/locust_cloud-1.23.1.tar.gz", hash = "sha256:a09161752b8c9a9205e97cef5223ee3ad967bc2d91c52d61952aaa3da6802a55", size = 450937, upload-time = "2025-06-05T06:07:53.773Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/06/76/aa8b2f73bdf7de5ee344e5d0c4749e8d62ff38257b41d9df37b0b7ac84e2/locust_cloud-1.21.8-py3-none-any.whl", hash = "sha256:4f06b5d8a26ba91840a768008f4870965b13cc71481de9797409556de2edc007", size = 407879, upload-time = "2025-05-22T08:30:25.512Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/01/5af43edee540e38ba0ee0a2e3beb72c50073e0f646bb543a8b34650315e3/locust_cloud-1.23.1-py3-none-any.whl", hash = "sha256:11677895c6ed6d0beef1b425a6f04f10ea2cfcaeaefbf00a97fb3c9134296e54", size = 408323, upload-time = "2025-06-05T06:07:51.947Z" },
]
[[package]]
@@ -1414,40 +1415,41 @@ wheels = [
[[package]]
name = "mypy"
-version = "1.15.0"
+version = "1.16.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
+ { name = "pathspec" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433, upload-time = "2025-02-05T03:49:29.145Z" },
- { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472, upload-time = "2025-02-05T03:49:16.986Z" },
- { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424, upload-time = "2025-02-05T03:49:46.908Z" },
- { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450, upload-time = "2025-02-05T03:50:05.89Z" },
- { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765, upload-time = "2025-02-05T03:49:33.56Z" },
- { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701, upload-time = "2025-02-05T03:49:38.981Z" },
- { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload-time = "2025-02-05T03:50:17.287Z" },
- { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload-time = "2025-02-05T03:49:51.21Z" },
- { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload-time = "2025-02-05T03:50:20.885Z" },
- { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload-time = "2025-02-05T03:49:42.408Z" },
- { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload-time = "2025-02-05T03:49:07.707Z" },
- { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload-time = "2025-02-05T03:49:54.581Z" },
- { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981, upload-time = "2025-02-05T03:50:28.25Z" },
- { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175, upload-time = "2025-02-05T03:50:13.411Z" },
- { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675, upload-time = "2025-02-05T03:50:31.421Z" },
- { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020, upload-time = "2025-02-05T03:48:48.705Z" },
- { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582, upload-time = "2025-02-05T03:49:03.628Z" },
- { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614, upload-time = "2025-02-05T03:50:00.313Z" },
- { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload-time = "2025-02-05T03:48:55.789Z" },
- { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload-time = "2025-02-05T03:48:44.581Z" },
- { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload-time = "2025-02-05T03:49:25.514Z" },
- { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" },
- { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" },
- { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" },
- { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" },
+ { url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" },
+ { url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" },
+ { url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" },
+ { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" },
+ { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" },
+ { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" },
+ { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" },
+ { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" },
+ { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" },
+ { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" },
+ { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" },
+ { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" },
]
[[package]]
@@ -2017,14 +2019,14 @@ wheels = [
[[package]]
name = "pytest-mock"
-version = "3.14.0"
+version = "3.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814, upload-time = "2024-03-21T22:14:04.964Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863, upload-time = "2024-03-21T22:14:02.694Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" },
]
[[package]]
@@ -2050,14 +2052,14 @@ wheels = [
[[package]]
name = "python-engineio"
-version = "4.12.0"
+version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "simple-websocket" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f7/e1/eee1129544b7f78fa2afa9fa0fce153cdcb21015b9b331d1b8adf90f45cb/python_engineio-4.12.0.tar.gz", hash = "sha256:f42a36a868d7063aa10ddccf6bd6117a169b6bd00d7ca53999772093b62014f9", size = 91503, upload-time = "2025-04-12T15:30:23.905Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/0b/67295279b66835f9fa7a491650efcd78b20321c127036eef62c11a31e028/python_engineio-4.12.2.tar.gz", hash = "sha256:e7e712ffe1be1f6a05ee5f951e72d434854a32fcfc7f6e4d9d3cae24ec70defa", size = 91677, upload-time = "2025-06-04T19:22:18.789Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2b/f7/0aeea75424c47633c1d98557a2323be23bed31fa950f00161b34a5150d06/python_engineio-4.12.0-py3-none-any.whl", hash = "sha256:a0c47c129c39777e8ebc6d18011efd50db2144e4e8f08983acae8a3614626535", size = 59319, upload-time = "2025-04-12T15:30:22.325Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/fa/df59acedf7bbb937f69174d00f921a7b93aa5a5f5c17d05296c814fff6fc/python_engineio-4.12.2-py3-none-any.whl", hash = "sha256:8218ab66950e179dfec4b4bbb30aecf3f5d86f5e58e6fc1aa7fde2c698b2804f", size = 59536, upload-time = "2025-06-04T19:22:16.916Z" },
]
[[package]]
@@ -2300,27 +2302,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.11.11"
+version = "0.11.13"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b2/53/ae4857030d59286924a8bdb30d213d6ff22d8f0957e738d0289990091dd8/ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d", size = 4186707, upload-time = "2025-05-22T19:19:34.363Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b1/14/f2326676197bab099e2a24473158c21656fbf6a207c65f596ae15acb32b9/ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092", size = 10229049, upload-time = "2025-05-22T19:18:45.516Z" },
- { url = "https://files.pythonhosted.org/packages/9a/f3/bff7c92dd66c959e711688b2e0768e486bbca46b2f35ac319bb6cce04447/ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4", size = 11053601, upload-time = "2025-05-22T19:18:49.269Z" },
- { url = "https://files.pythonhosted.org/packages/e2/38/8e1a3efd0ef9d8259346f986b77de0f62c7a5ff4a76563b6b39b68f793b9/ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd", size = 10367421, upload-time = "2025-05-22T19:18:51.754Z" },
- { url = "https://files.pythonhosted.org/packages/b4/50/557ad9dd4fb9d0bf524ec83a090a3932d284d1a8b48b5906b13b72800e5f/ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6", size = 10581980, upload-time = "2025-05-22T19:18:54.011Z" },
- { url = "https://files.pythonhosted.org/packages/c4/b2/e2ed82d6e2739ece94f1bdbbd1d81b712d3cdaf69f0a1d1f1a116b33f9ad/ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4", size = 10089241, upload-time = "2025-05-22T19:18:56.041Z" },
- { url = "https://files.pythonhosted.org/packages/3d/9f/b4539f037a5302c450d7c695c82f80e98e48d0d667ecc250e6bdeb49b5c3/ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac", size = 11699398, upload-time = "2025-05-22T19:18:58.248Z" },
- { url = "https://files.pythonhosted.org/packages/61/fb/32e029d2c0b17df65e6eaa5ce7aea5fbeaed22dddd9fcfbbf5fe37c6e44e/ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709", size = 12427955, upload-time = "2025-05-22T19:19:00.981Z" },
- { url = "https://files.pythonhosted.org/packages/6e/e3/160488dbb11f18c8121cfd588e38095ba779ae208292765972f7732bfd95/ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8", size = 12069803, upload-time = "2025-05-22T19:19:03.258Z" },
- { url = "https://files.pythonhosted.org/packages/ff/16/3b006a875f84b3d0bff24bef26b8b3591454903f6f754b3f0a318589dcc3/ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b", size = 11242630, upload-time = "2025-05-22T19:19:05.871Z" },
- { url = "https://files.pythonhosted.org/packages/65/0d/0338bb8ac0b97175c2d533e9c8cdc127166de7eb16d028a43c5ab9e75abd/ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875", size = 11507310, upload-time = "2025-05-22T19:19:08.584Z" },
- { url = "https://files.pythonhosted.org/packages/6f/bf/d7130eb26174ce9b02348b9f86d5874eafbf9f68e5152e15e8e0a392e4a3/ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1", size = 10441144, upload-time = "2025-05-22T19:19:13.621Z" },
- { url = "https://files.pythonhosted.org/packages/b3/f3/4be2453b258c092ff7b1761987cf0749e70ca1340cd1bfb4def08a70e8d8/ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81", size = 10081987, upload-time = "2025-05-22T19:19:15.821Z" },
- { url = "https://files.pythonhosted.org/packages/6c/6e/dfa4d2030c5b5c13db158219f2ec67bf333e8a7748dccf34cfa2a6ab9ebc/ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639", size = 11073922, upload-time = "2025-05-22T19:19:18.104Z" },
- { url = "https://files.pythonhosted.org/packages/ff/f4/f7b0b0c3d32b593a20ed8010fa2c1a01f2ce91e79dda6119fcc51d26c67b/ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345", size = 11568537, upload-time = "2025-05-22T19:19:20.889Z" },
- { url = "https://files.pythonhosted.org/packages/d2/46/0e892064d0adc18bcc81deed9aaa9942a27fd2cd9b1b7791111ce468c25f/ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112", size = 10536492, upload-time = "2025-05-22T19:19:23.642Z" },
- { url = "https://files.pythonhosted.org/packages/1b/d9/232e79459850b9f327e9f1dc9c047a2a38a6f9689e1ec30024841fc4416c/ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f", size = 11612562, upload-time = "2025-05-22T19:19:27.013Z" },
- { url = "https://files.pythonhosted.org/packages/ce/eb/09c132cff3cc30b2e7244191dcce69437352d6d6709c0adf374f3e6f476e/ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b", size = 10735951, upload-time = "2025-05-22T19:19:30.043Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" },
+ { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" },
+ { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" },
+ { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" },
+ { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" },
+ { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" },
]
[[package]]
@@ -2627,16 +2629,16 @@ wheels = [
[[package]]
name = "uvicorn"
-version = "0.34.2"
+version = "0.34.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
]
[package.optional-dependencies]
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 74597b43bc..ee646354d1 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -177,6 +177,7 @@ Class | Method | HTTP request | Description
*SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
+*SearchApi* | [**searchAssetStatistics**](doc//SearchApi.md#searchassetstatistics) | **POST** /search/statistics |
*SearchApi* | [**searchAssets**](doc//SearchApi.md#searchassets) | **POST** /search/metadata |
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
@@ -425,6 +426,7 @@ Class | Method | HTTP request | Description
- [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
- [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
- [SearchResponseDto](doc//SearchResponseDto.md)
+ - [SearchStatisticsResponseDto](doc//SearchStatisticsResponseDto.md)
- [SearchSuggestionType](doc//SearchSuggestionType.md)
- [ServerAboutResponseDto](doc//ServerAboutResponseDto.md)
- [ServerApkLinksDto](doc//ServerApkLinksDto.md)
@@ -453,6 +455,7 @@ Class | Method | HTTP request | Description
- [StackCreateDto](doc//StackCreateDto.md)
- [StackResponseDto](doc//StackResponseDto.md)
- [StackUpdateDto](doc//StackUpdateDto.md)
+ - [StatisticsSearchDto](doc//StatisticsSearchDto.md)
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
- [SyncAckDto](doc//SyncAckDto.md)
- [SyncAckSetDto](doc//SyncAckSetDto.md)
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 7b49661844..9bf4026320 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -214,6 +214,7 @@ part 'model/search_explore_response_dto.dart';
part 'model/search_facet_count_response_dto.dart';
part 'model/search_facet_response_dto.dart';
part 'model/search_response_dto.dart';
+part 'model/search_statistics_response_dto.dart';
part 'model/search_suggestion_type.dart';
part 'model/server_about_response_dto.dart';
part 'model/server_apk_links_dto.dart';
@@ -242,6 +243,7 @@ part 'model/source_type.dart';
part 'model/stack_create_dto.dart';
part 'model/stack_response_dto.dart';
part 'model/stack_update_dto.dart';
+part 'model/statistics_search_dto.dart';
part 'model/sync_ack_delete_dto.dart';
part 'model/sync_ack_dto.dart';
part 'model/sync_ack_set_dto.dart';
diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart
index 632107ff79..5c7a8de59d 100644
--- a/mobile/openapi/lib/api/search_api.dart
+++ b/mobile/openapi/lib/api/search_api.dart
@@ -193,6 +193,53 @@ class SearchApi {
return null;
}
+ /// Performs an HTTP 'POST /search/statistics' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [StatisticsSearchDto] statisticsSearchDto (required):
+ Future searchAssetStatisticsWithHttpInfo(StatisticsSearchDto statisticsSearchDto,) async {
+ // ignore: prefer_const_declarations
+ final apiPath = r'/search/statistics';
+
+ // ignore: prefer_final_locals
+ Object? postBody = statisticsSearchDto;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = ['application/json'];
+
+
+ return apiClient.invokeAPI(
+ apiPath,
+ 'POST',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [StatisticsSearchDto] statisticsSearchDto (required):
+ Future searchAssetStatistics(StatisticsSearchDto statisticsSearchDto,) async {
+ final response = await searchAssetStatisticsWithHttpInfo(statisticsSearchDto,);
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ // When a remote server returns no body with a status of 204, we shall not decode it.
+ // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+ // FormatException when trying to decode an empty string.
+ if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+ return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchStatisticsResponseDto',) as SearchStatisticsResponseDto;
+
+ }
+ return null;
+ }
+
/// Performs an HTTP 'POST /search/metadata' operation and returns the [Response].
/// Parameters:
///
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index a96b895655..bc7f48255c 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -484,6 +484,8 @@ class ApiClient {
return SearchFacetResponseDto.fromJson(value);
case 'SearchResponseDto':
return SearchResponseDto.fromJson(value);
+ case 'SearchStatisticsResponseDto':
+ return SearchStatisticsResponseDto.fromJson(value);
case 'SearchSuggestionType':
return SearchSuggestionTypeTypeTransformer().decode(value);
case 'ServerAboutResponseDto':
@@ -540,6 +542,8 @@ class ApiClient {
return StackResponseDto.fromJson(value);
case 'StackUpdateDto':
return StackUpdateDto.fromJson(value);
+ case 'StatisticsSearchDto':
+ return StatisticsSearchDto.fromJson(value);
case 'SyncAckDeleteDto':
return SyncAckDeleteDto.fromJson(value);
case 'SyncAckDto':
diff --git a/mobile/openapi/lib/model/search_statistics_response_dto.dart b/mobile/openapi/lib/model/search_statistics_response_dto.dart
new file mode 100644
index 0000000000..84f31373d8
--- /dev/null
+++ b/mobile/openapi/lib/model/search_statistics_response_dto.dart
@@ -0,0 +1,99 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class SearchStatisticsResponseDto {
+ /// Returns a new [SearchStatisticsResponseDto] instance.
+ SearchStatisticsResponseDto({
+ required this.total,
+ });
+
+ int total;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is SearchStatisticsResponseDto &&
+ other.total == total;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (total.hashCode);
+
+ @override
+ String toString() => 'SearchStatisticsResponseDto[total=$total]';
+
+ Map toJson() {
+ final json = {};
+ json[r'total'] = this.total;
+ return json;
+ }
+
+ /// Returns a new [SearchStatisticsResponseDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static SearchStatisticsResponseDto? fromJson(dynamic value) {
+ upgradeDto(value, "SearchStatisticsResponseDto");
+ if (value is Map) {
+ final json = value.cast();
+
+ return SearchStatisticsResponseDto(
+ total: mapValueOfType(json, r'total')!,
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = SearchStatisticsResponseDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = SearchStatisticsResponseDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of SearchStatisticsResponseDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = SearchStatisticsResponseDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'total',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/statistics_search_dto.dart b/mobile/openapi/lib/model/statistics_search_dto.dart
new file mode 100644
index 0000000000..0fe0770b6d
--- /dev/null
+++ b/mobile/openapi/lib/model/statistics_search_dto.dart
@@ -0,0 +1,500 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class StatisticsSearchDto {
+ /// Returns a new [StatisticsSearchDto] instance.
+ StatisticsSearchDto({
+ this.city,
+ this.country,
+ this.createdAfter,
+ this.createdBefore,
+ this.description,
+ this.deviceId,
+ this.isEncoded,
+ this.isFavorite,
+ this.isMotion,
+ this.isNotInAlbum,
+ this.isOffline,
+ this.lensModel,
+ this.libraryId,
+ this.make,
+ this.model,
+ this.personIds = const [],
+ this.rating,
+ this.state,
+ this.tagIds = const [],
+ this.takenAfter,
+ this.takenBefore,
+ this.trashedAfter,
+ this.trashedBefore,
+ this.type,
+ this.updatedAfter,
+ this.updatedBefore,
+ this.visibility,
+ });
+
+ String? city;
+
+ String? country;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ DateTime? createdAfter;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ DateTime? createdBefore;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ String? description;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ String? deviceId;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ bool? isEncoded;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ bool? isFavorite;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ bool? isMotion;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ bool? isNotInAlbum;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ bool? isOffline;
+
+ String? lensModel;
+
+ String? libraryId;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ String? make;
+
+ String? model;
+
+ List personIds;
+
+ /// Minimum value: -1
+ /// Maximum value: 5
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ num? rating;
+
+ String? state;
+
+ List tagIds;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ DateTime? takenAfter;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ DateTime? takenBefore;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ DateTime? trashedAfter;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ DateTime? trashedBefore;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ AssetTypeEnum? type;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ DateTime? updatedAfter;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ DateTime? updatedBefore;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ AssetVisibility? visibility;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is StatisticsSearchDto &&
+ other.city == city &&
+ other.country == country &&
+ other.createdAfter == createdAfter &&
+ other.createdBefore == createdBefore &&
+ other.description == description &&
+ other.deviceId == deviceId &&
+ other.isEncoded == isEncoded &&
+ other.isFavorite == isFavorite &&
+ other.isMotion == isMotion &&
+ other.isNotInAlbum == isNotInAlbum &&
+ other.isOffline == isOffline &&
+ other.lensModel == lensModel &&
+ other.libraryId == libraryId &&
+ other.make == make &&
+ other.model == model &&
+ _deepEquality.equals(other.personIds, personIds) &&
+ other.rating == rating &&
+ other.state == state &&
+ _deepEquality.equals(other.tagIds, tagIds) &&
+ other.takenAfter == takenAfter &&
+ other.takenBefore == takenBefore &&
+ other.trashedAfter == trashedAfter &&
+ other.trashedBefore == trashedBefore &&
+ other.type == type &&
+ other.updatedAfter == updatedAfter &&
+ other.updatedBefore == updatedBefore &&
+ other.visibility == visibility;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (city == null ? 0 : city!.hashCode) +
+ (country == null ? 0 : country!.hashCode) +
+ (createdAfter == null ? 0 : createdAfter!.hashCode) +
+ (createdBefore == null ? 0 : createdBefore!.hashCode) +
+ (description == null ? 0 : description!.hashCode) +
+ (deviceId == null ? 0 : deviceId!.hashCode) +
+ (isEncoded == null ? 0 : isEncoded!.hashCode) +
+ (isFavorite == null ? 0 : isFavorite!.hashCode) +
+ (isMotion == null ? 0 : isMotion!.hashCode) +
+ (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
+ (isOffline == null ? 0 : isOffline!.hashCode) +
+ (lensModel == null ? 0 : lensModel!.hashCode) +
+ (libraryId == null ? 0 : libraryId!.hashCode) +
+ (make == null ? 0 : make!.hashCode) +
+ (model == null ? 0 : model!.hashCode) +
+ (personIds.hashCode) +
+ (rating == null ? 0 : rating!.hashCode) +
+ (state == null ? 0 : state!.hashCode) +
+ (tagIds.hashCode) +
+ (takenAfter == null ? 0 : takenAfter!.hashCode) +
+ (takenBefore == null ? 0 : takenBefore!.hashCode) +
+ (trashedAfter == null ? 0 : trashedAfter!.hashCode) +
+ (trashedBefore == null ? 0 : trashedBefore!.hashCode) +
+ (type == null ? 0 : type!.hashCode) +
+ (updatedAfter == null ? 0 : updatedAfter!.hashCode) +
+ (updatedBefore == null ? 0 : updatedBefore!.hashCode) +
+ (visibility == null ? 0 : visibility!.hashCode);
+
+ @override
+ String toString() => 'StatisticsSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]';
+
+ Map toJson() {
+ final json = {};
+ if (this.city != null) {
+ json[r'city'] = this.city;
+ } else {
+ // json[r'city'] = null;
+ }
+ if (this.country != null) {
+ json[r'country'] = this.country;
+ } else {
+ // json[r'country'] = null;
+ }
+ if (this.createdAfter != null) {
+ json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String();
+ } else {
+ // json[r'createdAfter'] = null;
+ }
+ if (this.createdBefore != null) {
+ json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String();
+ } else {
+ // json[r'createdBefore'] = null;
+ }
+ if (this.description != null) {
+ json[r'description'] = this.description;
+ } else {
+ // json[r'description'] = null;
+ }
+ if (this.deviceId != null) {
+ json[r'deviceId'] = this.deviceId;
+ } else {
+ // json[r'deviceId'] = null;
+ }
+ if (this.isEncoded != null) {
+ json[r'isEncoded'] = this.isEncoded;
+ } else {
+ // json[r'isEncoded'] = null;
+ }
+ if (this.isFavorite != null) {
+ json[r'isFavorite'] = this.isFavorite;
+ } else {
+ // json[r'isFavorite'] = null;
+ }
+ if (this.isMotion != null) {
+ json[r'isMotion'] = this.isMotion;
+ } else {
+ // json[r'isMotion'] = null;
+ }
+ if (this.isNotInAlbum != null) {
+ json[r'isNotInAlbum'] = this.isNotInAlbum;
+ } else {
+ // json[r'isNotInAlbum'] = null;
+ }
+ if (this.isOffline != null) {
+ json[r'isOffline'] = this.isOffline;
+ } else {
+ // json[r'isOffline'] = null;
+ }
+ if (this.lensModel != null) {
+ json[r'lensModel'] = this.lensModel;
+ } else {
+ // json[r'lensModel'] = null;
+ }
+ if (this.libraryId != null) {
+ json[r'libraryId'] = this.libraryId;
+ } else {
+ // json[r'libraryId'] = null;
+ }
+ if (this.make != null) {
+ json[r'make'] = this.make;
+ } else {
+ // json[r'make'] = null;
+ }
+ if (this.model != null) {
+ json[r'model'] = this.model;
+ } else {
+ // json[r'model'] = null;
+ }
+ json[r'personIds'] = this.personIds;
+ if (this.rating != null) {
+ json[r'rating'] = this.rating;
+ } else {
+ // json[r'rating'] = null;
+ }
+ if (this.state != null) {
+ json[r'state'] = this.state;
+ } else {
+ // json[r'state'] = null;
+ }
+ json[r'tagIds'] = this.tagIds;
+ if (this.takenAfter != null) {
+ json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String();
+ } else {
+ // json[r'takenAfter'] = null;
+ }
+ if (this.takenBefore != null) {
+ json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String();
+ } else {
+ // json[r'takenBefore'] = null;
+ }
+ if (this.trashedAfter != null) {
+ json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String();
+ } else {
+ // json[r'trashedAfter'] = null;
+ }
+ if (this.trashedBefore != null) {
+ json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String();
+ } else {
+ // json[r'trashedBefore'] = null;
+ }
+ if (this.type != null) {
+ json[r'type'] = this.type;
+ } else {
+ // json[r'type'] = null;
+ }
+ if (this.updatedAfter != null) {
+ json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String();
+ } else {
+ // json[r'updatedAfter'] = null;
+ }
+ if (this.updatedBefore != null) {
+ json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String();
+ } else {
+ // json[r'updatedBefore'] = null;
+ }
+ if (this.visibility != null) {
+ json[r'visibility'] = this.visibility;
+ } else {
+ // json[r'visibility'] = null;
+ }
+ return json;
+ }
+
+ /// Returns a new [StatisticsSearchDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static StatisticsSearchDto? fromJson(dynamic value) {
+ upgradeDto(value, "StatisticsSearchDto");
+ if (value is Map) {
+ final json = value.cast();
+
+ return StatisticsSearchDto(
+ city: mapValueOfType(json, r'city'),
+ country: mapValueOfType(json, r'country'),
+ createdAfter: mapDateTime(json, r'createdAfter', r''),
+ createdBefore: mapDateTime(json, r'createdBefore', r''),
+ description: mapValueOfType(json, r'description'),
+ deviceId: mapValueOfType(json, r'deviceId'),
+ isEncoded: mapValueOfType(json, r'isEncoded'),
+ isFavorite: mapValueOfType(json, r'isFavorite'),
+ isMotion: mapValueOfType(json, r'isMotion'),
+ isNotInAlbum: mapValueOfType(json, r'isNotInAlbum'),
+ isOffline: mapValueOfType(json, r'isOffline'),
+ lensModel: mapValueOfType(json, r'lensModel'),
+ libraryId: mapValueOfType(json, r'libraryId'),
+ make: mapValueOfType(json, r'make'),
+ model: mapValueOfType(json, r'model'),
+ personIds: json[r'personIds'] is Iterable
+ ? (json[r'personIds'] as Iterable).cast().toList(growable: false)
+ : const [],
+ rating: num.parse('${json[r'rating']}'),
+ state: mapValueOfType(json, r'state'),
+ tagIds: json[r'tagIds'] is Iterable
+ ? (json[r'tagIds'] as Iterable).cast().toList(growable: false)
+ : const [],
+ takenAfter: mapDateTime(json, r'takenAfter', r''),
+ takenBefore: mapDateTime(json, r'takenBefore', r''),
+ trashedAfter: mapDateTime(json, r'trashedAfter', r''),
+ trashedBefore: mapDateTime(json, r'trashedBefore', r''),
+ type: AssetTypeEnum.fromJson(json[r'type']),
+ updatedAfter: mapDateTime(json, r'updatedAfter', r''),
+ updatedBefore: mapDateTime(json, r'updatedBefore', r''),
+ visibility: AssetVisibility.fromJson(json[r'visibility']),
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = StatisticsSearchDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = StatisticsSearchDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of StatisticsSearchDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = StatisticsSearchDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ };
+}
+
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 0e35be2ee0..352fe768f8 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -5158,6 +5158,48 @@
]
}
},
+ "/search/statistics": {
+ "post": {
+ "operationId": "searchAssetStatistics",
+ "parameters": [],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StatisticsSearchDto"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SearchStatisticsResponseDto"
+ }
+ }
+ },
+ "description": ""
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ },
+ {
+ "cookie": []
+ },
+ {
+ "api_key": []
+ }
+ ],
+ "tags": [
+ "Search"
+ ]
+ }
+ },
"/search/suggestions": {
"get": {
"operationId": "getSearchSuggestions",
@@ -12069,6 +12111,17 @@
],
"type": "object"
},
+ "SearchStatisticsResponseDto": {
+ "properties": {
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "total"
+ ],
+ "type": "object"
+ },
"SearchSuggestionType": {
"enum": [
"country",
@@ -12974,6 +13027,125 @@
},
"type": "object"
},
+ "StatisticsSearchDto": {
+ "properties": {
+ "city": {
+ "nullable": true,
+ "type": "string"
+ },
+ "country": {
+ "nullable": true,
+ "type": "string"
+ },
+ "createdAfter": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "createdBefore": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "deviceId": {
+ "type": "string"
+ },
+ "isEncoded": {
+ "type": "boolean"
+ },
+ "isFavorite": {
+ "type": "boolean"
+ },
+ "isMotion": {
+ "type": "boolean"
+ },
+ "isNotInAlbum": {
+ "type": "boolean"
+ },
+ "isOffline": {
+ "type": "boolean"
+ },
+ "lensModel": {
+ "nullable": true,
+ "type": "string"
+ },
+ "libraryId": {
+ "format": "uuid",
+ "nullable": true,
+ "type": "string"
+ },
+ "make": {
+ "type": "string"
+ },
+ "model": {
+ "nullable": true,
+ "type": "string"
+ },
+ "personIds": {
+ "items": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "rating": {
+ "maximum": 5,
+ "minimum": -1,
+ "type": "number"
+ },
+ "state": {
+ "nullable": true,
+ "type": "string"
+ },
+ "tagIds": {
+ "items": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "takenAfter": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "takenBefore": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "trashedAfter": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "trashedBefore": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "type": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/AssetTypeEnum"
+ }
+ ]
+ },
+ "updatedAfter": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "updatedBefore": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "visibility": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/AssetVisibility"
+ }
+ ]
+ }
+ },
+ "type": "object"
+ },
"SyncAckDeleteDto": {
"properties": {
"types": {
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index fa75049168..0308ecb9e0 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -995,6 +995,38 @@ export type SmartSearchDto = {
withDeleted?: boolean;
withExif?: boolean;
};
+export type StatisticsSearchDto = {
+ city?: string | null;
+ country?: string | null;
+ createdAfter?: string;
+ createdBefore?: string;
+ description?: string;
+ deviceId?: string;
+ isEncoded?: boolean;
+ isFavorite?: boolean;
+ isMotion?: boolean;
+ isNotInAlbum?: boolean;
+ isOffline?: boolean;
+ lensModel?: string | null;
+ libraryId?: string | null;
+ make?: string;
+ model?: string | null;
+ personIds?: string[];
+ rating?: number;
+ state?: string | null;
+ tagIds?: string[];
+ takenAfter?: string;
+ takenBefore?: string;
+ trashedAfter?: string;
+ trashedBefore?: string;
+ "type"?: AssetTypeEnum;
+ updatedAfter?: string;
+ updatedBefore?: string;
+ visibility?: AssetVisibility;
+};
+export type SearchStatisticsResponseDto = {
+ total: number;
+};
export type ServerAboutResponseDto = {
build?: string;
buildImage?: string;
@@ -2882,6 +2914,18 @@ export function searchSmart({ smartSearchDto }: {
body: smartSearchDto
})));
}
+export function searchAssetStatistics({ statisticsSearchDto }: {
+ statisticsSearchDto: StatisticsSearchDto;
+}, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchJson<{
+ status: 200;
+ data: SearchStatisticsResponseDto;
+ }>("/search/statistics", oazapfts.json({
+ ...opts,
+ method: "POST",
+ body: statisticsSearchDto
+ })));
+}
export function getSearchSuggestions({ country, includeNull, make, model, state, $type }: {
country?: string;
includeNull?: boolean;
diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts
index c51ad8e06a..9bda1fcada 100644
--- a/server/src/controllers/search.controller.ts
+++ b/server/src/controllers/search.controller.ts
@@ -11,8 +11,10 @@ import {
SearchPeopleDto,
SearchPlacesDto,
SearchResponseDto,
+ SearchStatisticsResponseDto,
SearchSuggestionRequestDto,
SmartSearchDto,
+ StatisticsSearchDto,
} from 'src/dtos/search.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { SearchService } from 'src/services/search.service';
@@ -29,6 +31,13 @@ export class SearchController {
return this.service.searchMetadata(auth, dto);
}
+ @Post('statistics')
+ @HttpCode(HttpStatus.OK)
+ @Authenticated()
+ searchAssetStatistics(@Auth() auth: AuthDto, @Body() dto: StatisticsSearchDto): Promise {
+ return this.service.searchStatistics(auth, dto);
+ }
+
@Post('random')
@HttpCode(HttpStatus.OK)
@Authenticated()
diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts
index 579cba680e..81d74e0a76 100644
--- a/server/src/dtos/search.dto.ts
+++ b/server/src/dtos/search.dto.ts
@@ -37,12 +37,6 @@ class BaseSearchDto {
@ValidateAssetVisibility({ optional: true })
visibility?: AssetVisibility;
- @ValidateBoolean({ optional: true })
- withDeleted?: boolean;
-
- @ValidateBoolean({ optional: true })
- withExif?: boolean;
-
@ValidateDate({ optional: true })
createdBefore?: Date;
@@ -92,13 +86,6 @@ class BaseSearchDto {
@Optional({ nullable: true, emptyToNull: true })
lensModel?: string | null;
- @IsInt()
- @Min(1)
- @Max(1000)
- @Type(() => Number)
- @Optional()
- size?: number;
-
@ValidateBoolean({ optional: true })
isNotInAlbum?: boolean;
@@ -115,7 +102,22 @@ class BaseSearchDto {
rating?: number;
}
-export class RandomSearchDto extends BaseSearchDto {
+class BaseSearchWithResultsDto extends BaseSearchDto {
+ @ValidateBoolean({ optional: true })
+ withDeleted?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ withExif?: boolean;
+
+ @IsInt()
+ @Min(1)
+ @Max(1000)
+ @Type(() => Number)
+ @Optional()
+ size?: number;
+}
+
+export class RandomSearchDto extends BaseSearchWithResultsDto {
@ValidateBoolean({ optional: true })
withStacked?: boolean;
@@ -179,7 +181,14 @@ export class MetadataSearchDto extends RandomSearchDto {
page?: number;
}
-export class SmartSearchDto extends BaseSearchDto {
+export class StatisticsSearchDto extends BaseSearchDto {
+ @IsString()
+ @IsNotEmpty()
+ @Optional()
+ description?: string;
+}
+
+export class SmartSearchDto extends BaseSearchWithResultsDto {
@IsString()
@IsNotEmpty()
query!: string;
@@ -299,6 +308,11 @@ export class SearchResponseDto {
assets!: SearchAssetResponseDto;
}
+export class SearchStatisticsResponseDto {
+ @ApiProperty({ type: 'integer' })
+ total!: number;
+}
+
class SearchExploreItem {
value!: string;
data!: AssetResponseDto;
diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql
index 806fdb1c70..b68e0e177e 100644
--- a/server/src/queries/search.repository.sql
+++ b/server/src/queries/search.repository.sql
@@ -20,6 +20,20 @@ limit
offset
$7
+-- SearchRepository.searchStatistics
+select
+ count(*) as "total"
+from
+ "assets"
+ inner join "exif" on "assets"."id" = "exif"."assetId"
+where
+ "assets"."visibility" = $1
+ and "assets"."fileCreatedAt" >= $2
+ and "exif"."lensModel" = $3
+ and "assets"."ownerId" = any ($4::uuid[])
+ and "assets"."isFavorite" = $5
+ and "assets"."deletedAt" is null
+
-- SearchRepository.searchRandom
(
select
diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts
index 747a59c65b..14150d4243 100644
--- a/server/src/repositories/search.repository.ts
+++ b/server/src/repositories/search.repository.ts
@@ -185,6 +185,7 @@ export class SearchRepository {
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) {
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection;
const items = await searchAssetBuilder(this.db, options)
+ .selectAll('assets')
.orderBy('assets.fileCreatedAt', orderDirection)
.limit(pagination.size + 1)
.offset((pagination.page - 1) * pagination.size)
@@ -193,6 +194,22 @@ export class SearchRepository {
return paginationHelper(items, pagination.size);
}
+ @GenerateSql({
+ params: [
+ {
+ takenAfter: DummyValue.DATE,
+ lensModel: DummyValue.STRING,
+ isFavorite: true,
+ userIds: [DummyValue.UUID],
+ },
+ ],
+ })
+ searchStatistics(options: AssetSearchOptions) {
+ return searchAssetBuilder(this.db, options)
+ .select((qb) => qb.fn.countAll().as('total'))
+ .executeTakeFirstOrThrow();
+ }
+
@GenerateSql({
params: [
100,
@@ -209,10 +226,12 @@ export class SearchRepository {
const uuid = randomUUID();
const builder = searchAssetBuilder(this.db, options);
const lessThan = builder
+ .selectAll('assets')
.where('assets.id', '<', uuid)
.orderBy(sql`random()`)
.limit(size);
const greaterThan = builder
+ .selectAll('assets')
.where('assets.id', '>', uuid)
.orderBy(sql`random()`)
.limit(size);
@@ -241,6 +260,7 @@ export class SearchRepository {
return this.db.transaction().execute(async (trx) => {
await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx);
const items = await searchAssetBuilder(trx, options)
+ .selectAll('assets')
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
.limit(pagination.size + 1)
diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts
index 3f122b5e74..73678f05af 100644
--- a/server/src/services/search.service.ts
+++ b/server/src/services/search.service.ts
@@ -10,9 +10,11 @@ import {
SearchPeopleDto,
SearchPlacesDto,
SearchResponseDto,
+ SearchStatisticsResponseDto,
SearchSuggestionRequestDto,
SearchSuggestionType,
SmartSearchDto,
+ StatisticsSearchDto,
} from 'src/dtos/search.dto';
import { AssetOrder, AssetVisibility } from 'src/enum';
import { BaseService } from 'src/services/base.service';
@@ -67,6 +69,15 @@ export class SearchService extends BaseService {
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
}
+ async searchStatistics(auth: AuthDto, dto: StatisticsSearchDto): Promise {
+ const userIds = await this.getUserIdsToSearch(auth);
+
+ return await this.searchRepository.searchStatistics({
+ ...dto,
+ userIds,
+ });
+ }
+
async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise {
if (dto.visibility === AssetVisibility.LOCKED) {
requireElevatedPermission(auth);
diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts
index 5e5c6c5fb4..d8171dc955 100644
--- a/server/src/utils/database.ts
+++ b/server/src/utils/database.ts
@@ -291,7 +291,6 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild
return kysely
.withPlugin(joinDeduplicationPlugin)
.selectFrom('assets')
- .selectAll('assets')
.where('assets.visibility', '=', visibility)
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
.$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!))
diff --git a/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte b/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte
index e36c173b1d..440cdeb2a6 100644
--- a/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte
+++ b/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte
@@ -12,8 +12,9 @@
onClick(!isPlaying)}
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index cb4c9eeff0..b64ab51124 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -52,16 +52,20 @@ export const addAssetsToAlbum = async (albumId: string, assetIds: string[], show
key: authManager.key,
});
const count = result.filter(({ success }) => success).length;
+ const duplicateErrorCount = result.filter(({ error }) => error === 'duplicate').length;
const $t = get(t);
if (showNotification) {
+ let message = $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } });
+ if (count > 0) {
+ message = $t('assets_added_to_album_count', { values: { count } });
+ } else if (duplicateErrorCount > 0) {
+ message = $t('assets_were_part_of_album_count', { values: { count: duplicateErrorCount } });
+ }
notificationController.show({
type: NotificationType.Info,
timeout: 5000,
- message:
- count > 0
- ? $t('assets_added_to_album_count', { values: { count } })
- : $t('assets_were_part_of_album_count', { values: { count: assetIds.length } }),
+ message,
button: {
text: $t('view_album'),
onClick() {
@@ -125,23 +129,37 @@ export const addAssetsToNewAlbum = async (albumName: string, assetIds: string[])
}
const $t = get(t);
// for reasons beyond me > doesn't work, even though it's (afaik) exactly this object
- notificationController.show<{ key: Translations; values: InterpolationValues }>({
- type: NotificationType.Info,
- timeout: 5000,
- component: {
- type: FormatBoldMessage,
- props: {
- key: 'assets_added_to_name_count',
- values: { count: assetIds.length, name: albumName, hasName: !!albumName },
+ if (album.assets.length === 0) {
+ notificationController.show({
+ type: NotificationType.Info,
+ timeout: 5000,
+ message: $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } }),
+ button: {
+ text: $t('view_album'),
+ onClick() {
+ return goto(`${AppRoute.ALBUMS}/${album.id}`);
+ },
},
- },
- button: {
- text: $t('view_album'),
- onClick() {
- return goto(`${AppRoute.ALBUMS}/${album.id}`);
+ });
+ } else {
+ notificationController.show<{ key: Translations; values: InterpolationValues }>({
+ type: NotificationType.Info,
+ timeout: 5000,
+ component: {
+ type: FormatBoldMessage,
+ props: {
+ key: 'assets_added_to_name_count',
+ values: { count: album.assets.length, name: albumName, hasName: !!albumName },
+ },
},
- },
- });
+ button: {
+ text: $t('view_album'),
+ onClick() {
+ return goto(`${AppRoute.ALBUMS}/${album.id}`);
+ },
+ },
+ });
+ }
return album;
};