mirror of
https://github.com/immich-app/immich.git
synced 2026-03-15 22:58:36 -07:00
Compare commits
10 Commits
feat/check
...
feat/mobil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef96fa62c1 | ||
|
|
884ebbc965 | ||
|
|
93cd80ad12 | ||
|
|
6052f84022 | ||
|
|
207d8ace07 | ||
|
|
82cfadb599 | ||
|
|
8ab8a9156f | ||
|
|
d1466731d8 | ||
|
|
f706738f93 | ||
|
|
811d3e1c33 |
@@ -24,7 +24,7 @@ Immich has three main clients:
|
||||
3. CLI - Command-line utility for bulk upload
|
||||
|
||||
:::info
|
||||
All three clients use [OpenAPI](/api.md) to auto-generate rest clients for easy integration. For more information about this process, see [OpenAPI](/api.md).
|
||||
All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for easy integration. For more information about this process, see [OpenAPI](./open-api.md).
|
||||
:::
|
||||
|
||||
### Mobile App
|
||||
@@ -71,7 +71,7 @@ An incoming HTTP request is mapped to a controller (`src/controllers`). Controll
|
||||
|
||||
### Domain Transfer Objects (DTOs)
|
||||
|
||||
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](/api.md) schemas and control the generated code used by each client.
|
||||
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](./open-api.md) schemas and control the generated code used by each client.
|
||||
|
||||
### Background Jobs
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# API
|
||||
# OpenAPI
|
||||
|
||||
Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generate API documentation. To view the published docs see [here](https://api.immich.app/).
|
||||
|
||||
@@ -53,7 +53,7 @@ You can use `dart fix --apply` and `dcm fix lib` to potentially correct some iss
|
||||
|
||||
## OpenAPI
|
||||
|
||||
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/api.md) for more details.
|
||||
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/developer/open-api.md) for more details.
|
||||
|
||||
## Database Migrations
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`.
|
||||
- The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached.
|
||||
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting).
|
||||
- MIGraphX is a new backend for AMD cards, which compiles models at runtime. As such, the first few inferences will be slow.
|
||||
|
||||
#### OpenVINO
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const prism = require('prism-react-renderer');
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'Immich',
|
||||
tagline: 'Self-hosted photo and video management solution',
|
||||
tagline: 'High performance self-hosted photo and video backup solution directly from your mobile phone',
|
||||
url: 'https://docs.immich.app',
|
||||
baseUrl: '/',
|
||||
onBrokenLinks: 'throw',
|
||||
@@ -93,15 +93,35 @@ const config = {
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://immich.app/',
|
||||
to: '/overview/quick-start',
|
||||
position: 'right',
|
||||
label: 'Home',
|
||||
label: 'Docs',
|
||||
},
|
||||
{
|
||||
href: 'https://immich.app/roadmap',
|
||||
position: 'right',
|
||||
label: 'Roadmap',
|
||||
},
|
||||
{
|
||||
href: 'https://api.immich.app/',
|
||||
position: 'right',
|
||||
label: 'API',
|
||||
},
|
||||
{
|
||||
href: 'https://immich.store',
|
||||
position: 'right',
|
||||
label: 'Merch',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/immich-app/immich',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://discord.immich.app',
|
||||
label: 'Discord',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
type: 'html',
|
||||
position: 'right',
|
||||
@@ -114,78 +134,19 @@ const config = {
|
||||
style: 'light',
|
||||
links: [
|
||||
{
|
||||
title: 'Download',
|
||||
title: 'Overview',
|
||||
items: [
|
||||
{
|
||||
label: 'Android',
|
||||
href: 'https://get.immich.app/android',
|
||||
label: 'Quick start',
|
||||
to: '/overview/quick-start',
|
||||
},
|
||||
{
|
||||
label: 'iOS',
|
||||
href: 'https://get.immich.app/ios',
|
||||
label: 'Installation',
|
||||
to: '/install/requirements',
|
||||
},
|
||||
{
|
||||
label: 'Server',
|
||||
href: 'https://immich.app/download',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
items: [
|
||||
{
|
||||
label: 'FUTO',
|
||||
href: 'https://futo.tech/',
|
||||
},
|
||||
{
|
||||
label: 'Purchase',
|
||||
href: 'https://buy.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Merch',
|
||||
href: 'https://immich.store/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Sites',
|
||||
items: [
|
||||
{
|
||||
label: 'Home',
|
||||
href: 'https://immich.app',
|
||||
},
|
||||
{
|
||||
label: 'My Immich',
|
||||
href: 'https://my.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Awesome Immich',
|
||||
href: 'https://awesome.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Immich API',
|
||||
href: 'https://api.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Immich Data',
|
||||
href: 'https://data.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Immich Datasets',
|
||||
href: 'https://datasets.immich.app/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Miscellaneous',
|
||||
items: [
|
||||
{
|
||||
label: 'Roadmap',
|
||||
href: 'https://immich.app/roadmap',
|
||||
},
|
||||
{
|
||||
label: 'Cursed Knowledge',
|
||||
href: 'https://immich.app/cursed-knowledge',
|
||||
label: 'Contributing',
|
||||
to: '/overview/support-the-project',
|
||||
},
|
||||
{
|
||||
label: 'Privacy Policy',
|
||||
@@ -194,7 +155,24 @@ const config = {
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Social',
|
||||
title: 'Documentation',
|
||||
items: [
|
||||
{
|
||||
label: 'Roadmap',
|
||||
href: 'https://immich.app/roadmap',
|
||||
},
|
||||
{
|
||||
label: 'API',
|
||||
href: 'https://api.immich.app/',
|
||||
},
|
||||
{
|
||||
label: 'Cursed Knowledge',
|
||||
href: 'https://immich.app/cursed-knowledge',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Links',
|
||||
items: [
|
||||
{
|
||||
label: 'GitHub',
|
||||
|
||||
1
docs/static/_redirects
vendored
1
docs/static/_redirects
vendored
@@ -23,7 +23,6 @@
|
||||
/features/storage-template /administration/storage-template 307
|
||||
/features/user-management /administration/user-management 307
|
||||
/developer/contributing /developer/pr-checklist 307
|
||||
/developer/open-api /api 307
|
||||
/guides/machine-learning /guides/remote-machine-learning 307
|
||||
/administration/password-login /administration/system-settings 307
|
||||
/features/search /features/searching 307
|
||||
|
||||
@@ -1810,8 +1810,9 @@
|
||||
"rate_asset": "Rate Asset",
|
||||
"rating": "Star rating",
|
||||
"rating_clear": "Clear rating",
|
||||
"rating_count": "{count, plural, =0 {Unrated} one {# star} other {# stars}}",
|
||||
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
||||
"rating_description": "Display the EXIF rating in the info panel",
|
||||
"rating_set": "Rating set to {rating, plural, one {# star} other {# stars}}",
|
||||
"reaction_options": "Reaction options",
|
||||
"read_changelog": "Read Changelog",
|
||||
"readonly_mode_disabled": "Read-only mode disabled",
|
||||
@@ -1883,10 +1884,7 @@
|
||||
"reset_pin_code_success": "Successfully reset PIN code",
|
||||
"reset_pin_code_with_password": "You can always reset your PIN code with your password",
|
||||
"reset_sqlite": "Reset SQLite Database",
|
||||
"reset_sqlite_clear_app_data": "Clear Data",
|
||||
"reset_sqlite_confirmation": "Are you sure you want to clear the app data? This will remove all settings and sign you out.",
|
||||
"reset_sqlite_confirmation_note": "Note: You will need to restart the app after clearing.",
|
||||
"reset_sqlite_done": "App data has been cleared. Please restart Immich and log in again.",
|
||||
"reset_sqlite_confirmation": "Are you sure you want to reset the SQLite database? You will need to log out and log in again to resync the data",
|
||||
"reset_sqlite_success": "Successfully reset the SQLite database",
|
||||
"reset_to_default": "Reset to default",
|
||||
"resolution": "Resolution",
|
||||
@@ -1914,7 +1912,6 @@
|
||||
"saved_settings": "Saved settings",
|
||||
"say_something": "Say something",
|
||||
"scaffold_body_error_occurred": "Error occurred",
|
||||
"scaffold_body_error_unrecoverable": "An unrecoverable error has occurred. Please share the error and stack trace on Discord or GitHub so we can help. If advised, you can clear the app data below.",
|
||||
"scan": "Scan",
|
||||
"scan_all_libraries": "Scan All Libraries",
|
||||
"scan_library": "Scan",
|
||||
|
||||
@@ -22,7 +22,48 @@ FROM builder-cpu AS builder-rknn
|
||||
|
||||
# Warning: 25GiB+ disk space required to pull this image
|
||||
# TODO: find a way to reduce the image size
|
||||
FROM rocm/dev-ubuntu-24.04:7.2-complete@sha256:86e11093b4a7ec2a79b1b6701d10e840a6994f21c7e05929b51eb9be361c683a AS builder-rocm
|
||||
FROM rocm/dev-ubuntu-24.04:6.4.4-complete@sha256:31418ac10a3769a71eaef330c07280d1d999d7074621339b8f93c484c35f6078 AS builder-rocm
|
||||
|
||||
# renovate: datasource=github-releases depName=Microsoft/onnxruntime
|
||||
ARG ONNXRUNTIME_VERSION="v1.22.1"
|
||||
WORKDIR /code
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends wget git
|
||||
RUN wget -nv https://github.com/Kitware/CMake/releases/download/v3.31.9/cmake-3.31.9-linux-x86_64.sh && \
|
||||
chmod +x cmake-3.31.9-linux-x86_64.sh && \
|
||||
mkdir -p /code/cmake-3.31.9-linux-x86_64 && \
|
||||
./cmake-3.31.9-linux-x86_64.sh --skip-license --prefix=/code/cmake-3.31.9-linux-x86_64 && \
|
||||
rm cmake-3.31.9-linux-x86_64.sh
|
||||
|
||||
RUN git clone --single-branch --branch "${ONNXRUNTIME_VERSION}" --recursive "https://github.com/Microsoft/onnxruntime" onnxruntime
|
||||
WORKDIR /code/onnxruntime
|
||||
# Fix for multi-threading based on comments in https://github.com/microsoft/onnxruntime/pull/19567
|
||||
# TODO: find a way to fix this without disabling algo caching
|
||||
COPY ./patches/* /tmp/
|
||||
RUN git apply /tmp/*.patch
|
||||
|
||||
RUN /bin/sh ./dockerfiles/scripts/install_common_deps.sh
|
||||
|
||||
ENV PATH=/opt/rocm-venv/bin:/code/cmake-3.31.9-linux-x86_64/bin:${PATH}
|
||||
ENV CCACHE_DIR="/ccache"
|
||||
# Note: the `parallel` setting uses a substantial amount of RAM
|
||||
RUN --mount=type=cache,target=/ccache \
|
||||
./build.sh \
|
||||
--allow_running_as_root \
|
||||
--config Release \
|
||||
--build_wheel \
|
||||
--update \
|
||||
--build \
|
||||
--parallel 48 \
|
||||
--cmake_extra_defines \
|
||||
ONNXRUNTIME_VERSION="${ONNXRUNTIME_VERSION}" \
|
||||
CMAKE_HIP_ARCHITECTURES="gfx900;gfx906;gfx908;gfx90a;gfx940;gfx941;gfx942;gfx1030;gfx1100;gfx1101;gfx1102;gfx1200;gfx1201" \
|
||||
--skip_tests \
|
||||
--use_rocm \
|
||||
--rocm_home=/opt/rocm \
|
||||
--use_cache \
|
||||
--compile_no_warning_as_error
|
||||
RUN mv /code/onnxruntime/build/Linux/Release/dist/*.whl /opt/
|
||||
|
||||
FROM builder-${DEVICE} AS builder
|
||||
|
||||
@@ -38,6 +79,9 @@ 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 \
|
||||
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
|
||||
RUN if [ "$DEVICE" = "rocm" ]; then \
|
||||
uv pip install /opt/onnxruntime_rocm-*.whl; \
|
||||
fi
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:04cd27899595a99dfe77709d96f08876bf2ee99139ee2f0fe9ac948005034e5b AS prod-cpu
|
||||
|
||||
@@ -76,11 +120,7 @@ COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3
|
||||
COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
||||
COPY --from=builder-cuda /usr/local/lib/libpython3.11.so /usr/local/lib/libpython3.11.so
|
||||
|
||||
FROM rocm/dev-ubuntu-24.04:7.2-complete@sha256:86e11093b4a7ec2a79b1b6701d10e840a6994f21c7e05929b51eb9be361c683a AS prod-rocm
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -yqq migraphx miopen-hip && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
FROM rocm/dev-ubuntu-24.04:6.4.4-complete@sha256:31418ac10a3769a71eaef330c07280d1d999d7074621339b8f93c484c35f6078 AS prod-rocm
|
||||
|
||||
FROM prod-cpu AS prod-armnn
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@ class Settings(BaseSettings):
|
||||
preload: PreloadModelData | None = None
|
||||
max_batch_size: MaxBatchSize | None = None
|
||||
openvino_precision: ModelPrecision = ModelPrecision.FP32
|
||||
rocm_precision: ModelPrecision = ModelPrecision.FP32
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
|
||||
@@ -90,7 +90,7 @@ _PADDLE_MODELS = {
|
||||
|
||||
SUPPORTED_PROVIDERS = [
|
||||
"CUDAExecutionProvider",
|
||||
"MIGraphXExecutionProvider",
|
||||
"ROCMExecutionProvider",
|
||||
"OpenVINOExecutionProvider",
|
||||
"CoreMLExecutionProvider",
|
||||
"CPUExecutionProvider",
|
||||
|
||||
@@ -8,7 +8,7 @@ import onnxruntime as ort
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from immich_ml.models.constants import SUPPORTED_PROVIDERS
|
||||
from immich_ml.schemas import ModelPrecision, SessionNode
|
||||
from immich_ml.schemas import SessionNode
|
||||
|
||||
from ..config import log, settings
|
||||
|
||||
@@ -90,17 +90,8 @@ class OrtSession:
|
||||
match provider:
|
||||
case "CPUExecutionProvider":
|
||||
options = {"arena_extend_strategy": "kSameAsRequested"}
|
||||
case "CUDAExecutionProvider":
|
||||
case "CUDAExecutionProvider" | "ROCMExecutionProvider":
|
||||
options = {"arena_extend_strategy": "kSameAsRequested", "device_id": settings.device_id}
|
||||
case "MIGraphXExecutionProvider":
|
||||
migraphx_dir = self.model_path.parent / "migraphx"
|
||||
# MIGraphX does not create the underlying folder and will crash if it does not exist
|
||||
migraphx_dir.mkdir(parents=True, exist_ok=True)
|
||||
options = {
|
||||
"device_id": settings.device_id,
|
||||
"migraphx_model_cache_dir": migraphx_dir.as_posix(),
|
||||
"migraphx_fp16_enable": "1" if settings.rocm_precision == ModelPrecision.FP16 else "0",
|
||||
}
|
||||
case "OpenVINOExecutionProvider":
|
||||
openvino_dir = self.model_path.parent / "openvino"
|
||||
device = f"GPU.{settings.device_id}"
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
commit 16839b58d9b3c3162a67ce5d776b36d4d24e801f
|
||||
Author: mertalev <101130780+mertalev@users.noreply.github.com>
|
||||
Date: Wed Mar 5 11:25:38 2025 -0500
|
||||
|
||||
disable algo caching (attributed to @dmnieto in https://github.com/microsoft/onnxruntime/pull/19567)
|
||||
|
||||
diff --git a/onnxruntime/core/providers/rocm/nn/conv.cc b/onnxruntime/core/providers/rocm/nn/conv.cc
|
||||
index d7f47d07a8..4060a2af52 100644
|
||||
--- a/onnxruntime/core/providers/rocm/nn/conv.cc
|
||||
+++ b/onnxruntime/core/providers/rocm/nn/conv.cc
|
||||
@@ -127,7 +127,6 @@ Status Conv<T, NHWC>::UpdateState(OpKernelContext* context, bool bias_expected)
|
||||
|
||||
if (w_dims_changed) {
|
||||
s_.last_w_dims = gsl::make_span(w_dims);
|
||||
- s_.cached_benchmark_fwd_results.clear();
|
||||
}
|
||||
|
||||
ORT_RETURN_IF_ERROR(conv_attrs_.ValidateInputShape(X->Shape(), W->Shape(), channels_last, channels_last));
|
||||
@@ -277,35 +276,6 @@ Status Conv<T, NHWC>::UpdateState(OpKernelContext* context, bool bias_expected)
|
||||
HIP_CALL_THROW(hipMalloc(&s_.b_zero, malloc_size));
|
||||
HIP_CALL_THROW(hipMemsetAsync(s_.b_zero, 0, malloc_size, Stream(context)));
|
||||
}
|
||||
-
|
||||
- if (!s_.cached_benchmark_fwd_results.contains(x_dims_miopen)) {
|
||||
- miopenConvAlgoPerf_t perf;
|
||||
- int algo_count = 1;
|
||||
- const ROCMExecutionProvider* rocm_ep = static_cast<const ROCMExecutionProvider*>(this->Info().GetExecutionProvider());
|
||||
- static constexpr int num_algos = MIOPEN_CONVOLUTION_FWD_ALGO_COUNT;
|
||||
- size_t max_ws_size = rocm_ep->GetMiopenConvUseMaxWorkspace() ? GetMaxWorkspaceSize(GetMiopenHandle(context), s_, kAllAlgos, num_algos, rocm_ep->GetDeviceId())
|
||||
- : AlgoSearchWorkspaceSize;
|
||||
- IAllocatorUniquePtr<void> algo_search_workspace = GetTransientScratchBuffer<void>(max_ws_size);
|
||||
- MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionForwardAlgorithm(
|
||||
- GetMiopenHandle(context),
|
||||
- s_.x_tensor,
|
||||
- s_.x_data,
|
||||
- s_.w_desc,
|
||||
- s_.w_data,
|
||||
- s_.conv_desc,
|
||||
- s_.y_tensor,
|
||||
- s_.y_data,
|
||||
- 1, // requestedAlgoCount
|
||||
- &algo_count, // returnedAlgoCount
|
||||
- &perf,
|
||||
- algo_search_workspace.get(),
|
||||
- max_ws_size,
|
||||
- false)); // Do not do exhaustive algo search.
|
||||
- s_.cached_benchmark_fwd_results.insert(x_dims_miopen, {perf.fwd_algo, perf.memory});
|
||||
- }
|
||||
- const auto& perf = s_.cached_benchmark_fwd_results.at(x_dims_miopen);
|
||||
- s_.fwd_algo = perf.fwd_algo;
|
||||
- s_.workspace_bytes = perf.memory;
|
||||
} else {
|
||||
// set Y
|
||||
s_.Y = context->Output(0, TensorShape(s_.y_dims));
|
||||
@@ -319,6 +289,31 @@ Status Conv<T, NHWC>::UpdateState(OpKernelContext* context, bool bias_expected)
|
||||
s_.y_data = reinterpret_cast<HipT*>(s_.Y->MutableData<T>());
|
||||
}
|
||||
}
|
||||
+
|
||||
+ miopenConvAlgoPerf_t perf;
|
||||
+ int algo_count = 1;
|
||||
+ const ROCMExecutionProvider* rocm_ep = static_cast<const ROCMExecutionProvider*>(this->Info().GetExecutionProvider());
|
||||
+ static constexpr int num_algos = MIOPEN_CONVOLUTION_FWD_ALGO_COUNT;
|
||||
+ size_t max_ws_size = rocm_ep->GetMiopenConvUseMaxWorkspace() ? GetMaxWorkspaceSize(GetMiopenHandle(context), s_, kAllAlgos, num_algos, rocm_ep->GetDeviceId())
|
||||
+ : AlgoSearchWorkspaceSize;
|
||||
+ IAllocatorUniquePtr<void> algo_search_workspace = GetTransientScratchBuffer<void>(max_ws_size);
|
||||
+ MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionForwardAlgorithm(
|
||||
+ GetMiopenHandle(context),
|
||||
+ s_.x_tensor,
|
||||
+ s_.x_data,
|
||||
+ s_.w_desc,
|
||||
+ s_.w_data,
|
||||
+ s_.conv_desc,
|
||||
+ s_.y_tensor,
|
||||
+ s_.y_data,
|
||||
+ 1, // requestedAlgoCount
|
||||
+ &algo_count, // returnedAlgoCount
|
||||
+ &perf,
|
||||
+ algo_search_workspace.get(),
|
||||
+ max_ws_size,
|
||||
+ false)); // Do not do exhaustive algo search.
|
||||
+ s_.fwd_algo = perf.fwd_algo;
|
||||
+ s_.workspace_bytes = perf.memory;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
diff --git a/onnxruntime/core/providers/rocm/nn/conv.h b/onnxruntime/core/providers/rocm/nn/conv.h
|
||||
index bc9846203e..d54218f258 100644
|
||||
--- a/onnxruntime/core/providers/rocm/nn/conv.h
|
||||
+++ b/onnxruntime/core/providers/rocm/nn/conv.h
|
||||
@@ -108,9 +108,6 @@ class lru_unordered_map {
|
||||
list_type lru_list_;
|
||||
};
|
||||
|
||||
-// cached miopen descriptors
|
||||
-constexpr size_t MAX_CACHED_ALGO_PERF_RESULTS = 10000;
|
||||
-
|
||||
template <typename AlgoPerfType>
|
||||
struct MiopenConvState {
|
||||
// if x/w dims changed, update algo and miopenTensors
|
||||
@@ -148,9 +145,6 @@ struct MiopenConvState {
|
||||
decltype(AlgoPerfType().memory) memory;
|
||||
};
|
||||
|
||||
- lru_unordered_map<TensorShapeVector, PerfFwdResultParams, vector_hash> cached_benchmark_fwd_results{MAX_CACHED_ALGO_PERF_RESULTS};
|
||||
- lru_unordered_map<TensorShapeVector, PerfBwdResultParams, vector_hash> cached_benchmark_bwd_results{MAX_CACHED_ALGO_PERF_RESULTS};
|
||||
-
|
||||
// Some properties needed to support asymmetric padded Conv nodes
|
||||
bool post_slicing_required;
|
||||
TensorShapeVector slice_starts;
|
||||
diff --git a/onnxruntime/core/providers/rocm/nn/conv_transpose.cc b/onnxruntime/core/providers/rocm/nn/conv_transpose.cc
|
||||
index 7447113fdf..a662e35b2e 100644
|
||||
--- a/onnxruntime/core/providers/rocm/nn/conv_transpose.cc
|
||||
+++ b/onnxruntime/core/providers/rocm/nn/conv_transpose.cc
|
||||
@@ -76,7 +76,6 @@ Status ConvTranspose<T, NHWC>::DoConvTranspose(OpKernelContext* context, bool dy
|
||||
|
||||
if (w_dims_changed) {
|
||||
s_.last_w_dims = gsl::make_span(w_dims);
|
||||
- s_.cached_benchmark_bwd_results.clear();
|
||||
}
|
||||
|
||||
ConvTransposeAttributes::Prepare p;
|
||||
@@ -126,35 +125,29 @@ Status ConvTranspose<T, NHWC>::DoConvTranspose(OpKernelContext* context, bool dy
|
||||
}
|
||||
|
||||
y_data = reinterpret_cast<HipT*>(p.Y->MutableData<T>());
|
||||
-
|
||||
- if (!s_.cached_benchmark_bwd_results.contains(x_dims)) {
|
||||
- IAllocatorUniquePtr<void> algo_search_workspace = GetScratchBuffer<void>(AlgoSearchWorkspaceSize, context->GetComputeStream());
|
||||
-
|
||||
- miopenConvAlgoPerf_t perf;
|
||||
- int algo_count = 1;
|
||||
- MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionBackwardDataAlgorithm(
|
||||
- GetMiopenHandle(context),
|
||||
- s_.x_tensor,
|
||||
- x_data,
|
||||
- s_.w_desc,
|
||||
- w_data,
|
||||
- s_.conv_desc,
|
||||
- s_.y_tensor,
|
||||
- y_data,
|
||||
- 1,
|
||||
- &algo_count,
|
||||
- &perf,
|
||||
- algo_search_workspace.get(),
|
||||
- AlgoSearchWorkspaceSize,
|
||||
- false));
|
||||
- s_.cached_benchmark_bwd_results.insert(x_dims, {perf.bwd_data_algo, perf.memory});
|
||||
- }
|
||||
-
|
||||
- const auto& perf = s_.cached_benchmark_bwd_results.at(x_dims);
|
||||
- s_.bwd_data_algo = perf.bwd_data_algo;
|
||||
- s_.workspace_bytes = perf.memory;
|
||||
}
|
||||
|
||||
+ IAllocatorUniquePtr<void> algo_search_workspace = GetScratchBuffer<void>(AlgoSearchWorkspaceSize, context->GetComputeStream());
|
||||
+ miopenConvAlgoPerf_t perf;
|
||||
+ int algo_count = 1;
|
||||
+ MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionBackwardDataAlgorithm(
|
||||
+ GetMiopenHandle(context),
|
||||
+ s_.x_tensor,
|
||||
+ x_data,
|
||||
+ s_.w_desc,
|
||||
+ w_data,
|
||||
+ s_.conv_desc,
|
||||
+ s_.y_tensor,
|
||||
+ y_data,
|
||||
+ 1,
|
||||
+ &algo_count,
|
||||
+ &perf,
|
||||
+ algo_search_workspace.get(),
|
||||
+ AlgoSearchWorkspaceSize,
|
||||
+ false));
|
||||
+ s_.bwd_data_algo = perf.bwd_data_algo;
|
||||
+ s_.workspace_bytes = perf.memory;
|
||||
+
|
||||
// The following block will be executed in case there has been no change in the shapes of the
|
||||
// input and the filter compared to the previous run
|
||||
if (!y_data) {
|
||||
33
machine-learning/patches/0002-install-system-deps.patch
Normal file
33
machine-learning/patches/0002-install-system-deps.patch
Normal file
@@ -0,0 +1,33 @@
|
||||
diff --git a/dockerfiles/scripts/install_common_deps.sh b/dockerfiles/scripts/install_common_deps.sh
|
||||
index bbb672a99e..0dc652fbda 100644
|
||||
--- a/dockerfiles/scripts/install_common_deps.sh
|
||||
+++ b/dockerfiles/scripts/install_common_deps.sh
|
||||
@@ -8,16 +8,23 @@ apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
libcurl4-openssl-dev \
|
||||
libssl-dev \
|
||||
- python3-dev
|
||||
+ python3-dev \
|
||||
+ ccache
|
||||
|
||||
# Dependencies: conda
|
||||
-wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-4.5.11-Linux-x86_64.sh -O ~/miniconda.sh --no-check-certificate && /bin/bash ~/miniconda.sh -b -p /opt/miniconda
|
||||
+wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-py312_25.9.1-1-Linux-x86_64.sh -O ~/miniconda.sh && /bin/bash ~/miniconda.sh -b -p /opt/miniconda
|
||||
rm ~/miniconda.sh
|
||||
/opt/miniconda/bin/conda clean -ya
|
||||
|
||||
-pip install numpy
|
||||
-pip install packaging
|
||||
-pip install "wheel>=0.35.1"
|
||||
+# Dependencies: venv and packages
|
||||
+/opt/miniconda/bin/python3 -m venv /opt/rocm-venv
|
||||
+/opt/rocm-venv/bin/pip install --no-cache-dir --upgrade pip
|
||||
+/opt/rocm-venv/bin/pip install --no-cache-dir \
|
||||
+ "numpy==2.3.4" \
|
||||
+ "packaging==25.0" \
|
||||
+ "wheel==0.45.1" \
|
||||
+ "setuptools==80.9.0"
|
||||
+
|
||||
rm -rf /opt/miniconda/pkgs
|
||||
|
||||
# Dependencies: cmake
|
||||
@@ -52,7 +52,7 @@ cuda = ["onnxruntime-gpu>=1.23.2,<2"]
|
||||
openvino = ["onnxruntime-openvino>=1.23.0,<2"]
|
||||
armnn = ["onnxruntime>=1.23.2,<2"]
|
||||
rknn = ["onnxruntime>=1.23.2,<2", "rknn-toolkit-lite2>=2.3.0,<3"]
|
||||
rocm = ["onnxruntime-migraphx>=1.23.2,<2"]
|
||||
rocm = []
|
||||
|
||||
[tool.uv]
|
||||
compile-bytecode = true
|
||||
|
||||
@@ -179,7 +179,7 @@ class TestOrtSession:
|
||||
OV_EP = ["OpenVINOExecutionProvider", "CPUExecutionProvider"]
|
||||
CUDA_EP_OUT_OF_ORDER = ["CPUExecutionProvider", "CUDAExecutionProvider"]
|
||||
TRT_EP = ["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"]
|
||||
ROCM_EP = ["MIGraphXExecutionProvider", "CPUExecutionProvider"]
|
||||
ROCM_EP = ["ROCMExecutionProvider", "CPUExecutionProvider"]
|
||||
COREML_EP = ["CoreMLExecutionProvider", "CPUExecutionProvider"]
|
||||
|
||||
@pytest.mark.providers(CPU_EP)
|
||||
@@ -289,38 +289,12 @@ class TestOrtSession:
|
||||
|
||||
assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}]
|
||||
|
||||
def test_sets_provider_options_for_rocm(self, mocker: MockerFixture) -> None:
|
||||
model_path = "/cache/ViT-B-32__openai/textual/model.onnx"
|
||||
def test_sets_provider_options_for_rocm(self) -> None:
|
||||
os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1"
|
||||
mkdir = mocker.patch("immich_ml.sessions.ort.Path.mkdir")
|
||||
|
||||
session = OrtSession(model_path, providers=["MIGraphXExecutionProvider"])
|
||||
session = OrtSession("ViT-B-32__openai", providers=["ROCMExecutionProvider"])
|
||||
|
||||
assert session.provider_options == [
|
||||
{
|
||||
"device_id": "1",
|
||||
"migraphx_model_cache_dir": "/cache/ViT-B-32__openai/textual/migraphx",
|
||||
"migraphx_fp16_enable": "0",
|
||||
}
|
||||
]
|
||||
mkdir.assert_called_once_with(parents=True, exist_ok=True)
|
||||
|
||||
def test_sets_rocm_to_fp16_if_enabled(self, path: mock.Mock, mocker: MockerFixture) -> None:
|
||||
model_path = "/cache/ViT-B-32__openai/textual/model.onnx"
|
||||
os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1"
|
||||
mocker.patch.object(settings, "rocm_precision", ModelPrecision.FP16)
|
||||
mkdir = mocker.patch("immich_ml.sessions.ort.Path.mkdir")
|
||||
|
||||
session = OrtSession(model_path, providers=["MIGraphXExecutionProvider"])
|
||||
|
||||
assert session.provider_options == [
|
||||
{
|
||||
"device_id": "1",
|
||||
"migraphx_model_cache_dir": "/cache/ViT-B-32__openai/textual/migraphx",
|
||||
"migraphx_fp16_enable": "1",
|
||||
}
|
||||
]
|
||||
mkdir.assert_called_once_with(parents=True, exist_ok=True)
|
||||
assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}]
|
||||
|
||||
def test_sets_provider_options_kwarg(self) -> None:
|
||||
session = OrtSession(
|
||||
|
||||
93
machine-learning/uv.lock
generated
93
machine-learning/uv.lock
generated
@@ -960,9 +960,6 @@ rknn = [
|
||||
{ name = "onnxruntime" },
|
||||
{ name = "rknn-toolkit-lite2" },
|
||||
]
|
||||
rocm = [
|
||||
{ name = "onnxruntime-migraphx" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
@@ -1016,7 +1013,6 @@ requires-dist = [
|
||||
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
|
||||
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" },
|
||||
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.23.2,<2" },
|
||||
{ name = "onnxruntime-migraphx", marker = "extra == 'rocm'", specifier = ">=1.23.2,<2" },
|
||||
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.23.0,<2" },
|
||||
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
||||
{ name = "orjson", specifier = ">=3.9.5" },
|
||||
@@ -1433,55 +1429,32 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "msgpack"
|
||||
version = "1.1.2"
|
||||
version = "1.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/d5/5662032db1571110b5b51647aed4b56dfbd01bfae789fa566a2be1f385d1/msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", size = 166311, upload-time = "2023-09-28T13:20:36.726Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/b3/309de40dc7406b7f3492332c5ee2b492a593c2a9bb97ea48ebf2f5279999/msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", size = 305096, upload-time = "2023-09-28T13:18:49.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/56/a677cd761a2cefb2e3ffe7e684633294dccb161d78e8ea6da9277e45b4a2/msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", size = 235210, upload-time = "2023-09-28T13:18:51.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/4e/1ab4a982cbd90f988e49f849fc1212f2c04a59870c59daabf8950617e2aa/msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", size = 231952, upload-time = "2023-09-28T13:18:52.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/74/bd02044eb628c7361ad2bd8c1a6147af5c6c2bbceb77b3b1da20f4a8a9c5/msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", size = 549511, upload-time = "2023-09-28T13:18:54.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/09/dee50913ba5cc047f7fd7162f09453a676e7935c84b3bf3a398e12108677/msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", size = 557980, upload-time = "2023-09-28T13:18:56.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/a5/78a7d87f5f8ffe4c32167afa15d4957db649bab4822f909d8d765339bbab/msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", size = 545547, upload-time = "2023-09-28T13:18:57.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/53/698c10913947f97f6fe7faad86a34e6aa1b66cea2df6f99105856bd346d9/msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", size = 554669, upload-time = "2023-09-28T13:18:58.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/3f/9730c6cb574b15d349b80cd8523a7df4b82058528339f952ea1c32ac8a10/msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", size = 583353, upload-time = "2023-09-28T13:19:01.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/bc/dc184d943692671149848438fb3bed3a3de288ce7998cb91bc98f40f201b/msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", size = 557455, upload-time = "2023-09-28T13:19:03.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/7b/1bc69d4a56c8d2f4f2dfbe4722d40344af9a85b6fb3b09cfb350ba6a42f6/msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", size = 216367, upload-time = "2023-09-28T13:19:04.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/3d/c8dd23050eefa3d9b9c5b8329ed3308c2f2f80f65825e9ea4b7fa621cdab/msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", size = 222860, upload-time = "2023-09-28T13:19:06.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/47/20dff6b4512cf3575550c8801bc53fe7d540f4efef9c5c37af51760fcdcf/msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", size = 305759, upload-time = "2023-09-28T13:19:08.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/8a/34f1726d2c9feccec3d946776e9bce8f20ae09d8b91899fc20b296c942af/msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", size = 235330, upload-time = "2023-09-28T13:19:09.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/f6/e64c72577d6953789c3cb051b059a4b56317056b3c65013952338ed8a34e/msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", size = 232537, upload-time = "2023-09-28T13:19:10.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/75/1ed3a96e12941873fd957e016cc40c0c178861a872bd45e75b9a188eb422/msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", size = 546561, upload-time = "2023-09-28T13:19:12.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/0a/c6a1390f9c6a31da0fecbbfdb86b1cb39ad302d9e24f9cca3d9e14c364f0/msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", size = 559009, upload-time = "2023-09-28T13:19:14.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/74/99f6077754665613ea1f37b3d91c10129f6976b7721ab4d0973023808e5a/msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", size = 543882, upload-time = "2023-09-28T13:19:16.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/7e/dc0dc8de2bf27743b31691149258f9b1bd4bf3c44c105df3df9b97081cd1/msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", size = 546949, upload-time = "2023-09-28T13:19:18.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/61/91bae9474def032f6c333d62889bbeda9e1554c6b123375ceeb1767efd78/msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", size = 579836, upload-time = "2023-09-28T13:19:19.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/4d/d98592099d4f18945f89cf3e634dc0cb128bb33b1b93f85a84173d35e181/msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", size = 556587, upload-time = "2023-09-28T13:19:21.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/44/6556ffe169bf2c0e974e2ea25fb82a7e55ebcf52a81b03a5e01820de5f84/msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", size = 216509, upload-time = "2023-09-28T13:19:23.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/c1/63903f30d51d165e132e5221a2a4a1bbfab7508b68131c871d70bffac78a/msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", size = 223287, upload-time = "2023-09-28T13:19:25.097Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1728,24 +1701,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/94/a3b20276261f5e64dbd72bda656af988282cff01f18c2685953600e2f810/onnxruntime_gpu-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2cee7e12b0f4813c62f9a48df83fd01d066cc970400c832252cf3c155a6957", size = 252633096, upload-time = "2026-02-05T17:24:53.248Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime-migraphx"
|
||||
version = "1.24.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flatbuffers" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "sympy" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/da/ca7ebc1a8d1193c97ceb9a05fad50f675eb955dc51beb7eb9ba89c8e7db0/onnxruntime_migraphx-1.24.2-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:a2b434fb8880cac2b268950bdf279f33741d29c1f1c5461d27af835e8e288043", size = 20339710, upload-time = "2026-02-21T07:25:13.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/2e/8c83ec45a9365b4256495ca55eea30da7f03b02177b6da423c7da1ff5f6a/onnxruntime_migraphx-1.24.2-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ec814818da952bda3062e26f56c88bb713c00491ef91f86716c8d7346f9bc31b", size = 20341883, upload-time = "2026-02-21T07:25:17.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/52/4776ac68dbc46ca02c9a14cc9e5c496017f47a18cedf606cc38f4911b96a/onnxruntime_migraphx-1.24.2-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:20e497538362170af639b03a40249d7ed61b873ac354f20d732b90252206e320", size = 20342422, upload-time = "2026-02-21T07:25:22.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/44/db9035204a3363f9c0a4822c68e9a7520c13ef8d261f96b89b1375106dab/onnxruntime_migraphx-1.24.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:9d7f1b1a2b9651143a2080b4f42ee99eead02023de1855d1b8a02199a9c179aa", size = 20343783, upload-time = "2026-02-21T07:25:29.155Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime-openvino"
|
||||
version = "1.23.0"
|
||||
|
||||
1
mobile/drift_schemas/main/drift_schema_v21.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v21.json
generated
Normal file
File diff suppressed because one or more lines are too long
126
mobile/lib/domain/models/ocr.model.dart
Normal file
126
mobile/lib/domain/models/ocr.model.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
class DriftOcr {
|
||||
final String id;
|
||||
final String assetId;
|
||||
final double x1;
|
||||
final double y1;
|
||||
final double x2;
|
||||
final double y2;
|
||||
final double x3;
|
||||
final double y3;
|
||||
final double x4;
|
||||
final double y4;
|
||||
final double boxScore;
|
||||
final double textScore;
|
||||
final String text;
|
||||
final bool isVisible;
|
||||
|
||||
const DriftOcr({
|
||||
required this.id,
|
||||
required this.assetId,
|
||||
required this.x1,
|
||||
required this.y1,
|
||||
required this.x2,
|
||||
required this.y2,
|
||||
required this.x3,
|
||||
required this.y3,
|
||||
required this.x4,
|
||||
required this.y4,
|
||||
required this.boxScore,
|
||||
required this.textScore,
|
||||
required this.text,
|
||||
required this.isVisible,
|
||||
});
|
||||
|
||||
DriftOcr copyWith({
|
||||
String? id,
|
||||
String? assetId,
|
||||
double? x1,
|
||||
double? y1,
|
||||
double? x2,
|
||||
double? y2,
|
||||
double? x3,
|
||||
double? y3,
|
||||
double? x4,
|
||||
double? y4,
|
||||
double? boxScore,
|
||||
double? textScore,
|
||||
String? text,
|
||||
bool? isVisible,
|
||||
}) {
|
||||
return DriftOcr(
|
||||
id: id ?? this.id,
|
||||
assetId: assetId ?? this.assetId,
|
||||
x1: x1 ?? this.x1,
|
||||
y1: y1 ?? this.y1,
|
||||
x2: x2 ?? this.x2,
|
||||
y2: y2 ?? this.y2,
|
||||
x3: x3 ?? this.x3,
|
||||
y3: y3 ?? this.y3,
|
||||
x4: x4 ?? this.x4,
|
||||
y4: y4 ?? this.y4,
|
||||
boxScore: boxScore ?? this.boxScore,
|
||||
textScore: textScore ?? this.textScore,
|
||||
text: text ?? this.text,
|
||||
isVisible: isVisible ?? this.isVisible,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Ocr {
|
||||
id: $id,
|
||||
assetId: $assetId,
|
||||
x1: $x1,
|
||||
y1: $y1,
|
||||
x2: $x2,
|
||||
y2: $y2,
|
||||
x3: $x3,
|
||||
y3: $y3,
|
||||
x4: $x4,
|
||||
y4: $y4,
|
||||
boxScore: $boxScore,
|
||||
textScore: $textScore,
|
||||
text: $text,
|
||||
isVisible: $isVisible
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is DriftOcr &&
|
||||
other.id == id &&
|
||||
other.assetId == assetId &&
|
||||
other.x1 == x1 &&
|
||||
other.y1 == y1 &&
|
||||
other.x2 == x2 &&
|
||||
other.y2 == y2 &&
|
||||
other.x3 == x3 &&
|
||||
other.y3 == y3 &&
|
||||
other.x4 == x4 &&
|
||||
other.y4 == y4 &&
|
||||
other.boxScore == boxScore &&
|
||||
other.textScore == textScore &&
|
||||
other.text == text &&
|
||||
other.isVisible == isVisible;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
assetId.hashCode ^
|
||||
x1.hashCode ^
|
||||
y1.hashCode ^
|
||||
x2.hashCode ^
|
||||
y2.hashCode ^
|
||||
x3.hashCode ^
|
||||
y3.hashCode ^
|
||||
x4.hashCode ^
|
||||
y4.hashCode ^
|
||||
boxScore.hashCode ^
|
||||
textScore.hashCode ^
|
||||
text.hashCode ^
|
||||
isVisible.hashCode;
|
||||
}
|
||||
}
|
||||
12
mobile/lib/domain/services/ocr.service.dart
Normal file
12
mobile/lib/domain/services/ocr.service.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/ocr.repository.dart';
|
||||
|
||||
class DriftOcrService {
|
||||
final DriftOcrRepository _repository;
|
||||
|
||||
const DriftOcrService(this._repository);
|
||||
|
||||
Future<List<DriftOcr>?> get(String assetId) {
|
||||
return _repository.get(assetId);
|
||||
}
|
||||
}
|
||||
@@ -290,6 +290,10 @@ class SyncStreamService {
|
||||
return _syncStreamRepository.updateAssetFacesV2(data.cast());
|
||||
case SyncEntityType.assetFaceDeleteV1:
|
||||
return _syncStreamRepository.deleteAssetFacesV1(data.cast());
|
||||
case SyncEntityType.assetOcrV1:
|
||||
return _syncStreamRepository.updateAssetOcrV1(data.cast());
|
||||
case SyncEntityType.assetOcrDeleteV1:
|
||||
return _syncStreamRepository.deleteAssetOcrV1(data.cast());
|
||||
default:
|
||||
_logger.warning("Unknown sync data type: $type");
|
||||
}
|
||||
|
||||
33
mobile/lib/infrastructure/entities/asset_ocr.entity.dart
Normal file
33
mobile/lib/infrastructure/entities/asset_ocr.entity.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class AssetOcrEntity extends Table with DriftDefaultsMixin {
|
||||
const AssetOcrEntity();
|
||||
|
||||
TextColumn get id => text()();
|
||||
|
||||
TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
RealColumn get x1 => real()();
|
||||
RealColumn get y1 => real()();
|
||||
|
||||
RealColumn get x2 => real()();
|
||||
RealColumn get y2 => real()();
|
||||
|
||||
RealColumn get x3 => real()();
|
||||
RealColumn get y3 => real()();
|
||||
|
||||
RealColumn get x4 => real()();
|
||||
RealColumn get y4 => real()();
|
||||
|
||||
RealColumn get boxScore => real()();
|
||||
RealColumn get textScore => real()();
|
||||
|
||||
TextColumn get recognizedText => text().named('text')();
|
||||
|
||||
BoolColumn get isVisible => boolean().withDefault(const Constant(true))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
1284
mobile/lib/infrastructure/entities/asset_ocr.entity.drift.dart
generated
Normal file
1284
mobile/lib/infrastructure/entities/asset_ocr.entity.drift.dart
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
@@ -66,6 +67,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||
AssetFaceEntity,
|
||||
StoreEntity,
|
||||
TrashedLocalAssetEntity,
|
||||
AssetOcrEntity,
|
||||
],
|
||||
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
||||
)
|
||||
@@ -97,7 +99,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 20;
|
||||
int get schemaVersion => 21;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -230,6 +232,9 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
|
||||
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
|
||||
},
|
||||
from20To21: (m, v21) async {
|
||||
await m.create(v21.assetOcrEntity);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -41,9 +41,11 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
||||
as i19;
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
|
||||
as i20;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart'
|
||||
as i21;
|
||||
import 'package:drift/internal/modular.dart' as i22;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
as i22;
|
||||
import 'package:drift/internal/modular.dart' as i23;
|
||||
|
||||
abstract class $Drift extends i0.GeneratedDatabase {
|
||||
$Drift(i0.QueryExecutor e) : super(e);
|
||||
@@ -85,9 +87,12 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this);
|
||||
late final i20.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i20
|
||||
.$TrashedLocalAssetEntityTable(this);
|
||||
i21.MergedAssetDrift get mergedAssetDrift => i22.ReadDatabaseContainer(
|
||||
late final i21.$AssetOcrEntityTable assetOcrEntity = i21.$AssetOcrEntityTable(
|
||||
this,
|
||||
).accessor<i21.MergedAssetDrift>(i21.MergedAssetDrift.new);
|
||||
);
|
||||
i22.MergedAssetDrift get mergedAssetDrift => i23.ReadDatabaseContainer(
|
||||
this,
|
||||
).accessor<i22.MergedAssetDrift>(i22.MergedAssetDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@@ -125,6 +130,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetOcrEntity,
|
||||
i10.idxPartnerSharedWithId,
|
||||
i11.idxLatLng,
|
||||
i12.idxRemoteAlbumAssetAlbumAsset,
|
||||
@@ -325,6 +331,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
),
|
||||
result: [i0.TableUpdate('asset_face_entity', kind: i0.UpdateKind.update)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'remote_asset_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('asset_ocr_entity', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
]);
|
||||
@override
|
||||
i0.DriftDatabaseOptions get options =>
|
||||
@@ -384,4 +397,6 @@ class $DriftManager {
|
||||
_db,
|
||||
_db.trashedLocalAssetEntity,
|
||||
);
|
||||
i21.$$AssetOcrEntityTableTableManager get assetOcrEntity =>
|
||||
i21.$$AssetOcrEntityTableTableManager(_db, _db.assetOcrEntity);
|
||||
}
|
||||
|
||||
@@ -8904,6 +8904,648 @@ i1.GeneratedColumn<bool> _column_102(String aliasedName) =>
|
||||
),
|
||||
defaultValue: const CustomExpression('1'),
|
||||
);
|
||||
|
||||
final class Schema21 extends i0.VersionedSchema {
|
||||
Schema21({required super.database}) : super(version: 21);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAlbumAssetAlbumAsset,
|
||||
idxRemoteAlbumOwnerId,
|
||||
idxLocalAssetChecksum,
|
||||
idxLocalAssetCloudId,
|
||||
idxStackPrimaryAssetId,
|
||||
idxRemoteAssetOwnerChecksum,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
idxRemoteAssetStackId,
|
||||
idxRemoteAssetLocalDateTimeDay,
|
||||
idxRemoteAssetLocalDateTimeMonth,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
remoteAssetCloudIdEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetOcrEntity,
|
||||
idxPartnerSharedWithId,
|
||||
idxLatLng,
|
||||
idxRemoteAlbumAssetAlbumAsset,
|
||||
idxRemoteAssetCloudId,
|
||||
idxPersonOwnerId,
|
||||
idxAssetFacePersonId,
|
||||
idxAssetFaceAssetId,
|
||||
idxTrashedLocalAssetChecksum,
|
||||
idxTrashedLocalAssetAlbum,
|
||||
];
|
||||
late final Shape20 userEntity = Shape20(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_3,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_91,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape28 remoteAssetEntity = Shape28(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
_column_16,
|
||||
_column_17,
|
||||
_column_18,
|
||||
_column_19,
|
||||
_column_20,
|
||||
_column_21,
|
||||
_column_86,
|
||||
_column_101,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape3 stackEntity = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'stack_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape26 localAssetEntity = Shape26(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_22,
|
||||
_column_14,
|
||||
_column_23,
|
||||
_column_98,
|
||||
_column_96,
|
||||
_column_46,
|
||||
_column_47,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape9 remoteAlbumEntity = Shape9(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_56,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_15,
|
||||
_column_57,
|
||||
_column_58,
|
||||
_column_59,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape19 localAlbumEntity = Shape19(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_5,
|
||||
_column_31,
|
||||
_column_32,
|
||||
_column_90,
|
||||
_column_33,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape22 localAlbumAssetEntity = Shape22(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_34, _column_35, _column_33],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_local_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAlbumOwnerId = i1.Index(
|
||||
'idx_remote_album_owner_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)',
|
||||
);
|
||||
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||
'idx_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||
'idx_local_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||
);
|
||||
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||
'idx_stack_primary_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||
'idx_remote_asset_owner_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_library_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||
'idx_remote_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||
'idx_remote_asset_stack_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
|
||||
'idx_remote_asset_local_date_time_day',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
|
||||
);
|
||||
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
|
||||
'idx_remote_asset_local_date_time_month',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
|
||||
);
|
||||
late final Shape21 authUserEntity = Shape21(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'auth_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_3,
|
||||
_column_2,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_92,
|
||||
_column_93,
|
||||
_column_7,
|
||||
_column_94,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape4 userMetadataEntity = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_metadata_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||
columns: [_column_25, _column_26, _column_27],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape5 partnerEntity = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'partner_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||
columns: [_column_28, _column_29, _column_30],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape8 remoteExifEntity = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_exif_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_36,
|
||||
_column_37,
|
||||
_column_38,
|
||||
_column_39,
|
||||
_column_40,
|
||||
_column_41,
|
||||
_column_11,
|
||||
_column_10,
|
||||
_column_42,
|
||||
_column_43,
|
||||
_column_44,
|
||||
_column_45,
|
||||
_column_46,
|
||||
_column_47,
|
||||
_column_48,
|
||||
_column_49,
|
||||
_column_50,
|
||||
_column_51,
|
||||
_column_52,
|
||||
_column_53,
|
||||
_column_54,
|
||||
_column_55,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_36, _column_60],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||
columns: [_column_60, _column_25, _column_61],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape27 remoteAssetCloudIdEntity = Shape27(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_cloud_id_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_36,
|
||||
_column_99,
|
||||
_column_100,
|
||||
_column_96,
|
||||
_column_46,
|
||||
_column_47,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape11 memoryEntity = Shape11(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_18,
|
||||
_column_15,
|
||||
_column_8,
|
||||
_column_62,
|
||||
_column_63,
|
||||
_column_64,
|
||||
_column_65,
|
||||
_column_66,
|
||||
_column_67,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape12 memoryAssetEntity = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||
columns: [_column_36, _column_68],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape14 personEntity = Shape14(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'person_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_15,
|
||||
_column_1,
|
||||
_column_69,
|
||||
_column_71,
|
||||
_column_72,
|
||||
_column_73,
|
||||
_column_74,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape29 assetFaceEntity = Shape29(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_face_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_36,
|
||||
_column_76,
|
||||
_column_77,
|
||||
_column_78,
|
||||
_column_79,
|
||||
_column_80,
|
||||
_column_81,
|
||||
_column_82,
|
||||
_column_83,
|
||||
_column_102,
|
||||
_column_18,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape18 storeEntity = Shape18(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'store_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_87, _column_88, _column_89],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape25 trashedLocalAssetEntity = Shape25(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'trashed_local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_95,
|
||||
_column_22,
|
||||
_column_14,
|
||||
_column_23,
|
||||
_column_97,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape30 assetOcrEntity = Shape30(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_ocr_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_36,
|
||||
_column_103,
|
||||
_column_104,
|
||||
_column_105,
|
||||
_column_106,
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_112,
|
||||
_column_113,
|
||||
_column_102,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||
'idx_partner_shared_with_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||
);
|
||||
final i1.Index idxLatLng = i1.Index(
|
||||
'idx_lat_lng',
|
||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||
);
|
||||
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_remote_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||
'idx_remote_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||
);
|
||||
final i1.Index idxPersonOwnerId = i1.Index(
|
||||
'idx_person_owner_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||
);
|
||||
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||
'idx_asset_face_person_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||
);
|
||||
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||
'idx_asset_face_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||
'idx_trashed_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||
'idx_trashed_local_asset_album',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||
);
|
||||
}
|
||||
|
||||
class Shape30 extends i0.VersionedTable {
|
||||
Shape30({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get assetId =>
|
||||
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<double> get x1 =>
|
||||
columnsByName['x1']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get y1 =>
|
||||
columnsByName['y1']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get x2 =>
|
||||
columnsByName['x2']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get y2 =>
|
||||
columnsByName['y2']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get x3 =>
|
||||
columnsByName['x3']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get y3 =>
|
||||
columnsByName['y3']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get x4 =>
|
||||
columnsByName['x4']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get y4 =>
|
||||
columnsByName['y4']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get boxScore =>
|
||||
columnsByName['box_score']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get textScore =>
|
||||
columnsByName['text_score']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<String> get recognizedText =>
|
||||
columnsByName['text']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get isVisible =>
|
||||
columnsByName['is_visible']! as i1.GeneratedColumn<bool>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<double> _column_103(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'x1',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_104(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'y1',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_105(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'x2',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_106(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'y2',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_107(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'x3',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_108(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'y3',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_109(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'x4',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_110(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'y4',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_111(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'box_score',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_112(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'text_score',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_113(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'text',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
@@ -8924,6 +9566,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
||||
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
||||
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
|
||||
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@@ -9022,6 +9665,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from19To20(migrator, schema);
|
||||
return 20;
|
||||
case 20:
|
||||
final schema = Schema21(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from20To21(migrator, schema);
|
||||
return 21;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@@ -9048,6 +9696,7 @@ i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
||||
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
||||
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
|
||||
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
@@ -9069,5 +9718,6 @@ i1.OnUpgrade stepByStep({
|
||||
from17To18: from17To18,
|
||||
from18To19: from18To19,
|
||||
from19To20: from19To20,
|
||||
from20To21: from20To21,
|
||||
),
|
||||
);
|
||||
|
||||
36
mobile/lib/infrastructure/repositories/ocr.repository.dart
Normal file
36
mobile/lib/infrastructure/repositories/ocr.repository.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
class DriftOcrRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftOcrRepository(this._db) : super(_db);
|
||||
|
||||
Future<List<DriftOcr>?> get(String assetId) async {
|
||||
final query = _db.select(_db.assetOcrEntity)..where((row) => row.assetId.equals(assetId));
|
||||
|
||||
final result = await query.get();
|
||||
return result.map((e) => e.toDto()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension on AssetOcrEntityData {
|
||||
DriftOcr toDto() {
|
||||
return DriftOcr(
|
||||
id: id,
|
||||
assetId: assetId,
|
||||
x1: x1,
|
||||
y1: y1,
|
||||
x2: x2,
|
||||
y2: y2,
|
||||
x3: x3,
|
||||
y3: y3,
|
||||
x4: x4,
|
||||
y4: y4,
|
||||
boxScore: boxScore,
|
||||
textScore: textScore,
|
||||
text: recognizedText,
|
||||
isVisible: isVisible,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ class SyncApiRepository {
|
||||
SyncRequestType.peopleV1,
|
||||
if (serverVersion < const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV1,
|
||||
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV2,
|
||||
SyncRequestType.assetOcrV1,
|
||||
],
|
||||
reset: shouldReset,
|
||||
).toJson(),
|
||||
@@ -195,6 +196,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
|
||||
SyncEntityType.assetFaceV2: SyncAssetFaceV2.fromJson,
|
||||
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
|
||||
SyncEntityType.assetOcrV1: SyncAssetOcrV1.fromJson,
|
||||
SyncEntityType.assetOcrDeleteV1: SyncAssetOcrDeleteV1.fromJson,
|
||||
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||
@@ -58,6 +59,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
await _db.userEntity.deleteAll();
|
||||
await _db.userMetadataEntity.deleteAll();
|
||||
await _db.remoteAssetCloudIdEntity.deleteAll();
|
||||
await _db.assetOcrEntity.deleteAll();
|
||||
});
|
||||
await _db.customStatement('PRAGMA foreign_keys = ON');
|
||||
});
|
||||
@@ -696,6 +698,53 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAssetOcrV1(Iterable<SyncAssetOcrV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final assetOcr in data) {
|
||||
final companion = AssetOcrEntityCompanion(
|
||||
id: Value(assetOcr.id),
|
||||
assetId: Value(assetOcr.assetId),
|
||||
recognizedText: Value(assetOcr.text),
|
||||
x1: Value(assetOcr.x1.toDouble()),
|
||||
y1: Value(assetOcr.y1.toDouble()),
|
||||
x2: Value(assetOcr.x2.toDouble()),
|
||||
y2: Value(assetOcr.y2.toDouble()),
|
||||
x3: Value(assetOcr.x3.toDouble()),
|
||||
y3: Value(assetOcr.y3.toDouble()),
|
||||
x4: Value(assetOcr.x4.toDouble()),
|
||||
y4: Value(assetOcr.y4.toDouble()),
|
||||
boxScore: Value(assetOcr.boxScore.toDouble()),
|
||||
textScore: Value(assetOcr.textScore.toDouble()),
|
||||
isVisible: Value(assetOcr.isVisible),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.assetOcrEntity,
|
||||
companion.copyWith(id: Value(assetOcr.id)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateAssetOcrV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAssetOcrV1(Iterable<SyncAssetOcrDeleteV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final assetOcr in data) {
|
||||
batch.deleteWhere(_db.assetOcrEntity, (row) => row.id.equals(assetOcr.id));
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deleteAssetOcrV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pruneAssets() async {
|
||||
try {
|
||||
await _db.transaction(() async {
|
||||
|
||||
@@ -20,7 +20,6 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||
@@ -50,34 +49,30 @@ import 'package:logging/logging.dart';
|
||||
import 'package:timezone/data/latest.dart';
|
||||
|
||||
void main() async {
|
||||
try {
|
||||
ImmichWidgetsBinding();
|
||||
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
|
||||
await EasyLocalization.ensureInitialized();
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await initApp();
|
||||
// Warm-up isolate pool for worker manager
|
||||
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
||||
await migrateDatabaseIfNeeded(isar, drift);
|
||||
HttpSSLOptions.apply();
|
||||
ImmichWidgetsBinding();
|
||||
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await initApp();
|
||||
// Warm-up isolate pool for worker manager
|
||||
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
||||
await migrateDatabaseIfNeeded(isar, drift);
|
||||
HttpSSLOptions.apply();
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(isar),
|
||||
isarProvider.overrideWithValue(isar),
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
child: const MainWidget(),
|
||||
),
|
||||
);
|
||||
} catch (error, stack) {
|
||||
runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString()));
|
||||
}
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(isar),
|
||||
isarProvider.overrideWithValue(isar),
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
child: const MainWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> initApp() async {
|
||||
await EasyLocalization.ensureInitialized();
|
||||
await initializeDateFormatting();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/colors.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
@@ -20,254 +13,7 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/theme/color_scheme.dart';
|
||||
import 'package:immich_mobile/theme/theme_data.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_logo.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode;
|
||||
|
||||
class BootstrapErrorWidget extends StatelessWidget {
|
||||
final String error;
|
||||
final String stack;
|
||||
|
||||
const BootstrapErrorWidget({super.key, required this.error, required this.stack});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext _) {
|
||||
final immichTheme = defaultColorPreset.themeOfPreset;
|
||||
|
||||
return EasyLocalization(
|
||||
supportedLocales: locales.values.toList(),
|
||||
path: translationsPath,
|
||||
useFallbackTranslations: true,
|
||||
fallbackLocale: locales.values.first,
|
||||
assetLoader: const CodegenLoader(),
|
||||
child: Builder(
|
||||
builder: (lCtx) => MaterialApp(
|
||||
title: 'Immich',
|
||||
debugShowCheckedModeBanner: true,
|
||||
localizationsDelegates: lCtx.localizationDelegates,
|
||||
supportedLocales: lCtx.supportedLocales,
|
||||
locale: lCtx.locale,
|
||||
themeMode: ThemeMode.system,
|
||||
darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: lCtx.locale),
|
||||
theme: getThemeData(colorScheme: immichTheme.light, locale: lCtx.locale),
|
||||
home: Builder(
|
||||
builder: (ctx) => Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
const SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [ImmichLogo(size: 48), SizedBox(width: 12), ImmichTitleText(fontSize: 24)],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: _ErrorCard(error: error, stack: stack),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
const SafeArea(
|
||||
top: false,
|
||||
child: Padding(padding: EdgeInsets.fromLTRB(24, 16, 24, 16), child: _BottomPanel()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomPanel extends StatefulWidget {
|
||||
const _BottomPanel();
|
||||
|
||||
@override
|
||||
State<_BottomPanel> createState() => _BottomPanelState();
|
||||
}
|
||||
|
||||
class _BottomPanelState extends State<_BottomPanel> {
|
||||
bool _cleared = false;
|
||||
|
||||
Future<void> _clearDatabase() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: Text(context.t.reset_sqlite_clear_app_data),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.t.reset_sqlite_confirmation),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
context.t.reset_sqlite_confirmation_note,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(dialogCtx).pop(false), child: Text(context.t.cancel)),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogCtx).pop(true),
|
||||
child: Text(context.t.confirm, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final db = Drift();
|
||||
try {
|
||||
await db.reset();
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _cleared = true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
_cleared ? context.t.reset_sqlite_done : context.t.scaffold_body_error_unrecoverable,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_ActionLink(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
label: context.t.discord,
|
||||
onTap: () => launchUrl(Uri.parse('https://discord.immich.app/'), mode: LaunchMode.externalApplication),
|
||||
),
|
||||
_ActionLink(
|
||||
icon: Icons.bug_report_outlined,
|
||||
label: context.t.profile_drawer_github,
|
||||
onTap: () => launchUrl(
|
||||
Uri.parse('https://github.com/immich-app/immich/issues'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
),
|
||||
if (!_cleared)
|
||||
_ActionLink(
|
||||
icon: Icons.delete_outline,
|
||||
label: context.t.reset_sqlite_clear_app_data,
|
||||
onTap: _clearDatabase,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionLink extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionLink({required this.icon, required this.label, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 24),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorCard extends StatelessWidget {
|
||||
final String error;
|
||||
final String stack;
|
||||
|
||||
const _ErrorCard({required this.error, required this.stack});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ColoredBox(
|
||||
color: scheme.error,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 8, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.t.scaffold_body_error_occurred,
|
||||
style: textTheme.titleSmall?.copyWith(color: scheme.onError),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.t.copy_error,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
icon: Icon(Icons.copy_outlined, size: 16, color: scheme.onError),
|
||||
onPressed: () => Clipboard.setData(ClipboardData(text: '$error\n\n$stack')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(error, style: textTheme.bodyMedium),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.t.stacktrace, style: textTheme.labelMedium),
|
||||
const SizedBox(height: 4),
|
||||
SelectableText(stack, style: textTheme.bodySmall?.copyWith(fontFamily: 'GoogleSansCode')),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class SplashScreenPage extends StatefulHookConsumerWidget {
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_overlay.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
@@ -64,6 +65,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_proxyScrollController.addListener(_onScroll);
|
||||
_eventSubscription = EventStream.shared.listen(_onEvent);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || !_proxyScrollController.hasClients) return;
|
||||
@@ -93,7 +95,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
|
||||
void _showDetails() {
|
||||
if (!_proxyScrollController.hasClients || _snapOffset <= 0) return;
|
||||
_viewer.setShowingDetails(true);
|
||||
_proxyScrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic);
|
||||
}
|
||||
|
||||
@@ -105,7 +106,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
SnapScrollPhysics.target(position, scrollVelocity, _snapOffset) < SnapScrollPhysics.minSnapDistance;
|
||||
}
|
||||
|
||||
void _syncShowingDetails() {
|
||||
void _onScroll() {
|
||||
final offset = _proxyScrollController.offset;
|
||||
if (offset > SnapScrollPhysics.minSnapDistance) {
|
||||
_viewer.setShowingDetails(true);
|
||||
@@ -149,8 +150,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
case _DragIntent.scroll:
|
||||
if (_drag == null) _startProxyDrag();
|
||||
_drag?.update(details);
|
||||
|
||||
_syncShowingDetails();
|
||||
case _DragIntent.dismiss:
|
||||
_handleDragDown(context, details.localPosition - _dragStart!.localPosition);
|
||||
}
|
||||
@@ -169,8 +168,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
case _DragIntent.none:
|
||||
case _DragIntent.scroll:
|
||||
final scrollVelocity = -(details.primaryVelocity ?? 0.0);
|
||||
_viewer.setShowingDetails(!_willClose(scrollVelocity));
|
||||
|
||||
if (_willClose(scrollVelocity)) {
|
||||
_viewer.setShowingDetails(false);
|
||||
}
|
||||
_drag?.end(details);
|
||||
_drag = null;
|
||||
case _DragIntent.dismiss:
|
||||
@@ -307,7 +307,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
if (displayAsset.isImage && !isPlayingMotionVideo) {
|
||||
final size = context.sizeData;
|
||||
return PhotoView(
|
||||
key: Key(displayAsset.heroTag),
|
||||
key: ValueKey(displayAsset.heroTag),
|
||||
index: widget.index,
|
||||
imageProvider: getFullImageProvider(displayAsset, size: size),
|
||||
heroAttributes: heroAttributes,
|
||||
@@ -335,7 +335,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
}
|
||||
|
||||
return PhotoView.customChild(
|
||||
key: Key(displayAsset.heroTag),
|
||||
key: ValueKey(displayAsset),
|
||||
onDragStart: _onDragStart,
|
||||
onDragUpdate: _onDragUpdate,
|
||||
onDragEnd: _onDragEnd,
|
||||
@@ -351,11 +351,12 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
enablePanAlways: true,
|
||||
backgroundDecoration: backgroundDecoration,
|
||||
child: NativeVideoViewer(
|
||||
key: _NativeVideoViewerKey(displayAsset.heroTag),
|
||||
key: ValueKey(displayAsset),
|
||||
asset: displayAsset,
|
||||
scaleStateNotifier: _videoScaleStateNotifier,
|
||||
disableScaleGestures: showingDetails,
|
||||
image: Image(
|
||||
key: ValueKey(displayAsset.heroTag),
|
||||
image: getFullImageProvider(displayAsset, size: context.sizeData),
|
||||
height: context.height,
|
||||
width: context.width,
|
||||
@@ -372,6 +373,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
_showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
||||
final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
||||
final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider);
|
||||
final showingOcr = ref.watch(assetViewerProvider.select((s) => s.showingOcr));
|
||||
|
||||
final asset = ref.read(timelineServiceProvider).getAssetSafe(widget.index);
|
||||
if (asset == null) {
|
||||
@@ -432,6 +434,14 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
backgroundDecoration: BoxDecoration(color: _showingDetails ? Colors.black : Colors.transparent),
|
||||
),
|
||||
),
|
||||
if (showingOcr && !_isZoomed && displayAsset.width != null && displayAsset.height != null)
|
||||
Positioned.fill(
|
||||
child: OcrOverlay(
|
||||
asset: displayAsset,
|
||||
imageSize: Size(displayAsset.width!.toDouble(), displayAsset.height!.toDouble()),
|
||||
viewportSize: Size(viewportWidth, viewportHeight),
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: !_showingDetails,
|
||||
child: Column(
|
||||
@@ -459,25 +469,3 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A global key is used for video viewers to prevent them from being
|
||||
// unnecessarily recreated. They're quite expensive, and maintain internal
|
||||
// state. This can cause videos to restart multiple times during normal usage,
|
||||
// like a hero animation.
|
||||
//
|
||||
// A plain ValueKey is insufficient, as it does not allow widgets to reparent. A
|
||||
// GlobalObjectKey is fragile, as it checks if the given objects are identical,
|
||||
// rather than equal. Hero tags are created with string interpolation, which
|
||||
// prevents Dart from interning them. As such, hero tags are not identical, even
|
||||
// if they are equal.
|
||||
class _NativeVideoViewerKey extends GlobalKey {
|
||||
final String value;
|
||||
|
||||
const _NativeVideoViewerKey(this.value) : super.constructor();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is _NativeVideoViewerKey && other.value == value;
|
||||
|
||||
@override
|
||||
int get hashCode => value.hashCode;
|
||||
}
|
||||
|
||||
@@ -117,9 +117,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
_reloadSubscription = EventStream.shared.listen(_onEvent);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
|
||||
|
||||
final assetViewer = ref.read(assetViewerProvider);
|
||||
_setSystemUIMode(assetViewer.showingControls, assetViewer.showingDetails);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -229,13 +226,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
_onAssetChanged(index);
|
||||
}
|
||||
|
||||
void _setSystemUIMode(bool controls, bool details) {
|
||||
final mode = !controls || (CurrentPlatform.isIOS && details)
|
||||
? SystemUiMode.immersiveSticky
|
||||
: SystemUiMode.edgeToEdge;
|
||||
unawaited(SystemChrome.setEnabledSystemUIMode(mode));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||
@@ -255,7 +245,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
|
||||
ref.listen(assetViewerProvider.select((value) => (value.showingControls, value.showingDetails)), (_, state) {
|
||||
final (controls, details) = state;
|
||||
_setSystemUIMode(controls, details);
|
||||
final mode = !controls || (CurrentPlatform.isIOS && details)
|
||||
? SystemUiMode.immersiveSticky
|
||||
: SystemUiMode.edgeToEdge;
|
||||
unawaited(SystemChrome.setEnabledSystemUIMode(mode));
|
||||
});
|
||||
|
||||
return PopScope(
|
||||
|
||||
@@ -7,6 +7,7 @@ class AssetViewerState {
|
||||
final bool showingDetails;
|
||||
final bool showingControls;
|
||||
final bool isZoomed;
|
||||
final bool showingOcr;
|
||||
final BaseAsset? currentAsset;
|
||||
final int stackIndex;
|
||||
|
||||
@@ -15,6 +16,7 @@ class AssetViewerState {
|
||||
this.showingDetails = false,
|
||||
this.showingControls = true,
|
||||
this.isZoomed = false,
|
||||
this.showingOcr = false,
|
||||
this.currentAsset,
|
||||
this.stackIndex = 0,
|
||||
});
|
||||
@@ -24,6 +26,7 @@ class AssetViewerState {
|
||||
bool? showingDetails,
|
||||
bool? showingControls,
|
||||
bool? isZoomed,
|
||||
bool? showingOcr,
|
||||
BaseAsset? currentAsset,
|
||||
int? stackIndex,
|
||||
}) {
|
||||
@@ -32,6 +35,7 @@ class AssetViewerState {
|
||||
showingDetails: showingDetails ?? this.showingDetails,
|
||||
showingControls: showingControls ?? this.showingControls,
|
||||
isZoomed: isZoomed ?? this.isZoomed,
|
||||
showingOcr: showingOcr ?? this.showingOcr,
|
||||
currentAsset: currentAsset ?? this.currentAsset,
|
||||
stackIndex: stackIndex ?? this.stackIndex,
|
||||
);
|
||||
@@ -39,7 +43,7 @@ class AssetViewerState {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AssetViewerState(opacity: $backgroundOpacity, showingDetails: $showingDetails, controls: $showingControls, isZoomed: $isZoomed)';
|
||||
return 'AssetViewerState(opacity: $backgroundOpacity, showingDetails: $showingDetails, controls: $showingControls, isZoomed: $isZoomed, showingOcr: $showingOcr)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -51,6 +55,7 @@ class AssetViewerState {
|
||||
other.showingDetails == showingDetails &&
|
||||
other.showingControls == showingControls &&
|
||||
other.isZoomed == isZoomed &&
|
||||
other.showingOcr == showingOcr &&
|
||||
other.currentAsset == currentAsset &&
|
||||
other.stackIndex == stackIndex;
|
||||
}
|
||||
@@ -61,6 +66,7 @@ class AssetViewerState {
|
||||
showingDetails.hashCode ^
|
||||
showingControls.hashCode ^
|
||||
isZoomed.hashCode ^
|
||||
showingOcr.hashCode ^
|
||||
currentAsset.hashCode ^
|
||||
stackIndex.hashCode;
|
||||
}
|
||||
@@ -123,6 +129,10 @@ class AssetViewerStateNotifier extends Notifier<AssetViewerState> {
|
||||
}
|
||||
state = state.copyWith(stackIndex: index);
|
||||
}
|
||||
|
||||
void toggleOcr() {
|
||||
state = state.copyWith(showingOcr: !state.showingOcr);
|
||||
}
|
||||
}
|
||||
|
||||
final assetViewerProvider = NotifierProvider<AssetViewerStateNotifier, AssetViewerState>(AssetViewerStateNotifier.new);
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/ocr.provider.dart';
|
||||
|
||||
class OcrOverlay extends ConsumerStatefulWidget {
|
||||
final BaseAsset asset;
|
||||
final Size imageSize;
|
||||
final Size viewportSize;
|
||||
|
||||
const OcrOverlay({super.key, required this.asset, required this.imageSize, required this.viewportSize});
|
||||
|
||||
@override
|
||||
ConsumerState<OcrOverlay> createState() => _OcrOverlayState();
|
||||
}
|
||||
|
||||
class _OcrOverlayState extends ConsumerState<OcrOverlay> {
|
||||
int? _selectedBoxIndex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.asset is! RemoteAsset) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final ocrData = ref.watch(driftOcrAssetProvider((widget.asset as RemoteAsset).id));
|
||||
|
||||
return ocrData.when(
|
||||
data: (data) {
|
||||
if (data == null || data.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return _buildOcrBoxes(data);
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOcrBoxes(List<DriftOcr> ocrData) {
|
||||
// Calculate the scale factor to fit the image in the viewport
|
||||
final imageWidth = widget.imageSize.width;
|
||||
final imageHeight = widget.imageSize.height;
|
||||
final viewportWidth = widget.viewportSize.width;
|
||||
final viewportHeight = widget.viewportSize.height;
|
||||
|
||||
// Calculate how the image is scaled to fit in the viewport
|
||||
final scaleX = viewportWidth / imageWidth;
|
||||
final scaleY = viewportHeight / imageHeight;
|
||||
final scale = scaleX < scaleY ? scaleX : scaleY;
|
||||
|
||||
// Calculate the actual displayed image size
|
||||
final displayedWidth = imageWidth * scale;
|
||||
final displayedHeight = imageHeight * scale;
|
||||
|
||||
// Calculate the offset to center the image
|
||||
final offsetX = (viewportWidth - displayedWidth) / 2;
|
||||
final offsetY = (viewportHeight - displayedHeight) / 2;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedBoxIndex = null;
|
||||
});
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
// Invisible layer to catch taps outside of boxes
|
||||
SizedBox(width: viewportWidth, height: viewportHeight),
|
||||
...ocrData.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final ocr = entry.value;
|
||||
final isSelected = _selectedBoxIndex == index;
|
||||
|
||||
// Normalize coordinates (0-1 range) and scale to displayed image size
|
||||
final x1 = ocr.x1 * displayedWidth + offsetX;
|
||||
final y1 = ocr.y1 * displayedHeight + offsetY;
|
||||
final x2 = ocr.x2 * displayedWidth + offsetX;
|
||||
final y2 = ocr.y2 * displayedHeight + offsetY;
|
||||
final x3 = ocr.x3 * displayedWidth + offsetX;
|
||||
final y3 = ocr.y3 * displayedHeight + offsetY;
|
||||
final x4 = ocr.x4 * displayedWidth + offsetX;
|
||||
final y4 = ocr.y4 * displayedHeight + offsetY;
|
||||
|
||||
// Calculate bounding rectangle for hit testing
|
||||
final minX = [x1, x2, x3, x4].reduce((a, b) => a < b ? a : b);
|
||||
final maxX = [x1, x2, x3, x4].reduce((a, b) => a > b ? a : b);
|
||||
final minY = [y1, y2, y3, y4].reduce((a, b) => a < b ? a : b);
|
||||
final maxY = [y1, y2, y3, y4].reduce((a, b) => a > b ? a : b);
|
||||
|
||||
// Calculate rotation angle from the bottom edge (x1,y1) to (x2,y2)
|
||||
final angle = math.atan2(y2 - y1, x2 - x1);
|
||||
final centerX = (minX + maxX) / 2;
|
||||
final centerY = (minY + maxY) / 2;
|
||||
|
||||
return Positioned(
|
||||
left: minX,
|
||||
top: minY,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedBoxIndex = isSelected ? null : index;
|
||||
});
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: SizedBox(
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomPaint(
|
||||
painter: _OcrBoxPainter(
|
||||
points: [
|
||||
Offset(x1 - minX, y1 - minY),
|
||||
Offset(x2 - minX, y2 - minY),
|
||||
Offset(x3 - minX, y3 - minY),
|
||||
Offset(x4 - minX, y4 - minY),
|
||||
],
|
||||
isSelected: isSelected,
|
||||
context: context,
|
||||
),
|
||||
size: Size(maxX - minX, maxY - minY),
|
||||
),
|
||||
if (isSelected)
|
||||
Positioned(
|
||||
left: centerX - minX,
|
||||
top: centerY - minY,
|
||||
child: FractionalTranslation(
|
||||
translation: const Offset(-0.5, -0.5),
|
||||
child: Transform.rotate(
|
||||
angle: angle,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800]?.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: math.max(50, maxX - minX),
|
||||
maxHeight: math.max(20, maxY - minY),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: SelectableText(
|
||||
ocr.text,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: math.max(12, (maxY - minY) * 0.6),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OcrBoxPainter extends CustomPainter {
|
||||
final List<Offset> points;
|
||||
final bool isSelected;
|
||||
final BuildContext context;
|
||||
|
||||
_OcrBoxPainter({required this.points, required this.isSelected, required this.context});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = isSelected ? context.primaryColor : Colors.green
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0;
|
||||
|
||||
final fillPaint = Paint()
|
||||
..color = (isSelected ? context.primaryColor : Colors.green).withValues(alpha: 0.1)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final path = Path()
|
||||
..moveTo(points[0].dx, points[0].dy)
|
||||
..lineTo(points[1].dx, points[1].dy)
|
||||
..lineTo(points[2].dx, points[2].dy)
|
||||
..lineTo(points[3].dx, points[3].dy)
|
||||
..close();
|
||||
|
||||
canvas.drawPath(path, fillPaint);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_OcrBoxPainter oldDelegate) {
|
||||
return oldDelegate.isSelected != isSelected || oldDelegate.points != points;
|
||||
}
|
||||
}
|
||||
@@ -420,18 +420,20 @@ class NativeVideoViewer extends HookConsumerWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
// Hide thumbnail once video is visible to avoid it showing in background when zooming out on video.
|
||||
if (!isVisible.value || controller.value == null) Center(child: image),
|
||||
if (!isVisible.value || controller.value == null) Center(key: ValueKey(asset.heroTag), child: image),
|
||||
if (aspectRatio.value != null && !isCasting && isCurrent)
|
||||
Visibility.maintain(
|
||||
key: ValueKey(asset),
|
||||
visible: isVisible.value,
|
||||
child: PhotoView.customChild(
|
||||
key: ValueKey(asset),
|
||||
enableRotation: false,
|
||||
disableScaleGestures: disableScaleGestures,
|
||||
// Transparent to avoid a black flash when viewer becomes visible but video isn't loaded yet.
|
||||
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
|
||||
scaleStateChangedCallback: (state) => scaleStateNotifier?.value = state,
|
||||
childSize: videoContextSize(aspectRatio.value, context),
|
||||
child: NativeVideoPlayerView(onViewReady: initController),
|
||||
child: NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController),
|
||||
),
|
||||
),
|
||||
if (showControls) const Center(child: VideoViewerControls()),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/ocr.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||
@@ -33,6 +34,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
final hasOcr = asset is RemoteAsset && ref.watch(driftOcrAssetProvider(asset.id)).valueOrNull?.isNotEmpty == true;
|
||||
|
||||
final showingDetails = ref.watch(assetViewerProvider.select((state) => state.showingDetails));
|
||||
double opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||
@@ -47,8 +49,15 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
}
|
||||
|
||||
final originalTheme = context.themeData;
|
||||
final showingOcr = ref.watch(assetViewerProvider.select((state) => state.showingOcr));
|
||||
|
||||
final actions = <Widget>[
|
||||
if (hasOcr)
|
||||
IconButton(
|
||||
icon: Icon(showingOcr ? Icons.text_fields : Icons.text_fields_outlined),
|
||||
onPressed: () => ref.read(assetViewerProvider.notifier).toggleOcr(),
|
||||
color: showingOcr ? context.primaryColor : null,
|
||||
),
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||
if (album != null && album.isActivityEnabled && album.isShared)
|
||||
IconButton(
|
||||
|
||||
16
mobile/lib/providers/infrastructure/ocr.provider.dart
Normal file
16
mobile/lib/providers/infrastructure/ocr.provider.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/domain/services/ocr.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/ocr.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
final driftOcrRepositoryProvider = Provider<DriftOcrRepository>((ref) => DriftOcrRepository(ref.watch(driftProvider)));
|
||||
|
||||
final driftOcrServiceProvider = Provider<DriftOcrService>(
|
||||
(ref) => DriftOcrService(ref.watch(driftOcrRepositoryProvider)),
|
||||
);
|
||||
|
||||
final driftOcrAssetProvider = FutureProvider.family<List<DriftOcr>?, String>((ref, assetId) async {
|
||||
final service = ref.watch(driftOcrServiceProvider);
|
||||
return service.get(assetId);
|
||||
});
|
||||
@@ -62,7 +62,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
||||
pinned: pinned,
|
||||
snap: snap,
|
||||
expandedHeight: expandedHeight,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(5))),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
title: title ?? const _ImmichLogoWithText(),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -6,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
@@ -89,27 +87,25 @@ class SyncStatusAndActions extends HookConsumerWidget {
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(context.t.reset_sqlite),
|
||||
content: Text(context.t.reset_sqlite_confirmation),
|
||||
title: Text("reset_sqlite".t(context: context)),
|
||||
content: Text("reset_sqlite_confirmation".t(context: context)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel)),
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text("cancel".t(context: context)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref.read(driftProvider).reset();
|
||||
context.pop();
|
||||
unawaited(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(context.t.reset_sqlite_success),
|
||||
content: Text(context.t.reset_sqlite_done),
|
||||
actions: [TextButton(onPressed: () => ctx.pop(), child: Text(context.t.ok))],
|
||||
),
|
||||
),
|
||||
context.scaffoldMessenger.showSnackBar(
|
||||
SnackBar(content: Text("reset_sqlite_success".t(context: context))),
|
||||
);
|
||||
},
|
||||
child: Text(context.t.confirm, style: TextStyle(color: context.colorScheme.error)),
|
||||
child: Text(
|
||||
"confirm".t(context: context),
|
||||
style: TextStyle(color: context.colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -584,6 +584,8 @@ Class | Method | HTTP request | Description
|
||||
- [SyncAssetFaceV2](doc//SyncAssetFaceV2.md)
|
||||
- [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md)
|
||||
- [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md)
|
||||
- [SyncAssetOcrDeleteV1](doc//SyncAssetOcrDeleteV1.md)
|
||||
- [SyncAssetOcrV1](doc//SyncAssetOcrV1.md)
|
||||
- [SyncAssetV1](doc//SyncAssetV1.md)
|
||||
- [SyncAuthUserV1](doc//SyncAuthUserV1.md)
|
||||
- [SyncEntityType](doc//SyncEntityType.md)
|
||||
|
||||
2
mobile/openapi/lib/api.dart
generated
2
mobile/openapi/lib/api.dart
generated
@@ -323,6 +323,8 @@ part 'model/sync_asset_face_v1.dart';
|
||||
part 'model/sync_asset_face_v2.dart';
|
||||
part 'model/sync_asset_metadata_delete_v1.dart';
|
||||
part 'model/sync_asset_metadata_v1.dart';
|
||||
part 'model/sync_asset_ocr_delete_v1.dart';
|
||||
part 'model/sync_asset_ocr_v1.dart';
|
||||
part 'model/sync_asset_v1.dart';
|
||||
part 'model/sync_auth_user_v1.dart';
|
||||
part 'model/sync_entity_type.dart';
|
||||
|
||||
4
mobile/openapi/lib/api/search_api.dart
generated
4
mobile/openapi/lib/api/search_api.dart
generated
@@ -410,7 +410,7 @@ class SearchApi {
|
||||
/// Filter by person IDs
|
||||
///
|
||||
/// * [num] rating:
|
||||
/// Filter by rating [1-5], or null for unrated
|
||||
/// Filter by rating
|
||||
///
|
||||
/// * [num] size:
|
||||
/// Number of results to return
|
||||
@@ -633,7 +633,7 @@ class SearchApi {
|
||||
/// Filter by person IDs
|
||||
///
|
||||
/// * [num] rating:
|
||||
/// Filter by rating [1-5], or null for unrated
|
||||
/// Filter by rating
|
||||
///
|
||||
/// * [num] size:
|
||||
/// Number of results to return
|
||||
|
||||
30
mobile/openapi/lib/api/timeline_api.dart
generated
30
mobile/openapi/lib/api/timeline_api.dart
generated
@@ -30,9 +30,6 @@ class TimelineApi {
|
||||
/// * [String] albumId:
|
||||
/// Filter assets belonging to a specific album
|
||||
///
|
||||
/// * [String] bbox:
|
||||
/// Bounding box coordinates as west,south,east,north (WGS84)
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status (true for favorites only, false for non-favorites only)
|
||||
///
|
||||
@@ -66,7 +63,7 @@ class TimelineApi {
|
||||
///
|
||||
/// * [bool] withStacked:
|
||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/timeline/bucket';
|
||||
|
||||
@@ -80,9 +77,6 @@ class TimelineApi {
|
||||
if (albumId != null) {
|
||||
queryParams.addAll(_queryParams('', 'albumId', albumId));
|
||||
}
|
||||
if (bbox != null) {
|
||||
queryParams.addAll(_queryParams('', 'bbox', bbox));
|
||||
}
|
||||
if (isFavorite != null) {
|
||||
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
||||
}
|
||||
@@ -147,9 +141,6 @@ class TimelineApi {
|
||||
/// * [String] albumId:
|
||||
/// Filter assets belonging to a specific album
|
||||
///
|
||||
/// * [String] bbox:
|
||||
/// Bounding box coordinates as west,south,east,north (WGS84)
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status (true for favorites only, false for non-favorites only)
|
||||
///
|
||||
@@ -183,8 +174,8 @@ class TimelineApi {
|
||||
///
|
||||
/// * [bool] withStacked:
|
||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
|
||||
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@@ -209,9 +200,6 @@ class TimelineApi {
|
||||
/// * [String] albumId:
|
||||
/// Filter assets belonging to a specific album
|
||||
///
|
||||
/// * [String] bbox:
|
||||
/// Bounding box coordinates as west,south,east,north (WGS84)
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status (true for favorites only, false for non-favorites only)
|
||||
///
|
||||
@@ -245,7 +233,7 @@ class TimelineApi {
|
||||
///
|
||||
/// * [bool] withStacked:
|
||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/timeline/buckets';
|
||||
|
||||
@@ -259,9 +247,6 @@ class TimelineApi {
|
||||
if (albumId != null) {
|
||||
queryParams.addAll(_queryParams('', 'albumId', albumId));
|
||||
}
|
||||
if (bbox != null) {
|
||||
queryParams.addAll(_queryParams('', 'bbox', bbox));
|
||||
}
|
||||
if (isFavorite != null) {
|
||||
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
||||
}
|
||||
@@ -322,9 +307,6 @@ class TimelineApi {
|
||||
/// * [String] albumId:
|
||||
/// Filter assets belonging to a specific album
|
||||
///
|
||||
/// * [String] bbox:
|
||||
/// Bounding box coordinates as west,south,east,north (WGS84)
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status (true for favorites only, false for non-favorites only)
|
||||
///
|
||||
@@ -358,8 +340,8 @@ class TimelineApi {
|
||||
///
|
||||
/// * [bool] withStacked:
|
||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
|
||||
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
@@ -692,6 +692,10 @@ class ApiClient {
|
||||
return SyncAssetMetadataDeleteV1.fromJson(value);
|
||||
case 'SyncAssetMetadataV1':
|
||||
return SyncAssetMetadataV1.fromJson(value);
|
||||
case 'SyncAssetOcrDeleteV1':
|
||||
return SyncAssetOcrDeleteV1.fromJson(value);
|
||||
case 'SyncAssetOcrV1':
|
||||
return SyncAssetOcrV1.fromJson(value);
|
||||
case 'SyncAssetV1':
|
||||
return SyncAssetV1.fromJson(value);
|
||||
case 'SyncAuthUserV1':
|
||||
|
||||
12
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
12
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@@ -86,10 +86,16 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
num? longitude;
|
||||
|
||||
/// Rating in range [1-5], or null for unrated
|
||||
/// Rating
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// Time zone (IANA timezone)
|
||||
@@ -217,9 +223,7 @@ class AssetBulkUpdateDto {
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
rating: json[r'rating'] == null
|
||||
? null
|
||||
: num.parse('${json[r'rating']}'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
||||
);
|
||||
|
||||
12
mobile/openapi/lib/model/metadata_search_dto.dart
generated
12
mobile/openapi/lib/model/metadata_search_dto.dart
generated
@@ -256,10 +256,16 @@ class MetadataSearchDto {
|
||||
///
|
||||
String? previewPath;
|
||||
|
||||
/// Filter by rating [1-5], or null for unrated
|
||||
/// Filter by rating
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// Number of results to return
|
||||
@@ -748,9 +754,7 @@ class MetadataSearchDto {
|
||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
previewPath: mapValueOfType<String>(json, r'previewPath'),
|
||||
rating: json[r'rating'] == null
|
||||
? null
|
||||
: num.parse('${json[r'rating']}'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
size: num.parse('${json[r'size']}'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
tagIds: json[r'tagIds'] is Iterable
|
||||
|
||||
12
mobile/openapi/lib/model/random_search_dto.dart
generated
12
mobile/openapi/lib/model/random_search_dto.dart
generated
@@ -159,10 +159,16 @@ class RandomSearchDto {
|
||||
/// Filter by person IDs
|
||||
List<String> personIds;
|
||||
|
||||
/// Filter by rating [1-5], or null for unrated
|
||||
/// Filter by rating
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// Number of results to return
|
||||
@@ -559,9 +565,7 @@ class RandomSearchDto {
|
||||
personIds: json[r'personIds'] is Iterable
|
||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
rating: json[r'rating'] == null
|
||||
? null
|
||||
: num.parse('${json[r'rating']}'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
size: num.parse('${json[r'size']}'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
tagIds: json[r'tagIds'] is Iterable
|
||||
|
||||
12
mobile/openapi/lib/model/smart_search_dto.dart
generated
12
mobile/openapi/lib/model/smart_search_dto.dart
generated
@@ -199,10 +199,16 @@ class SmartSearchDto {
|
||||
///
|
||||
String? queryAssetId;
|
||||
|
||||
/// Filter by rating [1-5], or null for unrated
|
||||
/// Filter by rating
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// Number of results to return
|
||||
@@ -599,9 +605,7 @@ class SmartSearchDto {
|
||||
: const [],
|
||||
query: mapValueOfType<String>(json, r'query'),
|
||||
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
|
||||
rating: json[r'rating'] == null
|
||||
? null
|
||||
: num.parse('${json[r'rating']}'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
size: num.parse('${json[r'size']}'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
tagIds: json[r'tagIds'] is Iterable
|
||||
|
||||
12
mobile/openapi/lib/model/statistics_search_dto.dart
generated
12
mobile/openapi/lib/model/statistics_search_dto.dart
generated
@@ -164,10 +164,16 @@ class StatisticsSearchDto {
|
||||
/// Filter by person IDs
|
||||
List<String> personIds;
|
||||
|
||||
/// Filter by rating [1-5], or null for unrated
|
||||
/// Filter by rating
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// Filter by state/province name
|
||||
@@ -489,9 +495,7 @@ class StatisticsSearchDto {
|
||||
personIds: json[r'personIds'] is Iterable
|
||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
rating: json[r'rating'] == null
|
||||
? null
|
||||
: num.parse('${json[r'rating']}'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
tagIds: json[r'tagIds'] is Iterable
|
||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
|
||||
118
mobile/openapi/lib/model/sync_asset_ocr_delete_v1.dart
generated
Normal file
118
mobile/openapi/lib/model/sync_asset_ocr_delete_v1.dart
generated
Normal file
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// 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 SyncAssetOcrDeleteV1 {
|
||||
/// Returns a new [SyncAssetOcrDeleteV1] instance.
|
||||
SyncAssetOcrDeleteV1({
|
||||
required this.assetId,
|
||||
required this.deletedAt,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
/// Original asset ID of the deleted OCR entry
|
||||
String assetId;
|
||||
|
||||
/// Timestamp when the OCR entry was deleted
|
||||
DateTime deletedAt;
|
||||
|
||||
/// Audit row ID of the deleted OCR entry
|
||||
String id;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAssetOcrDeleteV1 &&
|
||||
other.assetId == assetId &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.id == id;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetId.hashCode) +
|
||||
(deletedAt.hashCode) +
|
||||
(id.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAssetOcrDeleteV1[assetId=$assetId, deletedAt=$deletedAt, id=$id]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetId'] = this.assetId;
|
||||
json[r'deletedAt'] = this.deletedAt.toUtc().toIso8601String();
|
||||
json[r'id'] = this.id;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SyncAssetOcrDeleteV1] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SyncAssetOcrDeleteV1? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SyncAssetOcrDeleteV1");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncAssetOcrDeleteV1(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
deletedAt: mapDateTime(json, r'deletedAt', r'')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SyncAssetOcrDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAssetOcrDeleteV1>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAssetOcrDeleteV1.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SyncAssetOcrDeleteV1> mapFromJson(dynamic json) {
|
||||
final map = <String, SyncAssetOcrDeleteV1>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SyncAssetOcrDeleteV1.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SyncAssetOcrDeleteV1-objects as value to a dart map
|
||||
static Map<String, List<SyncAssetOcrDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SyncAssetOcrDeleteV1>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SyncAssetOcrDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetId',
|
||||
'deletedAt',
|
||||
'id',
|
||||
};
|
||||
}
|
||||
|
||||
217
mobile/openapi/lib/model/sync_asset_ocr_v1.dart
generated
Normal file
217
mobile/openapi/lib/model/sync_asset_ocr_v1.dart
generated
Normal file
@@ -0,0 +1,217 @@
|
||||
//
|
||||
// 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 SyncAssetOcrV1 {
|
||||
/// Returns a new [SyncAssetOcrV1] instance.
|
||||
SyncAssetOcrV1({
|
||||
required this.assetId,
|
||||
required this.boxScore,
|
||||
required this.id,
|
||||
required this.isVisible,
|
||||
required this.text,
|
||||
required this.textScore,
|
||||
required this.x1,
|
||||
required this.x2,
|
||||
required this.x3,
|
||||
required this.x4,
|
||||
required this.y1,
|
||||
required this.y2,
|
||||
required this.y3,
|
||||
required this.y4,
|
||||
});
|
||||
|
||||
/// Asset ID
|
||||
String assetId;
|
||||
|
||||
/// Confidence score of the bounding box
|
||||
num boxScore;
|
||||
|
||||
/// OCR entry ID
|
||||
String id;
|
||||
|
||||
/// Whether the OCR entry is visible
|
||||
bool isVisible;
|
||||
|
||||
/// Recognized text content
|
||||
String text;
|
||||
|
||||
/// Confidence score of the recognized text
|
||||
num textScore;
|
||||
|
||||
/// Top-left X coordinate (normalized 0–1)
|
||||
num x1;
|
||||
|
||||
/// Top-right X coordinate (normalized 0–1)
|
||||
num x2;
|
||||
|
||||
/// Bottom-right X coordinate (normalized 0–1)
|
||||
num x3;
|
||||
|
||||
/// Bottom-left X coordinate (normalized 0–1)
|
||||
num x4;
|
||||
|
||||
/// Top-left Y coordinate (normalized 0–1)
|
||||
num y1;
|
||||
|
||||
/// Top-right Y coordinate (normalized 0–1)
|
||||
num y2;
|
||||
|
||||
/// Bottom-right Y coordinate (normalized 0–1)
|
||||
num y3;
|
||||
|
||||
/// Bottom-left Y coordinate (normalized 0–1)
|
||||
num y4;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAssetOcrV1 &&
|
||||
other.assetId == assetId &&
|
||||
other.boxScore == boxScore &&
|
||||
other.id == id &&
|
||||
other.isVisible == isVisible &&
|
||||
other.text == text &&
|
||||
other.textScore == textScore &&
|
||||
other.x1 == x1 &&
|
||||
other.x2 == x2 &&
|
||||
other.x3 == x3 &&
|
||||
other.x4 == x4 &&
|
||||
other.y1 == y1 &&
|
||||
other.y2 == y2 &&
|
||||
other.y3 == y3 &&
|
||||
other.y4 == y4;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetId.hashCode) +
|
||||
(boxScore.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isVisible.hashCode) +
|
||||
(text.hashCode) +
|
||||
(textScore.hashCode) +
|
||||
(x1.hashCode) +
|
||||
(x2.hashCode) +
|
||||
(x3.hashCode) +
|
||||
(x4.hashCode) +
|
||||
(y1.hashCode) +
|
||||
(y2.hashCode) +
|
||||
(y3.hashCode) +
|
||||
(y4.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAssetOcrV1[assetId=$assetId, boxScore=$boxScore, id=$id, isVisible=$isVisible, text=$text, textScore=$textScore, x1=$x1, x2=$x2, x3=$x3, x4=$x4, y1=$y1, y2=$y2, y3=$y3, y4=$y4]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetId'] = this.assetId;
|
||||
json[r'boxScore'] = this.boxScore;
|
||||
json[r'id'] = this.id;
|
||||
json[r'isVisible'] = this.isVisible;
|
||||
json[r'text'] = this.text;
|
||||
json[r'textScore'] = this.textScore;
|
||||
json[r'x1'] = this.x1;
|
||||
json[r'x2'] = this.x2;
|
||||
json[r'x3'] = this.x3;
|
||||
json[r'x4'] = this.x4;
|
||||
json[r'y1'] = this.y1;
|
||||
json[r'y2'] = this.y2;
|
||||
json[r'y3'] = this.y3;
|
||||
json[r'y4'] = this.y4;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SyncAssetOcrV1] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SyncAssetOcrV1? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SyncAssetOcrV1");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncAssetOcrV1(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
boxScore: num.parse('${json[r'boxScore']}'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isVisible: mapValueOfType<bool>(json, r'isVisible')!,
|
||||
text: mapValueOfType<String>(json, r'text')!,
|
||||
textScore: num.parse('${json[r'textScore']}'),
|
||||
x1: num.parse('${json[r'x1']}'),
|
||||
x2: num.parse('${json[r'x2']}'),
|
||||
x3: num.parse('${json[r'x3']}'),
|
||||
x4: num.parse('${json[r'x4']}'),
|
||||
y1: num.parse('${json[r'y1']}'),
|
||||
y2: num.parse('${json[r'y2']}'),
|
||||
y3: num.parse('${json[r'y3']}'),
|
||||
y4: num.parse('${json[r'y4']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SyncAssetOcrV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAssetOcrV1>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAssetOcrV1.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SyncAssetOcrV1> mapFromJson(dynamic json) {
|
||||
final map = <String, SyncAssetOcrV1>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SyncAssetOcrV1.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SyncAssetOcrV1-objects as value to a dart map
|
||||
static Map<String, List<SyncAssetOcrV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SyncAssetOcrV1>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SyncAssetOcrV1.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetId',
|
||||
'boxScore',
|
||||
'id',
|
||||
'isVisible',
|
||||
'text',
|
||||
'textScore',
|
||||
'x1',
|
||||
'x2',
|
||||
'x3',
|
||||
'x4',
|
||||
'y1',
|
||||
'y2',
|
||||
'y3',
|
||||
'y4',
|
||||
};
|
||||
}
|
||||
|
||||
6
mobile/openapi/lib/model/sync_entity_type.dart
generated
6
mobile/openapi/lib/model/sync_entity_type.dart
generated
@@ -33,6 +33,8 @@ class SyncEntityType {
|
||||
static const assetEditDeleteV1 = SyncEntityType._(r'AssetEditDeleteV1');
|
||||
static const assetMetadataV1 = SyncEntityType._(r'AssetMetadataV1');
|
||||
static const assetMetadataDeleteV1 = SyncEntityType._(r'AssetMetadataDeleteV1');
|
||||
static const assetOcrV1 = SyncEntityType._(r'AssetOcrV1');
|
||||
static const assetOcrDeleteV1 = SyncEntityType._(r'AssetOcrDeleteV1');
|
||||
static const partnerV1 = SyncEntityType._(r'PartnerV1');
|
||||
static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1');
|
||||
static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1');
|
||||
@@ -86,6 +88,8 @@ class SyncEntityType {
|
||||
assetEditDeleteV1,
|
||||
assetMetadataV1,
|
||||
assetMetadataDeleteV1,
|
||||
assetOcrV1,
|
||||
assetOcrDeleteV1,
|
||||
partnerV1,
|
||||
partnerDeleteV1,
|
||||
partnerAssetV1,
|
||||
@@ -174,6 +178,8 @@ class SyncEntityTypeTypeTransformer {
|
||||
case r'AssetEditDeleteV1': return SyncEntityType.assetEditDeleteV1;
|
||||
case r'AssetMetadataV1': return SyncEntityType.assetMetadataV1;
|
||||
case r'AssetMetadataDeleteV1': return SyncEntityType.assetMetadataDeleteV1;
|
||||
case r'AssetOcrV1': return SyncEntityType.assetOcrV1;
|
||||
case r'AssetOcrDeleteV1': return SyncEntityType.assetOcrDeleteV1;
|
||||
case r'PartnerV1': return SyncEntityType.partnerV1;
|
||||
case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1;
|
||||
case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1;
|
||||
|
||||
3
mobile/openapi/lib/model/sync_request_type.dart
generated
3
mobile/openapi/lib/model/sync_request_type.dart
generated
@@ -32,6 +32,7 @@ class SyncRequestType {
|
||||
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
|
||||
static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1');
|
||||
static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1');
|
||||
static const assetOcrV1 = SyncRequestType._(r'AssetOcrV1');
|
||||
static const authUsersV1 = SyncRequestType._(r'AuthUsersV1');
|
||||
static const memoriesV1 = SyncRequestType._(r'MemoriesV1');
|
||||
static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1');
|
||||
@@ -57,6 +58,7 @@ class SyncRequestType {
|
||||
assetExifsV1,
|
||||
assetEditsV1,
|
||||
assetMetadataV1,
|
||||
assetOcrV1,
|
||||
authUsersV1,
|
||||
memoriesV1,
|
||||
memoryToAssetsV1,
|
||||
@@ -117,6 +119,7 @@ class SyncRequestTypeTypeTransformer {
|
||||
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
|
||||
case r'AssetEditsV1': return SyncRequestType.assetEditsV1;
|
||||
case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1;
|
||||
case r'AssetOcrV1': return SyncRequestType.assetOcrV1;
|
||||
case r'AuthUsersV1': return SyncRequestType.authUsersV1;
|
||||
case r'MemoriesV1': return SyncRequestType.memoriesV1;
|
||||
case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1;
|
||||
|
||||
12
mobile/openapi/lib/model/update_asset_dto.dart
generated
12
mobile/openapi/lib/model/update_asset_dto.dart
generated
@@ -71,10 +71,16 @@ class UpdateAssetDto {
|
||||
///
|
||||
num? longitude;
|
||||
|
||||
/// Rating in range [1-5], or null for unrated
|
||||
/// Rating
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// Asset visibility
|
||||
@@ -172,9 +178,7 @@ class UpdateAssetDto {
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
rating: json[r'rating'] == null
|
||||
? null
|
||||
: num.parse('${json[r'rating']}'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1217,10 +1217,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1910,10 +1910,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
version: "0.7.6"
|
||||
thumbhash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
4
mobile/test/drift/main/generated/schema.dart
generated
4
mobile/test/drift/main/generated/schema.dart
generated
@@ -23,6 +23,7 @@ import 'schema_v17.dart' as v17;
|
||||
import 'schema_v18.dart' as v18;
|
||||
import 'schema_v19.dart' as v19;
|
||||
import 'schema_v20.dart' as v20;
|
||||
import 'schema_v21.dart' as v21;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
@@ -68,6 +69,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
return v19.DatabaseAtV19(db);
|
||||
case 20:
|
||||
return v20.DatabaseAtV20(db);
|
||||
case 21:
|
||||
return v21.DatabaseAtV21(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
@@ -94,5 +97,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
];
|
||||
}
|
||||
|
||||
9084
mobile/test/drift/main/generated/schema_v21.dart
generated
Normal file
9084
mobile/test/drift/main/generated/schema_v21.dart
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9407,27 +9407,10 @@
|
||||
"name": "rating",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by rating [1-5], or null for unrated",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable",
|
||||
"description": "Filter by rating",
|
||||
"schema": {
|
||||
"minimum": -1,
|
||||
"maximum": 5,
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
@@ -13492,16 +13475,6 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bbox",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Bounding box coordinates as west,south,east,north (WGS84)",
|
||||
"schema": {
|
||||
"example": "11.075683,49.416711,11.117589,49.454875",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isFavorite",
|
||||
"required": false,
|
||||
@@ -13678,16 +13651,6 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bbox",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Bounding box coordinates as west,south,east,north (WGS84)",
|
||||
"schema": {
|
||||
"example": "11.075683,49.416711,11.117589,49.454875",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isFavorite",
|
||||
"required": false,
|
||||
@@ -15909,27 +15872,10 @@
|
||||
"type": "number"
|
||||
},
|
||||
"rating": {
|
||||
"description": "Rating in range [1-5], or null for unrated",
|
||||
"description": "Rating",
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"nullable": true,
|
||||
"type": "number",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
"type": "number"
|
||||
},
|
||||
"timeZone": {
|
||||
"description": "Time zone (IANA timezone)",
|
||||
@@ -19042,27 +18988,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"rating": {
|
||||
"description": "Filter by rating [1-5], or null for unrated",
|
||||
"description": "Filter by rating",
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"nullable": true,
|
||||
"type": "number",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"description": "Number of results to return",
|
||||
@@ -20785,27 +20714,10 @@
|
||||
"type": "array"
|
||||
},
|
||||
"rating": {
|
||||
"description": "Filter by rating [1-5], or null for unrated",
|
||||
"description": "Filter by rating",
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"nullable": true,
|
||||
"type": "number",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"description": "Number of results to return",
|
||||
@@ -22176,27 +22088,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"rating": {
|
||||
"description": "Filter by rating [1-5], or null for unrated",
|
||||
"description": "Filter by rating",
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"nullable": true,
|
||||
"type": "number",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"description": "Number of results to return",
|
||||
@@ -22427,27 +22322,10 @@
|
||||
"type": "array"
|
||||
},
|
||||
"rating": {
|
||||
"description": "Filter by rating [1-5], or null for unrated",
|
||||
"description": "Filter by rating",
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"nullable": true,
|
||||
"type": "number",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"description": "Filter by state/province name",
|
||||
@@ -23108,6 +22986,106 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAssetOcrDeleteV1": {
|
||||
"properties": {
|
||||
"assetId": {
|
||||
"description": "Original asset ID of the deleted OCR entry",
|
||||
"type": "string"
|
||||
},
|
||||
"deletedAt": {
|
||||
"description": "Timestamp when the OCR entry was deleted",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Audit row ID of the deleted OCR entry",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetId",
|
||||
"deletedAt",
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAssetOcrV1": {
|
||||
"properties": {
|
||||
"assetId": {
|
||||
"description": "Asset ID",
|
||||
"type": "string"
|
||||
},
|
||||
"boxScore": {
|
||||
"description": "Confidence score of the bounding box",
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"description": "OCR entry ID",
|
||||
"type": "string"
|
||||
},
|
||||
"isVisible": {
|
||||
"description": "Whether the OCR entry is visible",
|
||||
"type": "boolean"
|
||||
},
|
||||
"text": {
|
||||
"description": "Recognized text content",
|
||||
"type": "string"
|
||||
},
|
||||
"textScore": {
|
||||
"description": "Confidence score of the recognized text",
|
||||
"type": "number"
|
||||
},
|
||||
"x1": {
|
||||
"description": "Top-left X coordinate (normalized 0–1)",
|
||||
"type": "number"
|
||||
},
|
||||
"x2": {
|
||||
"description": "Top-right X coordinate (normalized 0–1)",
|
||||
"type": "number"
|
||||
},
|
||||
"x3": {
|
||||
"description": "Bottom-right X coordinate (normalized 0–1)",
|
||||
"type": "number"
|
||||
},
|
||||
"x4": {
|
||||
"description": "Bottom-left X coordinate (normalized 0–1)",
|
||||
"type": "number"
|
||||
},
|
||||
"y1": {
|
||||
"description": "Top-left Y coordinate (normalized 0–1)",
|
||||
"type": "number"
|
||||
},
|
||||
"y2": {
|
||||
"description": "Top-right Y coordinate (normalized 0–1)",
|
||||
"type": "number"
|
||||
},
|
||||
"y3": {
|
||||
"description": "Bottom-right Y coordinate (normalized 0–1)",
|
||||
"type": "number"
|
||||
},
|
||||
"y4": {
|
||||
"description": "Bottom-left Y coordinate (normalized 0–1)",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetId",
|
||||
"boxScore",
|
||||
"id",
|
||||
"isVisible",
|
||||
"text",
|
||||
"textScore",
|
||||
"x1",
|
||||
"x2",
|
||||
"x3",
|
||||
"x4",
|
||||
"y1",
|
||||
"y2",
|
||||
"y3",
|
||||
"y4"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAssetV1": {
|
||||
"properties": {
|
||||
"checksum": {
|
||||
@@ -23331,6 +23309,8 @@
|
||||
"AssetEditDeleteV1",
|
||||
"AssetMetadataV1",
|
||||
"AssetMetadataDeleteV1",
|
||||
"AssetOcrV1",
|
||||
"AssetOcrDeleteV1",
|
||||
"PartnerV1",
|
||||
"PartnerDeleteV1",
|
||||
"PartnerAssetV1",
|
||||
@@ -23628,6 +23608,7 @@
|
||||
"AssetExifsV1",
|
||||
"AssetEditsV1",
|
||||
"AssetMetadataV1",
|
||||
"AssetOcrV1",
|
||||
"AuthUsersV1",
|
||||
"MemoriesV1",
|
||||
"MemoryToAssetsV1",
|
||||
@@ -25331,27 +25312,10 @@
|
||||
"type": "number"
|
||||
},
|
||||
"rating": {
|
||||
"description": "Rating in range [1-5], or null for unrated",
|
||||
"description": "Rating",
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"nullable": true,
|
||||
"type": "number",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
"type": "number"
|
||||
},
|
||||
"visibility": {
|
||||
"allOf": [
|
||||
|
||||
@@ -834,8 +834,8 @@ export type AssetBulkUpdateDto = {
|
||||
latitude?: number;
|
||||
/** Longitude coordinate */
|
||||
longitude?: number;
|
||||
/** Rating in range [1-5], or null for unrated */
|
||||
rating?: number | null;
|
||||
/** Rating */
|
||||
rating?: number;
|
||||
/** Time zone (IANA timezone) */
|
||||
timeZone?: string;
|
||||
/** Asset visibility */
|
||||
@@ -944,8 +944,8 @@ export type UpdateAssetDto = {
|
||||
livePhotoVideoId?: string | null;
|
||||
/** Longitude coordinate */
|
||||
longitude?: number;
|
||||
/** Rating in range [1-5], or null for unrated */
|
||||
rating?: number | null;
|
||||
/** Rating */
|
||||
rating?: number;
|
||||
/** Asset visibility */
|
||||
visibility?: AssetVisibility;
|
||||
};
|
||||
@@ -1711,8 +1711,8 @@ export type MetadataSearchDto = {
|
||||
personIds?: string[];
|
||||
/** Filter by preview file path */
|
||||
previewPath?: string;
|
||||
/** Filter by rating [1-5], or null for unrated */
|
||||
rating?: number | null;
|
||||
/** Filter by rating */
|
||||
rating?: number;
|
||||
/** Number of results to return */
|
||||
size?: number;
|
||||
/** Filter by state/province name */
|
||||
@@ -1827,8 +1827,8 @@ export type RandomSearchDto = {
|
||||
ocr?: string;
|
||||
/** Filter by person IDs */
|
||||
personIds?: string[];
|
||||
/** Filter by rating [1-5], or null for unrated */
|
||||
rating?: number | null;
|
||||
/** Filter by rating */
|
||||
rating?: number;
|
||||
/** Number of results to return */
|
||||
size?: number;
|
||||
/** Filter by state/province name */
|
||||
@@ -1903,8 +1903,8 @@ export type SmartSearchDto = {
|
||||
query?: string;
|
||||
/** Asset ID to use as search reference */
|
||||
queryAssetId?: string;
|
||||
/** Filter by rating [1-5], or null for unrated */
|
||||
rating?: number | null;
|
||||
/** Filter by rating */
|
||||
rating?: number;
|
||||
/** Number of results to return */
|
||||
size?: number;
|
||||
/** Filter by state/province name */
|
||||
@@ -1969,8 +1969,8 @@ export type StatisticsSearchDto = {
|
||||
ocr?: string;
|
||||
/** Filter by person IDs */
|
||||
personIds?: string[];
|
||||
/** Filter by rating [1-5], or null for unrated */
|
||||
rating?: number | null;
|
||||
/** Filter by rating */
|
||||
rating?: number;
|
||||
/** Filter by state/province name */
|
||||
state?: string | null;
|
||||
/** Filter by tag IDs */
|
||||
@@ -3083,6 +3083,44 @@ export type SyncAssetMetadataV1 = {
|
||||
/** Value */
|
||||
value: object;
|
||||
};
|
||||
export type SyncAssetOcrDeleteV1 = {
|
||||
/** Original asset ID of the deleted OCR entry */
|
||||
assetId: string;
|
||||
/** Timestamp when the OCR entry was deleted */
|
||||
deletedAt: string;
|
||||
/** Audit row ID of the deleted OCR entry */
|
||||
id: string;
|
||||
};
|
||||
export type SyncAssetOcrV1 = {
|
||||
/** Asset ID */
|
||||
assetId: string;
|
||||
/** Confidence score of the bounding box */
|
||||
boxScore: number;
|
||||
/** OCR entry ID */
|
||||
id: string;
|
||||
/** Whether the OCR entry is visible */
|
||||
isVisible: boolean;
|
||||
/** Recognized text content */
|
||||
text: string;
|
||||
/** Confidence score of the recognized text */
|
||||
textScore: number;
|
||||
/** Top-left X coordinate (normalized 0–1) */
|
||||
x1: number;
|
||||
/** Top-right X coordinate (normalized 0–1) */
|
||||
x2: number;
|
||||
/** Bottom-right X coordinate (normalized 0–1) */
|
||||
x3: number;
|
||||
/** Bottom-left X coordinate (normalized 0–1) */
|
||||
x4: number;
|
||||
/** Top-left Y coordinate (normalized 0–1) */
|
||||
y1: number;
|
||||
/** Top-right Y coordinate (normalized 0–1) */
|
||||
y2: number;
|
||||
/** Bottom-right Y coordinate (normalized 0–1) */
|
||||
y3: number;
|
||||
/** Bottom-left Y coordinate (normalized 0–1) */
|
||||
y4: number;
|
||||
};
|
||||
export type SyncAssetV1 = {
|
||||
/** Checksum */
|
||||
checksum: string;
|
||||
@@ -5454,7 +5492,7 @@ export function searchLargeAssets({ albumIds, city, country, createdAfter, creat
|
||||
model?: string | null;
|
||||
ocr?: string;
|
||||
personIds?: string[];
|
||||
rating?: number | null;
|
||||
rating?: number;
|
||||
size?: number;
|
||||
state?: string | null;
|
||||
tagIds?: string[] | null;
|
||||
@@ -6421,9 +6459,8 @@ export function tagAssets({ id, bulkIdsDto }: {
|
||||
/**
|
||||
* Get time bucket
|
||||
*/
|
||||
export function getTimeBucket({ albumId, bbox, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withCoordinates, withPartners, withStacked }: {
|
||||
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withCoordinates, withPartners, withStacked }: {
|
||||
albumId?: string;
|
||||
bbox?: string;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
key?: string;
|
||||
@@ -6443,7 +6480,6 @@ export function getTimeBucket({ albumId, bbox, isFavorite, isTrashed, key, order
|
||||
data: TimeBucketAssetResponseDto;
|
||||
}>(`/timeline/bucket${QS.query(QS.explode({
|
||||
albumId,
|
||||
bbox,
|
||||
isFavorite,
|
||||
isTrashed,
|
||||
key,
|
||||
@@ -6464,9 +6500,8 @@ export function getTimeBucket({ albumId, bbox, isFavorite, isTrashed, key, order
|
||||
/**
|
||||
* Get time buckets
|
||||
*/
|
||||
export function getTimeBuckets({ albumId, bbox, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withCoordinates, withPartners, withStacked }: {
|
||||
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withCoordinates, withPartners, withStacked }: {
|
||||
albumId?: string;
|
||||
bbox?: string;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
key?: string;
|
||||
@@ -6485,7 +6520,6 @@ export function getTimeBuckets({ albumId, bbox, isFavorite, isTrashed, key, orde
|
||||
data: TimeBucketsResponseDto[];
|
||||
}>(`/timeline/buckets${QS.query(QS.explode({
|
||||
albumId,
|
||||
bbox,
|
||||
isFavorite,
|
||||
isTrashed,
|
||||
key,
|
||||
@@ -7248,6 +7282,8 @@ export enum SyncEntityType {
|
||||
AssetEditDeleteV1 = "AssetEditDeleteV1",
|
||||
AssetMetadataV1 = "AssetMetadataV1",
|
||||
AssetMetadataDeleteV1 = "AssetMetadataDeleteV1",
|
||||
AssetOcrV1 = "AssetOcrV1",
|
||||
AssetOcrDeleteV1 = "AssetOcrDeleteV1",
|
||||
PartnerV1 = "PartnerV1",
|
||||
PartnerDeleteV1 = "PartnerDeleteV1",
|
||||
PartnerAssetV1 = "PartnerAssetV1",
|
||||
@@ -7299,6 +7335,7 @@ export enum SyncRequestType {
|
||||
AssetExifsV1 = "AssetExifsV1",
|
||||
AssetEditsV1 = "AssetEditsV1",
|
||||
AssetMetadataV1 = "AssetMetadataV1",
|
||||
AssetOcrV1 = "AssetOcrV1",
|
||||
AuthUsersV1 = "AuthUsersV1",
|
||||
MemoriesV1 = "MemoriesV1",
|
||||
MemoryToAssetsV1 = "MemoryToAssetsV1",
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -344,8 +344,8 @@ importers:
|
||||
specifier: 2.0.0-rc13
|
||||
version: 2.0.0-rc13
|
||||
'@immich/sql-tools':
|
||||
specifier: ^0.3.2
|
||||
version: 0.3.2
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
'@nestjs/bullmq':
|
||||
specifier: ^11.0.1
|
||||
version: 11.0.4(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(bullmq@5.69.3)
|
||||
@@ -3017,9 +3017,8 @@ packages:
|
||||
'@immich/justified-layout-wasm@0.4.3':
|
||||
resolution: {integrity: sha512-fpcQ7zPhP3Cp1bEXhONVYSUeIANa2uzaQFGKufUZQo5FO7aFT77szTVChhlCy4XaVy5R4ZvgSkA/1TJmeORz7Q==}
|
||||
|
||||
'@immich/sql-tools@0.3.2':
|
||||
resolution: {integrity: sha512-UWhy/+Lf8C1dJip5wPfFytI3Vq/9UyDKQE1ROjXwVhT6E/CPgBkRLwHPetjYGPJ4o1JVVpRLnEEJCXdvzqVpGw==}
|
||||
hasBin: true
|
||||
'@immich/sql-tools@0.2.0':
|
||||
resolution: {integrity: sha512-AH0GRIUYrckNKuid5uO33vgRbGaznhRtArdQ91K310A1oUFjaoNzOaZyZhXwEmft3WYeC1bx4fdgUeois2QH5A==}
|
||||
|
||||
'@immich/svelte-markdown-preprocess@0.2.1':
|
||||
resolution: {integrity: sha512-mbr/g75lO8Zh+ELCuYrZP0XB4gf2UbK8rJcGYMYxFJJzMMunV+sm9FqtV1dbwW2dpXzCZGz1XPCEZ6oo526TbA==}
|
||||
@@ -6059,10 +6058,6 @@ packages:
|
||||
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commander@14.0.3:
|
||||
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
@@ -14860,9 +14855,8 @@ snapshots:
|
||||
|
||||
'@immich/justified-layout-wasm@0.4.3': {}
|
||||
|
||||
'@immich/sql-tools@0.3.2':
|
||||
'@immich/sql-tools@0.2.0':
|
||||
dependencies:
|
||||
commander: 14.0.3
|
||||
kysely: 0.28.11
|
||||
kysely-postgres-js: 3.0.0(kysely@0.28.11)(postgres@3.4.8)
|
||||
pg-connection-string: 2.11.0
|
||||
@@ -18230,8 +18224,6 @@ snapshots:
|
||||
|
||||
commander@13.1.0: {}
|
||||
|
||||
commander@14.0.3: {}
|
||||
|
||||
commander@2.20.3: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
"test:cov": "vitest --config test/vitest.config.mjs --coverage",
|
||||
"test:medium": "vitest --config test/vitest.config.medium.mjs",
|
||||
"typeorm": "typeorm",
|
||||
"migrations:debug": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate --debug",
|
||||
"migrations:generate": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate",
|
||||
"migrations:create": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate",
|
||||
"migrations:run": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations run",
|
||||
"migrations:revert": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations revert",
|
||||
"schema:drop": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} query 'DROP schema public cascade; CREATE schema public;'",
|
||||
"migrations:debug": "node ./dist/bin/migrations.js debug",
|
||||
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
||||
"migrations:create": "node ./dist/bin/migrations.js create",
|
||||
"migrations:run": "node ./dist/bin/migrations.js run",
|
||||
"migrations:revert": "node ./dist/bin/migrations.js revert",
|
||||
"schema:drop": "node ./dist/bin/migrations.js query 'DROP schema public cascade; CREATE schema public;'",
|
||||
"schema:reset": "pnpm run schema:drop && pnpm run migrations:run",
|
||||
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
||||
"sync:sql": "node ./dist/bin/sync-sql.js",
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@extism/extism": "2.0.0-rc13",
|
||||
"@immich/sql-tools": "^0.3.2",
|
||||
"@immich/sql-tools": "^0.2.0",
|
||||
"@nestjs/bullmq": "^11.0.1",
|
||||
"@nestjs/common": "^11.0.4",
|
||||
"@nestjs/core": "^11.0.4",
|
||||
|
||||
213
server/src/bin/migrations.ts
Normal file
213
server/src/bin/migrations.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env node
|
||||
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
|
||||
|
||||
import { schemaDiff, schemaFromCode, schemaFromDatabase } from '@immich/sql-tools';
|
||||
import { Kysely, sql } from 'kysely';
|
||||
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { basename, dirname, extname, join } from 'node:path';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import 'src/schema';
|
||||
import { getKyselyConfig } from 'src/utils/database';
|
||||
|
||||
const main = async () => {
|
||||
const command = process.argv[2];
|
||||
const path = process.argv[3] || 'src/Migration';
|
||||
|
||||
switch (command) {
|
||||
case 'debug': {
|
||||
await debug();
|
||||
return;
|
||||
}
|
||||
|
||||
case 'run': {
|
||||
await runMigrations();
|
||||
return;
|
||||
}
|
||||
|
||||
case 'revert': {
|
||||
await revert();
|
||||
return;
|
||||
}
|
||||
|
||||
case 'query': {
|
||||
const query = process.argv[3];
|
||||
await runQuery(query);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'create': {
|
||||
create(path, [], []);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'generate': {
|
||||
await generate(path);
|
||||
return;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.log(`Usage:
|
||||
node dist/bin/migrations.js create <name>
|
||||
node dist/bin/migrations.js generate <name>
|
||||
node dist/bin/migrations.js run
|
||||
node dist/bin/migrations.js revert
|
||||
`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabaseClient = () => {
|
||||
const configRepository = new ConfigRepository();
|
||||
const { database } = configRepository.getEnv();
|
||||
return new Kysely<any>(getKyselyConfig(database.config));
|
||||
};
|
||||
|
||||
const runQuery = async (query: string) => {
|
||||
const db = getDatabaseClient();
|
||||
await sql.raw(query).execute(db);
|
||||
await db.destroy();
|
||||
};
|
||||
|
||||
const runMigrations = async () => {
|
||||
const configRepository = new ConfigRepository();
|
||||
const logger = LoggingRepository.create();
|
||||
const db = getDatabaseClient();
|
||||
const databaseRepository = new DatabaseRepository(db, logger, configRepository);
|
||||
await databaseRepository.runMigrations();
|
||||
await db.destroy();
|
||||
};
|
||||
|
||||
const revert = async () => {
|
||||
const configRepository = new ConfigRepository();
|
||||
const logger = LoggingRepository.create();
|
||||
const db = getDatabaseClient();
|
||||
const databaseRepository = new DatabaseRepository(db, logger, configRepository);
|
||||
|
||||
try {
|
||||
const migrationName = await databaseRepository.revertLastMigration();
|
||||
if (!migrationName) {
|
||||
console.log('No migrations to revert');
|
||||
return;
|
||||
}
|
||||
|
||||
markMigrationAsReverted(migrationName);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
const debug = async () => {
|
||||
const { up } = await compare();
|
||||
const upSql = '-- UP\n' + up.asSql({ comments: true }).join('\n');
|
||||
// const downSql = '-- DOWN\n' + down.asSql({ comments: true }).join('\n');
|
||||
writeFileSync('./migrations.sql', upSql + '\n\n');
|
||||
console.log('Wrote migrations.sql');
|
||||
};
|
||||
|
||||
const generate = async (path: string) => {
|
||||
const { up, down } = await compare();
|
||||
if (up.items.length === 0) {
|
||||
console.log('No changes detected');
|
||||
return;
|
||||
}
|
||||
create(path, up.asSql(), down.asSql());
|
||||
};
|
||||
|
||||
const create = (path: string, up: string[], down: string[]) => {
|
||||
const timestamp = Date.now();
|
||||
const name = basename(path, extname(path));
|
||||
const filename = `${timestamp}-${name}.ts`;
|
||||
const folder = dirname(path);
|
||||
const fullPath = join(folder, filename);
|
||||
mkdirSync(folder, { recursive: true });
|
||||
writeFileSync(fullPath, asMigration({ up, down }));
|
||||
console.log(`Wrote ${fullPath}`);
|
||||
};
|
||||
|
||||
const compare = async () => {
|
||||
const configRepository = new ConfigRepository();
|
||||
const { database } = configRepository.getEnv();
|
||||
|
||||
const source = schemaFromCode({ overrides: true, namingStrategy: 'default' });
|
||||
const target = await schemaFromDatabase({ connection: database.config });
|
||||
|
||||
console.log(source.warnings.join('\n'));
|
||||
|
||||
const up = schemaDiff(source, target, {
|
||||
tables: { ignoreExtra: true },
|
||||
functions: { ignoreExtra: false },
|
||||
parameters: { ignoreExtra: true },
|
||||
});
|
||||
const down = schemaDiff(target, source, {
|
||||
tables: { ignoreExtra: false, ignoreMissing: true },
|
||||
functions: { ignoreExtra: false },
|
||||
extensions: { ignoreMissing: true },
|
||||
parameters: { ignoreMissing: true },
|
||||
});
|
||||
|
||||
return { up, down };
|
||||
};
|
||||
|
||||
type MigrationProps = {
|
||||
up: string[];
|
||||
down: string[];
|
||||
};
|
||||
|
||||
const asMigration = ({ up, down }: MigrationProps) => {
|
||||
const upSql = up.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n');
|
||||
const downSql = down.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n');
|
||||
|
||||
return `import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
${upSql}
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
${downSql}
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
const markMigrationAsReverted = (migrationName: string) => {
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
const distRoot = join(__dirname, '..');
|
||||
const projectRoot = join(distRoot, '..');
|
||||
const sourceFolder = join(projectRoot, 'src', 'schema', 'migrations');
|
||||
const distFolder = join(distRoot, 'schema', 'migrations');
|
||||
|
||||
const sourcePath = join(sourceFolder, `${migrationName}.ts`);
|
||||
const revertedFolder = join(sourceFolder, 'reverted');
|
||||
const revertedPath = join(revertedFolder, `${migrationName}.ts`);
|
||||
|
||||
if (existsSync(revertedPath)) {
|
||||
console.log(`Migration ${migrationName} is already marked as reverted`);
|
||||
} else if (existsSync(sourcePath)) {
|
||||
mkdirSync(revertedFolder, { recursive: true });
|
||||
renameSync(sourcePath, revertedPath);
|
||||
console.log(`Moved ${sourcePath} to ${revertedPath}`);
|
||||
} else {
|
||||
console.warn(`Source migration file not found for ${migrationName}`);
|
||||
}
|
||||
|
||||
const distBase = join(distFolder, migrationName);
|
||||
for (const extension of ['.js', '.js.map', '.d.ts']) {
|
||||
const filePath = `${distBase}${extension}`;
|
||||
if (existsSync(filePath)) {
|
||||
rmSync(filePath, { force: true });
|
||||
console.log(`Removed ${filePath}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
console.log('Something went wrong');
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -207,28 +207,12 @@ describe(AssetController.name, () => {
|
||||
});
|
||||
|
||||
it('should reject invalid rating', async () => {
|
||||
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: -2 }]) {
|
||||
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
}
|
||||
});
|
||||
|
||||
it('should convert rating 0 to null', async () => {
|
||||
const assetId = factory.uuid();
|
||||
const { status } = await request(ctx.getHttpServer()).put(`/assets/${assetId}`).send({ rating: 0 });
|
||||
expect(service.update).toHaveBeenCalledWith(undefined, assetId, { rating: null });
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
it('should leave correct ratings as-is', async () => {
|
||||
const assetId = factory.uuid();
|
||||
for (const test of [{ rating: -1 }, { rating: 1 }, { rating: 5 }]) {
|
||||
const { status } = await request(ctx.getHttpServer()).put(`/assets/${assetId}`).send(test);
|
||||
expect(service.update).toHaveBeenCalledWith(undefined, assetId, test);
|
||||
expect(status).toBe(200);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /assets/statistics', () => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
AssetFileType,
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
ChecksumAlgorithm,
|
||||
MemoryType,
|
||||
Permission,
|
||||
PluginContext,
|
||||
@@ -112,7 +111,6 @@ export type Memory = {
|
||||
export type Asset = {
|
||||
id: string;
|
||||
checksum: Buffer<ArrayBufferLike>;
|
||||
checksumAlgorithm: ChecksumAlgorithm;
|
||||
deviceAssetId: string;
|
||||
deviceId: string;
|
||||
fileCreatedAt: Date;
|
||||
@@ -331,7 +329,6 @@ export const columns = {
|
||||
asset: [
|
||||
'asset.id',
|
||||
'asset.checksum',
|
||||
'asset.checksumAlgorithm',
|
||||
'asset.deviceAssetId',
|
||||
'asset.deviceId',
|
||||
'asset.fileCreatedAt',
|
||||
@@ -440,6 +437,23 @@ export const columns = {
|
||||
'asset_exif.rating',
|
||||
'asset_exif.fps',
|
||||
],
|
||||
syncAssetOcr: [
|
||||
'asset_ocr.id',
|
||||
'asset_ocr.assetId',
|
||||
'asset_ocr.x1',
|
||||
'asset_ocr.y1',
|
||||
'asset_ocr.x2',
|
||||
'asset_ocr.y2',
|
||||
'asset_ocr.x3',
|
||||
'asset_ocr.y3',
|
||||
'asset_ocr.x4',
|
||||
'asset_ocr.y4',
|
||||
'asset_ocr.text',
|
||||
'asset_ocr.boxScore',
|
||||
'asset_ocr.textScore',
|
||||
'asset_ocr.updateId',
|
||||
'asset_ocr.isVisible',
|
||||
],
|
||||
syncAssetEdit: [
|
||||
'asset_edit.id',
|
||||
'asset_edit.assetId',
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from 'src/dtos/person.dto';
|
||||
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
import { AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum';
|
||||
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||
import { ImageDimensions } from 'src/types';
|
||||
import { getDimensions } from 'src/utils/asset.util';
|
||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
@@ -147,7 +147,6 @@ export type MapAsset = {
|
||||
updateId: string;
|
||||
status: AssetStatus;
|
||||
checksum: Buffer<ArrayBufferLike>;
|
||||
checksumAlgorithm: ChecksumAlgorithm;
|
||||
deviceAssetId: string;
|
||||
deviceId: string;
|
||||
duplicateId: string | null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsDateString,
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { HistoryBuilder, Property } from 'src/decorators';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetType, AssetVisibility } from 'src/enum';
|
||||
import { AssetStats } from 'src/repositories/asset.repository';
|
||||
@@ -57,19 +56,12 @@ export class UpdateAssetBase {
|
||||
@IsNotEmpty()
|
||||
longitude?: number;
|
||||
|
||||
@Property({
|
||||
description: 'Rating in range [1-5], or null for unrated',
|
||||
history: new HistoryBuilder()
|
||||
.added('v1')
|
||||
.stable('v2')
|
||||
.updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'),
|
||||
})
|
||||
@Optional({ nullable: true })
|
||||
@ApiProperty({ description: 'Rating' })
|
||||
@Optional()
|
||||
@IsInt()
|
||||
@Max(5)
|
||||
@Min(-1)
|
||||
@Transform(({ value }) => (value === 0 ? null : value))
|
||||
rating?: number | null;
|
||||
rating?: number;
|
||||
|
||||
@ApiProperty({ description: 'Asset description' })
|
||||
@Optional()
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsLatitude, IsLongitude } from 'class-validator';
|
||||
import { IsGreaterThanOrEqualTo } from 'src/validation';
|
||||
|
||||
export class BBoxDto {
|
||||
@ApiProperty({ format: 'double', description: 'West longitude (-180 to 180)' })
|
||||
@IsLongitude()
|
||||
west!: number;
|
||||
|
||||
@ApiProperty({ format: 'double', description: 'South latitude (-90 to 90)' })
|
||||
@IsLatitude()
|
||||
south!: number;
|
||||
|
||||
@ApiProperty({
|
||||
format: 'double',
|
||||
description: 'East longitude (-180 to 180). May be less than west when crossing the antimeridian.',
|
||||
})
|
||||
@IsLongitude()
|
||||
east!: number;
|
||||
|
||||
@ApiProperty({ format: 'double', description: 'North latitude (-90 to 90). Must be >= south.' })
|
||||
@IsLatitude()
|
||||
@IsGreaterThanOrEqualTo('south')
|
||||
north!: number;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||
import { Place } from 'src/database';
|
||||
import { HistoryBuilder, Property } from 'src/decorators';
|
||||
import { HistoryBuilder } from 'src/decorators';
|
||||
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AssetOrder, AssetType, AssetVisibility } from 'src/enum';
|
||||
@@ -103,21 +103,12 @@ class BaseSearchDto {
|
||||
@ValidateUUID({ each: true, optional: true, description: 'Filter by album IDs' })
|
||||
albumIds?: string[];
|
||||
|
||||
@Property({
|
||||
type: 'number',
|
||||
description: 'Filter by rating [1-5], or null for unrated',
|
||||
minimum: -1,
|
||||
maximum: 5,
|
||||
history: new HistoryBuilder()
|
||||
.added('v1')
|
||||
.stable('v2')
|
||||
.updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'),
|
||||
})
|
||||
@Optional({ nullable: true })
|
||||
@ApiPropertyOptional({ type: 'number', description: 'Filter by rating', minimum: -1, maximum: 5 })
|
||||
@Optional()
|
||||
@IsInt()
|
||||
@Max(5)
|
||||
@Min(-1)
|
||||
rating?: number | null;
|
||||
rating?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by OCR text content' })
|
||||
@IsString()
|
||||
|
||||
@@ -220,6 +220,62 @@ export class SyncAssetExifV1 {
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAssetOcrV1 {
|
||||
@ApiProperty({ description: 'OCR entry ID' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ description: 'Asset ID' })
|
||||
assetId!: string;
|
||||
|
||||
@ApiProperty({ description: 'Top-left X coordinate (normalized 0–1)' })
|
||||
x1!: number;
|
||||
|
||||
@ApiProperty({ description: 'Top-left Y coordinate (normalized 0–1)' })
|
||||
y1!: number;
|
||||
|
||||
@ApiProperty({ description: 'Top-right X coordinate (normalized 0–1)' })
|
||||
x2!: number;
|
||||
|
||||
@ApiProperty({ description: 'Top-right Y coordinate (normalized 0–1)' })
|
||||
y2!: number;
|
||||
|
||||
@ApiProperty({ description: 'Bottom-right X coordinate (normalized 0–1)' })
|
||||
x3!: number;
|
||||
|
||||
@ApiProperty({ description: 'Bottom-right Y coordinate (normalized 0–1)' })
|
||||
y3!: number;
|
||||
|
||||
@ApiProperty({ description: 'Bottom-left X coordinate (normalized 0–1)' })
|
||||
x4!: number;
|
||||
|
||||
@ApiProperty({ description: 'Bottom-left Y coordinate (normalized 0–1)' })
|
||||
y4!: number;
|
||||
|
||||
@ApiProperty({ description: 'Confidence score of the bounding box' })
|
||||
boxScore!: number;
|
||||
|
||||
@ApiProperty({ description: 'Confidence score of the recognized text' })
|
||||
textScore!: number;
|
||||
|
||||
@ApiProperty({ description: 'Recognized text content' })
|
||||
text!: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether the OCR entry is visible' })
|
||||
isVisible!: boolean;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAssetOcrDeleteV1 {
|
||||
@ApiProperty({ description: 'Audit row ID of the deleted OCR entry' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ description: 'Original asset ID of the deleted OCR entry' })
|
||||
assetId!: string;
|
||||
|
||||
@ApiProperty({ description: 'Timestamp when the OCR entry was deleted' })
|
||||
deletedAt!: Date;
|
||||
}
|
||||
|
||||
export class SyncAssetEditV1 {
|
||||
id!: string;
|
||||
assetId!: string;
|
||||
@@ -499,6 +555,8 @@ export type SyncItem = {
|
||||
[SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1;
|
||||
[SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1;
|
||||
[SyncEntityType.AssetExifV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AssetOcrV1]: SyncAssetOcrV1;
|
||||
[SyncEntityType.AssetOcrDeleteV1]: SyncAssetOcrDeleteV1;
|
||||
[SyncEntityType.AssetEditV1]: SyncAssetEditV1;
|
||||
[SyncEntityType.AssetEditDeleteV1]: SyncAssetEditDeleteV1;
|
||||
[SyncEntityType.PartnerAssetV1]: SyncAssetV1;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
import { IsString } from 'class-validator';
|
||||
import type { BBoxDto } from 'src/dtos/bbox.dto';
|
||||
import { AssetOrder, AssetVisibility } from 'src/enum';
|
||||
import { ValidateBBox } from 'src/utils/bbox';
|
||||
import { ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class TimeBucketDto {
|
||||
@@ -60,9 +59,6 @@ export class TimeBucketDto {
|
||||
description: 'Include location data in the response',
|
||||
})
|
||||
withCoordinates?: boolean;
|
||||
|
||||
@ValidateBBox({ optional: true })
|
||||
bbox?: BBoxDto;
|
||||
}
|
||||
|
||||
export class TimeBucketAssetDto extends TimeBucketDto {
|
||||
|
||||
@@ -37,11 +37,6 @@ export enum AssetType {
|
||||
Other = 'OTHER',
|
||||
}
|
||||
|
||||
export enum ChecksumAlgorithm {
|
||||
sha1File = 'sha1-file', // sha1 checksum of the whole file contents
|
||||
sha1Path = 'sha1-path', // sha1 checksum of "path:" plus the file path, currently used in external libraries, deprecated
|
||||
}
|
||||
|
||||
export enum AssetFileType {
|
||||
/**
|
||||
* An full/large-size image extracted/converted from RAW photos
|
||||
@@ -727,6 +722,7 @@ export enum SyncRequestType {
|
||||
AssetExifsV1 = 'AssetExifsV1',
|
||||
AssetEditsV1 = 'AssetEditsV1',
|
||||
AssetMetadataV1 = 'AssetMetadataV1',
|
||||
AssetOcrV1 = 'AssetOcrV1',
|
||||
AuthUsersV1 = 'AuthUsersV1',
|
||||
MemoriesV1 = 'MemoriesV1',
|
||||
MemoryToAssetsV1 = 'MemoryToAssetsV1',
|
||||
@@ -755,6 +751,8 @@ export enum SyncEntityType {
|
||||
AssetEditDeleteV1 = 'AssetEditDeleteV1',
|
||||
AssetMetadataV1 = 'AssetMetadataV1',
|
||||
AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1',
|
||||
AssetOcrV1 = 'AssetOcrV1',
|
||||
AssetOcrDeleteV1 = 'AssetOcrDeleteV1',
|
||||
|
||||
PartnerV1 = 'PartnerV1',
|
||||
PartnerDeleteV1 = 'PartnerDeleteV1',
|
||||
|
||||
@@ -250,7 +250,6 @@ where
|
||||
select
|
||||
"asset"."id",
|
||||
"asset"."checksum",
|
||||
"asset"."checksumAlgorithm",
|
||||
"asset"."deviceAssetId",
|
||||
"asset"."deviceId",
|
||||
"asset"."fileCreatedAt",
|
||||
@@ -563,7 +562,6 @@ select
|
||||
"asset"."checksum",
|
||||
"asset"."originalPath",
|
||||
"asset"."isExternal",
|
||||
"asset"."visibility",
|
||||
"asset"."originalFileName",
|
||||
"asset"."livePhotoVideoId",
|
||||
"asset"."fileCreatedAt",
|
||||
@@ -595,7 +593,6 @@ from
|
||||
where
|
||||
"asset"."deletedAt" is null
|
||||
and "asset"."id" = $2
|
||||
and "asset"."visibility" != $3
|
||||
|
||||
-- AssetJobRepository.streamForStorageTemplateJob
|
||||
select
|
||||
@@ -605,7 +602,6 @@ select
|
||||
"asset"."checksum",
|
||||
"asset"."originalPath",
|
||||
"asset"."isExternal",
|
||||
"asset"."visibility",
|
||||
"asset"."originalFileName",
|
||||
"asset"."livePhotoVideoId",
|
||||
"asset"."fileCreatedAt",
|
||||
@@ -636,7 +632,6 @@ from
|
||||
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."deletedAt" is null
|
||||
and "asset"."visibility" != $2
|
||||
|
||||
-- AssetJobRepository.streamForDeletedJob
|
||||
select
|
||||
|
||||
@@ -353,7 +353,6 @@ export class AssetJobRepository {
|
||||
'asset.checksum',
|
||||
'asset.originalPath',
|
||||
'asset.isExternal',
|
||||
'asset.visibility',
|
||||
'asset.originalFileName',
|
||||
'asset.livePhotoVideoId',
|
||||
'asset.fileCreatedAt',
|
||||
@@ -368,16 +367,13 @@ export class AssetJobRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getForStorageTemplateJob(id: string, options?: { includeHidden?: boolean }) {
|
||||
return this.storageTemplateAssetQuery()
|
||||
.where('asset.id', '=', id)
|
||||
.$if(!options?.includeHidden, (qb) => qb.where('asset.visibility', '!=', AssetVisibility.Hidden))
|
||||
.executeTakeFirst();
|
||||
getForStorageTemplateJob(id: string) {
|
||||
return this.storageTemplateAssetQuery().where('asset.id', '=', id).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
streamForStorageTemplateJob() {
|
||||
return this.storageTemplateAssetQuery().where('asset.visibility', '!=', AssetVisibility.Hidden).stream();
|
||||
return this.storageTemplateAssetQuery().stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.DATE], stream: true })
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ExpressionBuilder,
|
||||
Insertable,
|
||||
Kysely,
|
||||
NotNull,
|
||||
Selectable,
|
||||
SelectQueryBuilder,
|
||||
sql,
|
||||
Updateable,
|
||||
UpdateResult,
|
||||
} from 'kysely';
|
||||
import { ExpressionBuilder, Insertable, Kysely, NotNull, Selectable, sql, Updateable, UpdateResult } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { isEmpty, isUndefined, omitBy } from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
@@ -46,13 +36,6 @@ import { globToSqlPattern } from 'src/utils/misc';
|
||||
|
||||
export type AssetStats = Record<AssetType, number>;
|
||||
|
||||
export interface BoundingBox {
|
||||
west: number;
|
||||
south: number;
|
||||
east: number;
|
||||
north: number;
|
||||
}
|
||||
|
||||
interface AssetStatsOptions {
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
@@ -81,7 +64,6 @@ interface AssetBuilderOptions {
|
||||
assetType?: AssetType;
|
||||
visibility?: AssetVisibility;
|
||||
withCoordinates?: boolean;
|
||||
bbox?: BoundingBox;
|
||||
}
|
||||
|
||||
export interface TimeBucketOptions extends AssetBuilderOptions {
|
||||
@@ -138,34 +120,6 @@ interface GetByIdsRelations {
|
||||
const distinctLocked = <T extends LockableProperty[] | null>(eb: ExpressionBuilder<DB, 'asset_exif'>, columns: T) =>
|
||||
sql<T>`nullif(array(select distinct unnest(${eb.ref('asset_exif.lockedProperties')} || ${columns})), '{}')`;
|
||||
|
||||
const getBoundingCircle = (bbox: BoundingBox) => {
|
||||
const { west, south, east, north } = bbox;
|
||||
const eastUnwrapped = west <= east ? east : east + 360;
|
||||
const centerLongitude = (((west + eastUnwrapped) / 2 + 540) % 360) - 180;
|
||||
const centerLatitude = (south + north) / 2;
|
||||
const radius = sql<number>`greatest(
|
||||
earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${south}, ${west})),
|
||||
earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${south}, ${east})),
|
||||
earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${north}, ${west})),
|
||||
earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${north}, ${east}))
|
||||
)`;
|
||||
|
||||
return { centerLatitude, centerLongitude, radius };
|
||||
};
|
||||
|
||||
const withBoundingBox = <T>(qb: SelectQueryBuilder<DB, 'asset' | 'asset_exif', T>, bbox: BoundingBox) => {
|
||||
const { west, south, east, north } = bbox;
|
||||
const withLatitude = qb.where('asset_exif.latitude', '>=', south).where('asset_exif.latitude', '<=', north);
|
||||
|
||||
if (west <= east) {
|
||||
return withLatitude.where('asset_exif.longitude', '>=', west).where('asset_exif.longitude', '<=', east);
|
||||
}
|
||||
|
||||
return withLatitude.where((eb) =>
|
||||
eb.or([eb('asset_exif.longitude', '>=', west), eb('asset_exif.longitude', '<=', east)]),
|
||||
);
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AssetRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
@@ -697,20 +651,6 @@ export class AssetRepository {
|
||||
.select(truncatedDate<Date>().as('timeBucket'))
|
||||
.$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted))
|
||||
.where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.$if(!!options.bbox, (qb) => {
|
||||
const bbox = options.bbox!;
|
||||
const circle = getBoundingCircle(bbox);
|
||||
|
||||
const withBoundingCircle = qb
|
||||
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||
.where(
|
||||
sql`earth_box(ll_to_earth_public(${circle.centerLatitude}, ${circle.centerLongitude}), ${circle.radius})`,
|
||||
'@>',
|
||||
sql`ll_to_earth_public(asset_exif.latitude, asset_exif.longitude)`,
|
||||
);
|
||||
|
||||
return withBoundingBox(withBoundingCircle, bbox);
|
||||
})
|
||||
.$if(options.visibility === undefined, withDefaultVisibility)
|
||||
.$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!))
|
||||
.$if(!!options.albumId, (qb) =>
|
||||
@@ -785,18 +725,6 @@ export class AssetRepository {
|
||||
.where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.$if(options.visibility == undefined, withDefaultVisibility)
|
||||
.$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!))
|
||||
.$if(!!options.bbox, (qb) => {
|
||||
const bbox = options.bbox!;
|
||||
const circle = getBoundingCircle(bbox);
|
||||
|
||||
const withBoundingCircle = qb.where(
|
||||
sql`earth_box(ll_to_earth_public(${circle.centerLatitude}, ${circle.centerLongitude}), ${circle.radius})`,
|
||||
'@>',
|
||||
sql`ll_to_earth_public(asset_exif.latitude, asset_exif.longitude)`,
|
||||
);
|
||||
|
||||
return withBoundingBox(withBoundingCircle, bbox);
|
||||
})
|
||||
.where(truncatedDate(), '=', timeBucket.replace(/^[+-]/, ''))
|
||||
.$if(!!options.albumId, (qb) =>
|
||||
qb.where((eb) =>
|
||||
|
||||
@@ -22,7 +22,6 @@ import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import 'src/schema'; // make sure all schema definitions are imported for schemaFromCode
|
||||
import { DB } from 'src/schema';
|
||||
import { immich_uuid_v7 } from 'src/schema/functions';
|
||||
import { ExtensionVersion, VectorExtension, VectorUpdateResult } from 'src/types';
|
||||
import { vectorIndexQuery } from 'src/utils/database';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
@@ -289,11 +288,7 @@ export class DatabaseRepository {
|
||||
}
|
||||
|
||||
async getSchemaDrift() {
|
||||
const source = schemaFromCode({
|
||||
overrides: true,
|
||||
namingStrategy: 'default',
|
||||
uuidFunction: (version) => (version === 7 ? `${immich_uuid_v7.name}()` : 'uuid_generate_v4()'),
|
||||
});
|
||||
const source = schemaFromCode({ overrides: true, namingStrategy: 'default' });
|
||||
const { database } = this.configRepository.getEnv();
|
||||
const target = await schemaFromDatabase({ connection: database.config });
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ export class MediaRepository {
|
||||
ExposureTime: tags.exposureTime,
|
||||
ProfileDescription: tags.profileDescription,
|
||||
ColorSpace: tags.colorspace,
|
||||
Rating: tags.rating === null ? 0 : tags.rating,
|
||||
Rating: tags.rating,
|
||||
// specially convert Orientation to numeric Orientation# for exiftool
|
||||
'Orientation#': tags.orientation ? Number(tags.orientation) : undefined,
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ export class SyncRepository {
|
||||
assetEdit: AssetEditSync;
|
||||
assetFace: AssetFaceSync;
|
||||
assetMetadata: AssetMetadataSync;
|
||||
assetOcr: AssetOcrSync;
|
||||
authUser: AuthUserSync;
|
||||
memory: MemorySync;
|
||||
memoryToAsset: MemoryToAssetSync;
|
||||
@@ -79,6 +80,7 @@ export class SyncRepository {
|
||||
this.assetEdit = new AssetEditSync(this.db);
|
||||
this.assetFace = new AssetFaceSync(this.db);
|
||||
this.assetMetadata = new AssetMetadataSync(this.db);
|
||||
this.assetOcr = new AssetOcrSync(this.db);
|
||||
this.authUser = new AuthUserSync(this.db);
|
||||
this.memory = new MemorySync(this.db);
|
||||
this.memoryToAsset = new MemoryToAssetSync(this.db);
|
||||
@@ -798,3 +800,27 @@ class AssetMetadataSync extends BaseSync {
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class AssetOcrSync extends BaseSync {
|
||||
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
|
||||
getDeletes(options: SyncQueryOptions, userId: string) {
|
||||
return this.auditQuery('asset_ocr_audit', options)
|
||||
.select(['asset_ocr_audit.id', 'asset_ocr_audit.assetId', 'asset_ocr_audit.deletedAt'])
|
||||
.leftJoin('asset', 'asset.id', 'asset_ocr_audit.assetId')
|
||||
.where('asset.ownerId', '=', userId)
|
||||
.stream();
|
||||
}
|
||||
|
||||
cleanupAuditTable(daysAgo: number) {
|
||||
return this.auditCleanup('asset_ocr_audit', daysAgo);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
|
||||
getUpserts(options: SyncQueryOptions, userId: string) {
|
||||
return this.upsertQuery('asset_ocr', options)
|
||||
.select(columns.syncAssetOcr)
|
||||
.innerJoin('asset', 'asset.id', 'asset_ocr.assetId')
|
||||
.where('asset.ownerId', '=', userId)
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { registerEnum } from '@immich/sql-tools';
|
||||
import { AssetStatus, AssetVisibility, ChecksumAlgorithm, SourceType } from 'src/enum';
|
||||
import { AssetStatus, AssetVisibility, SourceType } from 'src/enum';
|
||||
|
||||
export const assets_status_enum = registerEnum({
|
||||
name: 'assets_status_enum',
|
||||
@@ -15,8 +15,3 @@ export const asset_visibility_enum = registerEnum({
|
||||
name: 'asset_visibility_enum',
|
||||
values: Object.values(AssetVisibility),
|
||||
});
|
||||
|
||||
export const asset_checksum_algorithm_enum = registerEnum({
|
||||
name: 'asset_checksum_algorithm_enum',
|
||||
values: Object.values(ChecksumAlgorithm),
|
||||
});
|
||||
|
||||
@@ -299,3 +299,16 @@ export const asset_edit_audit = registerFunction({
|
||||
RETURN NULL;
|
||||
END`,
|
||||
});
|
||||
|
||||
export const asset_ocr_delete_audit = registerFunction({
|
||||
name: 'asset_ocr_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO asset_ocr_audit ("assetId")
|
||||
SELECT "assetId"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END`,
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
asset_delete_audit,
|
||||
asset_face_audit,
|
||||
asset_metadata_audit,
|
||||
asset_ocr_delete_audit,
|
||||
f_concat_ws,
|
||||
f_unaccent,
|
||||
immich_uuid_v7,
|
||||
@@ -74,6 +75,7 @@ import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
|
||||
import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table';
|
||||
import { AssetOcrAuditTable } from './tables/asset-ocr-audit.table';
|
||||
|
||||
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
|
||||
@Database({ name: 'immich' })
|
||||
@@ -96,6 +98,7 @@ export class ImmichDatabase {
|
||||
AssetMetadataAuditTable,
|
||||
AssetJobStatusTable,
|
||||
AssetOcrTable,
|
||||
AssetOcrAuditTable,
|
||||
AssetTable,
|
||||
AssetFileTable,
|
||||
AuditTable,
|
||||
@@ -158,6 +161,7 @@ export class ImmichDatabase {
|
||||
user_metadata_audit,
|
||||
asset_metadata_audit,
|
||||
asset_face_audit,
|
||||
asset_ocr_delete_audit,
|
||||
];
|
||||
|
||||
enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum];
|
||||
@@ -195,6 +199,7 @@ export interface DB {
|
||||
asset_metadata_audit: AssetMetadataAuditTable;
|
||||
asset_job_status: AssetJobStatusTable;
|
||||
asset_ocr: AssetOcrTable;
|
||||
asset_ocr_audit: AssetOcrAuditTable;
|
||||
ocr_search: OcrSearchTable;
|
||||
|
||||
audit: AuditTable;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`UPDATE "asset_exif" SET "rating" = NULL WHERE "rating" = 0;`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// not supported
|
||||
}
|
||||
72
server/src/schema/migrations/1772025522559-AssetOcrSync.ts
Normal file
72
server/src/schema/migrations/1772025522559-AssetOcrSync.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION asset_edit_delete()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE asset
|
||||
SET "isEdited" = false
|
||||
FROM deleted_edit
|
||||
WHERE asset.id = deleted_edit."assetId" AND asset."isEdited"
|
||||
AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id);
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE OR REPLACE FUNCTION asset_ocr_delete_audit()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO asset_ocr_audit ("assetId")
|
||||
SELECT "assetId"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE TABLE "asset_ocr_audit" (
|
||||
"id" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
||||
"assetId" uuid NOT NULL,
|
||||
"deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(),
|
||||
CONSTRAINT "asset_ocr_audit_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "asset_ocr_audit_assetId_idx" ON "asset_ocr_audit" ("assetId");`.execute(db);
|
||||
await sql`CREATE INDEX "asset_ocr_audit_deletedAt_idx" ON "asset_ocr_audit" ("deletedAt");`.execute(db);
|
||||
await sql`ALTER TABLE "asset_ocr" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
|
||||
await sql`CREATE INDEX "asset_ocr_updateId_idx" ON "asset_ocr" ("updateId");`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "asset_ocr_delete_audit"
|
||||
AFTER DELETE ON "asset_ocr"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN (pg_trigger_depth() = 0)
|
||||
EXECUTE FUNCTION asset_ocr_delete_audit();`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_ocr_delete_audit', '{"type":"function","name":"asset_ocr_delete_audit","sql":"CREATE OR REPLACE FUNCTION asset_ocr_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO asset_ocr_audit (\\"assetId\\")\\n SELECT \\"assetId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_ocr_delete_audit', '{"type":"trigger","name":"asset_ocr_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"asset_ocr_delete_audit\\"\\n AFTER DELETE ON \\"asset_ocr\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_ocr_delete_audit();"}'::jsonb);`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"asset_edit_delete","sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\"\\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION public.asset_edit_delete()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
UPDATE asset
|
||||
SET "isEdited" = false
|
||||
FROM deleted_edit
|
||||
WHERE asset.id = deleted_edit."assetId" AND asset."isEdited"
|
||||
AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id);
|
||||
RETURN NULL;
|
||||
END
|
||||
$function$
|
||||
`.execute(db);
|
||||
await sql`DROP TRIGGER "asset_ocr_delete_audit" ON "asset_ocr";`.execute(db);
|
||||
await sql`DROP INDEX "asset_ocr_updateId_idx";`.execute(db);
|
||||
await sql`ALTER TABLE "asset_ocr" DROP COLUMN "updateId";`.execute(db);
|
||||
await sql`DROP TABLE "asset_ocr_audit";`.execute(db);
|
||||
await sql`DROP FUNCTION asset_ocr_delete_audit;`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\" \\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;","name":"asset_edit_delete","type":"function"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_ocr_delete_audit';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_ocr_delete_audit';`.execute(db);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE INDEX "IDX_asset_exif_gist_earthcoord" ON "asset_exif" USING gist (ll_to_earth_public(latitude, longitude));`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_IDX_asset_exif_gist_earthcoord', '{"type":"index","name":"IDX_asset_exif_gist_earthcoord","sql":"CREATE INDEX \\"IDX_asset_exif_gist_earthcoord\\" ON \\"asset_exif\\" USING gist (ll_to_earth_public(latitude, longitude));"}'::jsonb);`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP INDEX "IDX_asset_exif_gist_earthcoord";`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_IDX_asset_exif_gist_earthcoord';`.execute(db);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION asset_edit_delete()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE asset
|
||||
SET "isEdited" = false
|
||||
FROM deleted_edit
|
||||
WHERE asset.id = deleted_edit."assetId" AND asset."isEdited"
|
||||
AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id);
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"asset_edit_delete","sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\"\\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION public.asset_edit_delete()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
UPDATE asset
|
||||
SET "isEdited" = false
|
||||
FROM deleted_edit
|
||||
WHERE asset.id = deleted_edit."assetId" AND asset."isEdited"
|
||||
AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id);
|
||||
RETURN NULL;
|
||||
END
|
||||
$function$
|
||||
`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\" \\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;","name":"asset_edit_delete","type":"function"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE TYPE "asset_checksum_algorithm_enum" AS ENUM ('sha1-file','sha1-path');`.execute(db);
|
||||
await sql`ALTER TABLE "asset" ADD "checksumAlgorithm" asset_checksum_algorithm_enum;`.execute(db);
|
||||
|
||||
// Update in batches to handle millions of rows efficiently
|
||||
const batchSize = 10_000;
|
||||
let updatedRows: number;
|
||||
|
||||
do {
|
||||
const result = await sql`
|
||||
UPDATE "asset"
|
||||
SET "checksumAlgorithm" = CASE
|
||||
WHEN "isExternal" = true THEN 'sha1-path'::asset_checksum_algorithm_enum
|
||||
ELSE 'sha1-file'::asset_checksum_algorithm_enum
|
||||
END
|
||||
WHERE "id" IN (
|
||||
SELECT "id"
|
||||
FROM "asset"
|
||||
WHERE "checksumAlgorithm" IS NULL
|
||||
LIMIT ${batchSize}
|
||||
)
|
||||
`.execute(db);
|
||||
|
||||
updatedRows = Number(result.numAffectedRows ?? 0);
|
||||
} while (updatedRows > 0);
|
||||
|
||||
await sql`ALTER TABLE "asset" ALTER COLUMN "checksumAlgorithm" SET NOT NULL;`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "asset" DROP COLUMN "checksumAlgorithm";`.execute(db);
|
||||
await sql`DROP TYPE "asset_checksum_algorithm_enum";`.execute(db);
|
||||
}
|
||||
@@ -1,23 +1,9 @@
|
||||
import {
|
||||
Column,
|
||||
ForeignKeyColumn,
|
||||
Generated,
|
||||
Index,
|
||||
Int8,
|
||||
Table,
|
||||
Timestamp,
|
||||
UpdateDateColumn,
|
||||
} from '@immich/sql-tools';
|
||||
import { Column, ForeignKeyColumn, Generated, Int8, Table, Timestamp, UpdateDateColumn } from '@immich/sql-tools';
|
||||
import { LockableProperty } from 'src/database';
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
|
||||
@Table('asset_exif')
|
||||
@Index({
|
||||
name: 'IDX_asset_exif_gist_earthcoord',
|
||||
using: 'gist',
|
||||
expression: 'll_to_earth_public(latitude, longitude)',
|
||||
})
|
||||
@UpdatedAtTrigger('asset_exif_updatedAt')
|
||||
export class AssetExifTable {
|
||||
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', primary: true })
|
||||
|
||||
14
server/src/schema/tables/asset-ocr-audit.table.ts
Normal file
14
server/src/schema/tables/asset-ocr-audit.table.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Column, CreateDateColumn, Generated, Table } from '@immich/sql-tools';
|
||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||
|
||||
@Table('asset_ocr_audit')
|
||||
export class AssetOcrAuditTable {
|
||||
@PrimaryGeneratedUuidV7Column()
|
||||
id!: Generated<string>;
|
||||
|
||||
@Column({ type: 'uuid', index: true })
|
||||
assetId!: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
|
||||
deletedAt!: Date;
|
||||
}
|
||||
@@ -1,7 +1,22 @@
|
||||
import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table } from '@immich/sql-tools';
|
||||
import {
|
||||
AfterDeleteTrigger,
|
||||
Column,
|
||||
ForeignKeyColumn,
|
||||
Generated,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
} from '@immich/sql-tools';
|
||||
import { UpdateIdColumn } from 'src/decorators';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { asset_ocr_delete_audit } from '../functions';
|
||||
|
||||
@Table('asset_ocr')
|
||||
@AfterDeleteTrigger({
|
||||
scope: 'statement',
|
||||
function: asset_ocr_delete_audit,
|
||||
referencingOldTableAs: 'old',
|
||||
when: 'pg_trigger_depth() = 0',
|
||||
})
|
||||
export class AssetOcrTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: Generated<string>;
|
||||
@@ -45,4 +60,7 @@ export class AssetOcrTable {
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isVisible!: Generated<boolean>;
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from '@immich/sql-tools';
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum';
|
||||
import { asset_checksum_algorithm_enum, asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
|
||||
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||
import { asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
|
||||
import { asset_delete_audit } from 'src/schema/functions';
|
||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||
import { StackTable } from 'src/schema/tables/stack.table';
|
||||
@@ -98,9 +98,6 @@ export class AssetTable {
|
||||
@Column({ type: 'bytea', index: true })
|
||||
checksum!: Buffer; // sha1 checksum
|
||||
|
||||
@Column({ enum: asset_checksum_algorithm_enum })
|
||||
checksumAlgorithm!: ChecksumAlgorithm;
|
||||
|
||||
@ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
|
||||
livePhotoVideoId!: string | null;
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
AssetStatus,
|
||||
AssetVisibility,
|
||||
CacheControl,
|
||||
ChecksumAlgorithm,
|
||||
JobName,
|
||||
Permission,
|
||||
StorageFolder,
|
||||
@@ -410,7 +409,6 @@ export class AssetMediaService extends BaseService {
|
||||
deviceId: asset.deviceId,
|
||||
type: asset.type,
|
||||
checksum: asset.checksum,
|
||||
checksumAlgorithm: asset.checksumAlgorithm,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
localDateTime: asset.localDateTime,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
@@ -432,7 +430,6 @@ export class AssetMediaService extends BaseService {
|
||||
libraryId: null,
|
||||
|
||||
checksum: file.checksum,
|
||||
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||
originalPath: file.originalPath,
|
||||
|
||||
deviceAssetId: dto.deviceAssetId,
|
||||
|
||||
@@ -516,7 +516,7 @@ export class AssetService extends BaseService {
|
||||
dateTimeOriginal?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
rating?: number | null;
|
||||
rating?: number;
|
||||
}) {
|
||||
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
|
||||
const writes = _.omitBy(
|
||||
|
||||
@@ -17,17 +17,7 @@ import {
|
||||
ValidateLibraryImportPathResponseDto,
|
||||
ValidateLibraryResponseDto,
|
||||
} from 'src/dtos/library.dto';
|
||||
import {
|
||||
AssetStatus,
|
||||
AssetType,
|
||||
ChecksumAlgorithm,
|
||||
CronJob,
|
||||
DatabaseLock,
|
||||
ImmichWorker,
|
||||
JobName,
|
||||
JobStatus,
|
||||
QueueName,
|
||||
} from 'src/enum';
|
||||
import { AssetStatus, AssetType, CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { AssetSyncResult } from 'src/repositories/library.repository';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
@@ -410,7 +400,6 @@ export class LibraryService extends BaseService {
|
||||
ownerId,
|
||||
libraryId,
|
||||
checksum: this.cryptoRepository.hashSha1(`path:${assetPath}`),
|
||||
checksumAlgorithm: ChecksumAlgorithm.sha1Path,
|
||||
originalPath: assetPath,
|
||||
|
||||
fileCreatedAt: stat.mtime,
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
AssetFileType,
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
ChecksumAlgorithm,
|
||||
ExifOrientation,
|
||||
ImmichWorker,
|
||||
JobName,
|
||||
@@ -652,7 +651,6 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||
checksum: expect.any(Buffer),
|
||||
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||
deviceAssetId: 'NONE',
|
||||
deviceId: 'NONE',
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
@@ -706,7 +704,6 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||
checksum: expect.any(Buffer),
|
||||
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||
deviceAssetId: 'NONE',
|
||||
deviceId: 'NONE',
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
@@ -760,7 +757,6 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.storage.readFile).toHaveBeenCalledWith(asset.originalPath, expect.any(Object));
|
||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||
checksum: expect.any(Buffer),
|
||||
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||
deviceAssetId: 'NONE',
|
||||
deviceId: 'NONE',
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
@@ -1427,20 +1423,6 @@ describe(MetadataService.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 0 as unrated -> null', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ Rating: 0 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rating: null,
|
||||
}),
|
||||
{ lockedPropertiesBehavior: 'skip' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle valid negative rating value', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
@@ -1798,28 +1780,6 @@ describe(MetadataService.name, () => {
|
||||
'timeZone',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should write rating', async () => {
|
||||
const asset = factory.jobAssets.sidecarWrite();
|
||||
asset.exifInfo.rating = 4;
|
||||
|
||||
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
|
||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
|
||||
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 4 });
|
||||
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
|
||||
});
|
||||
|
||||
it('should write null rating as 0', async () => {
|
||||
const asset = factory.jobAssets.sidecarWrite();
|
||||
asset.exifInfo.rating = null;
|
||||
|
||||
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
|
||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
|
||||
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 0 });
|
||||
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('firstDateTime', () => {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
AssetFileType,
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
ChecksumAlgorithm,
|
||||
DatabaseLock,
|
||||
ExifOrientation,
|
||||
ImmichWorker,
|
||||
@@ -302,7 +301,7 @@ export class MetadataService extends BaseService {
|
||||
// comments
|
||||
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
|
||||
profileDescription: exifTags.ProfileDescription || null,
|
||||
rating: exifTags.Rating === 0 ? null : validateRange(exifTags.Rating, -1, 5),
|
||||
rating: validateRange(exifTags.Rating, -1, 5),
|
||||
|
||||
// grouping
|
||||
livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,
|
||||
@@ -452,7 +451,7 @@ export class MetadataService extends BaseService {
|
||||
dateTimeOriginal: asset.exifInfo.dateTimeOriginal as string | null,
|
||||
latitude: asset.exifInfo.latitude,
|
||||
longitude: asset.exifInfo.longitude,
|
||||
rating: asset.exifInfo.rating ?? 0,
|
||||
rating: asset.exifInfo.rating,
|
||||
tags: asset.exifInfo.tags,
|
||||
timeZone: asset.exifInfo.timeZone,
|
||||
},
|
||||
@@ -676,7 +675,6 @@ export class MetadataService extends BaseService {
|
||||
fileModifiedAt: stats.mtime,
|
||||
localDateTime: dates.localDateTime,
|
||||
checksum,
|
||||
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||
ownerId: asset.ownerId,
|
||||
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
||||
originalFileName: `${parse(asset.originalFileName).name}.mp4`,
|
||||
|
||||
@@ -9,9 +9,6 @@ import { userStub } from 'test/fixtures/user.stub';
|
||||
import { getForStorageTemplate } from 'test/mappers';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const motionAsset = AssetFactory.from({ type: AssetType.Video }).exif().build();
|
||||
const stillAsset = AssetFactory.from({ livePhotoVideoId: motionAsset.id }).exif().build();
|
||||
|
||||
describe(StorageTemplateService.name, () => {
|
||||
let sut: StorageTemplateService;
|
||||
let mocks: ServiceMocks;
|
||||
@@ -156,58 +153,6 @@ describe(StorageTemplateService.name, () => {
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, originalPath: newMotionPicturePath });
|
||||
});
|
||||
|
||||
it('should migrate live photo motion video alongside the still image using album in path', async () => {
|
||||
const motionAsset = AssetFactory.from({
|
||||
type: AssetType.Video,
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
const stillAsset = AssetFactory.from({
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
|
||||
const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/${album.albumName}/${stillAsset.originalFileName.slice(0, -4)}.mp4`;
|
||||
const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/${album.albumName}/${stillAsset.originalFileName}`;
|
||||
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(stillAsset));
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
||||
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
entityId: stillAsset.id,
|
||||
pathType: AssetPathType.Original,
|
||||
oldPath: stillAsset.originalPath,
|
||||
newPath: newStillPicturePath,
|
||||
});
|
||||
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '124',
|
||||
entityId: motionAsset.id,
|
||||
pathType: AssetPathType.Original,
|
||||
oldPath: motionAsset.originalPath,
|
||||
newPath: newMotionPicturePath,
|
||||
});
|
||||
|
||||
await expect(sut.handleMigrationSingle({ id: stillAsset.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.album.getByAssetId).toHaveBeenCalledWith(stillAsset.ownerId, stillAsset.id);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: stillAsset.id, originalPath: newStillPicturePath });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, originalPath: newMotionPicturePath });
|
||||
});
|
||||
|
||||
it('should use handlebar if condition for album', async () => {
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from().owner(user).exif().build();
|
||||
@@ -764,18 +709,12 @@ describe(StorageTemplateService.name, () => {
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/${album.albumName}/${stillAsset.originalFileName.slice(0, -4)}.mp4`;
|
||||
const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/${album.albumName}/${stillAsset.originalFileName}`;
|
||||
const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName.slice(0, -4)}.mp4`;
|
||||
const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`;
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)]));
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
||||
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
@@ -796,53 +735,11 @@ describe(StorageTemplateService.name, () => {
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
|
||||
expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(motionAsset.id);
|
||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: stillAsset.id, originalPath: newStillPicturePath });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, originalPath: newMotionPicturePath });
|
||||
});
|
||||
|
||||
it('should use still photo album info when migrating live photo motion video', async () => {
|
||||
const user = userStub.user1;
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other{{/if}}/{{filename}}';
|
||||
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)]));
|
||||
mocks.user.getList.mockResolvedValue([user]);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
||||
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
entityId: stillAsset.id,
|
||||
pathType: AssetPathType.Original,
|
||||
oldPath: stillAsset.originalPath,
|
||||
newPath: `/data/library/${user.id}/2022/${album.albumName}/${stillAsset.originalFileName}`,
|
||||
});
|
||||
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '124',
|
||||
entityId: motionAsset.id,
|
||||
pathType: AssetPathType.Original,
|
||||
oldPath: motionAsset.originalPath,
|
||||
newPath: `/data/library/${user.id}/2022/${album.albumName}/${motionAsset.originalFileName}`,
|
||||
});
|
||||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(mocks.album.getByAssetId).toHaveBeenCalledWith(stillAsset.ownerId, stillAsset.id);
|
||||
expect(mocks.album.getByAssetId).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: stillAsset.id,
|
||||
originalPath: expect.stringContaining(`/${album.albumName}/`),
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: motionAsset.id,
|
||||
originalPath: expect.stringContaining(`/${album.albumName}/`),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('file rename correctness', () => {
|
||||
|
||||
@@ -158,14 +158,12 @@ export class StorageTemplateService extends BaseService {
|
||||
|
||||
// move motion part of live photo
|
||||
if (asset.livePhotoVideoId) {
|
||||
const livePhotoVideo = await this.assetJobRepository.getForStorageTemplateJob(asset.livePhotoVideoId, {
|
||||
includeHidden: true,
|
||||
});
|
||||
const livePhotoVideo = await this.assetJobRepository.getForStorageTemplateJob(asset.livePhotoVideoId);
|
||||
if (!livePhotoVideo) {
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath);
|
||||
await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename }, asset);
|
||||
await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename });
|
||||
}
|
||||
return JobStatus.Success;
|
||||
}
|
||||
@@ -193,12 +191,10 @@ export class StorageTemplateService extends BaseService {
|
||||
|
||||
// move motion part of live photo
|
||||
if (asset.livePhotoVideoId) {
|
||||
const livePhotoVideo = await this.assetJobRepository.getForStorageTemplateJob(asset.livePhotoVideoId, {
|
||||
includeHidden: true,
|
||||
});
|
||||
const livePhotoVideo = await this.assetJobRepository.getForStorageTemplateJob(asset.livePhotoVideoId);
|
||||
if (livePhotoVideo) {
|
||||
const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath);
|
||||
await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename }, asset);
|
||||
await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,7 +214,7 @@ export class StorageTemplateService extends BaseService {
|
||||
await this.moveRepository.cleanMoveHistorySingle(assetId);
|
||||
}
|
||||
|
||||
async moveAsset(asset: StorageAsset, metadata: MoveAssetMetadata, stillPhoto?: StorageAsset) {
|
||||
async moveAsset(asset: StorageAsset, metadata: MoveAssetMetadata) {
|
||||
if (asset.isExternal || StorageCore.isAndroidMotionPath(asset.originalPath)) {
|
||||
// External assets are not affected by storage template
|
||||
// TODO: shouldn't this only apply to external assets?
|
||||
@@ -228,7 +224,7 @@ export class StorageTemplateService extends BaseService {
|
||||
return this.databaseRepository.withLock(DatabaseLock.StorageTemplateMigration, async () => {
|
||||
const { id, originalPath, checksum, fileSizeInByte } = asset;
|
||||
const oldPath = originalPath;
|
||||
const newPath = await this.getTemplatePath(asset, metadata, stillPhoto);
|
||||
const newPath = await this.getTemplatePath(asset, metadata);
|
||||
|
||||
if (!fileSizeInByte) {
|
||||
this.logger.error(`Asset ${id} missing exif info, skipping storage template migration`);
|
||||
@@ -259,11 +255,7 @@ export class StorageTemplateService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
private async getTemplatePath(
|
||||
asset: StorageAsset,
|
||||
metadata: MoveAssetMetadata,
|
||||
stillPhoto?: StorageAsset,
|
||||
): Promise<string> {
|
||||
private async getTemplatePath(asset: StorageAsset, metadata: MoveAssetMetadata): Promise<string> {
|
||||
const { storageLabel, filename } = metadata;
|
||||
|
||||
try {
|
||||
@@ -304,12 +296,8 @@ export class StorageTemplateService extends BaseService {
|
||||
let albumName = null;
|
||||
let albumStartDate = null;
|
||||
let albumEndDate = null;
|
||||
const assetForMetadata = stillPhoto || asset;
|
||||
|
||||
if (this.template.needsAlbum) {
|
||||
// For motion videos, use the still photo's album information since motion videos
|
||||
// don't have album metadata attached directly
|
||||
const albums = await this.albumRepository.getByAssetId(assetForMetadata.ownerId, assetForMetadata.id);
|
||||
const albums = await this.albumRepository.getByAssetId(asset.ownerId, asset.id);
|
||||
const album = albums?.[0];
|
||||
if (album) {
|
||||
albumName = album.albumName || null;
|
||||
@@ -322,18 +310,16 @@ export class StorageTemplateService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
// For motion videos that are part of live photos, use the still photo's date
|
||||
// to ensure both parts end up in the same folder
|
||||
const storagePath = this.render(this.template.compiled, {
|
||||
asset: assetForMetadata,
|
||||
asset,
|
||||
filename: sanitized,
|
||||
extension,
|
||||
albumName,
|
||||
albumStartDate,
|
||||
albumEndDate,
|
||||
make: assetForMetadata.make,
|
||||
model: assetForMetadata.model,
|
||||
lensModel: assetForMetadata.lensModel,
|
||||
make: asset.make,
|
||||
model: asset.model,
|
||||
lensModel: asset.lensModel,
|
||||
});
|
||||
const fullPath = path.normalize(path.join(rootPath, storagePath));
|
||||
let destination = `${fullPath}.${extension}`;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/com
|
||||
import { Insertable } from 'kysely';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { Writable } from 'node:stream';
|
||||
import { ConnectableObservable } from 'rxjs';
|
||||
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
||||
import { OnJob } from 'src/decorators';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
@@ -81,6 +82,7 @@ export const SYNC_TYPES_ORDER = [
|
||||
SyncRequestType.AlbumToAssetsV1,
|
||||
SyncRequestType.AssetExifsV1,
|
||||
SyncRequestType.AlbumAssetExifsV1,
|
||||
SyncRequestType.AssetOcrV1,
|
||||
SyncRequestType.PartnerAssetExifsV1,
|
||||
SyncRequestType.MemoriesV1,
|
||||
SyncRequestType.MemoryToAssetsV1,
|
||||
@@ -195,6 +197,7 @@ export class SyncService extends BaseService {
|
||||
[SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetFacesV2]: async () => this.syncAssetFacesV2(options, response, checkpointMap),
|
||||
[SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetOcrV1]: () => this.syncAssetOcrV1(options, response, checkpointMap, auth),
|
||||
};
|
||||
|
||||
for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) {
|
||||
@@ -225,6 +228,7 @@ export class SyncService extends BaseService {
|
||||
await this.syncRepository.stack.cleanupAuditTable(pruneThreshold);
|
||||
await this.syncRepository.user.cleanupAuditTable(pruneThreshold);
|
||||
await this.syncRepository.userMetadata.cleanupAuditTable(pruneThreshold);
|
||||
await this.syncRepository.assetOcr.cleanupAuditTable(pruneThreshold);
|
||||
}
|
||||
|
||||
private needsFullSync(checkpointMap: CheckpointMap) {
|
||||
@@ -873,6 +877,33 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAssetOcrV1(
|
||||
options: SyncQueryOptions,
|
||||
response: Writable,
|
||||
checkpointMap: CheckpointMap,
|
||||
auth: AuthDto,
|
||||
) {
|
||||
const deleteType = SyncEntityType.AssetOcrDeleteV1;
|
||||
const deletes = this.syncRepository.assetOcr.getDeletes(
|
||||
{ ...options, ack: checkpointMap[deleteType] },
|
||||
auth.user.id,
|
||||
);
|
||||
|
||||
for await (const row of deletes) {
|
||||
send(response, { type: deleteType, ids: [row.id], data: row });
|
||||
}
|
||||
|
||||
const upsertType = SyncEntityType.AssetOcrV1;
|
||||
const upserts = this.syncRepository.assetOcr.getUpserts(
|
||||
{ ...options, ack: checkpointMap[upsertType] },
|
||||
auth.user.id,
|
||||
);
|
||||
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
private async upsertBackfillCheckpoint(item: { type: SyncEntityType; sessionId: string; createId: string }) {
|
||||
const { type, sessionId, createId } = item;
|
||||
await this.syncCheckpointRepository.upsertAll([
|
||||
|
||||
@@ -23,24 +23,6 @@ describe(TimelineService.name, () => {
|
||||
userIds: [authStub.admin.user.id],
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass bbox options to repository when all bbox fields are provided', async () => {
|
||||
mocks.asset.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]);
|
||||
|
||||
await sut.getTimeBuckets(authStub.admin, {
|
||||
bbox: {
|
||||
west: -70,
|
||||
south: -30,
|
||||
east: 120,
|
||||
north: 55,
|
||||
},
|
||||
});
|
||||
|
||||
expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith({
|
||||
userIds: [authStub.admin.user.id],
|
||||
bbox: { west: -70, south: -30, east: 120, north: 55 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimeBucket', () => {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiPropertyOptions } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { IsNotEmpty, ValidateNested } from 'class-validator';
|
||||
import { Property } from 'src/decorators';
|
||||
import { BBoxDto } from 'src/dtos/bbox.dto';
|
||||
import { Optional } from 'src/validation';
|
||||
|
||||
type BBoxOptions = { optional?: boolean };
|
||||
export const ValidateBBox = (options: BBoxOptions & ApiPropertyOptions = {}) => {
|
||||
const { optional, ...apiPropertyOptions } = options;
|
||||
|
||||
return applyDecorators(
|
||||
Transform(({ value }) => {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
const [west, south, east, north] = value.split(',', 4).map(Number);
|
||||
return Object.assign(new BBoxDto(), { west, south, east, north });
|
||||
}),
|
||||
Type(() => BBoxDto),
|
||||
ValidateNested(),
|
||||
Property({
|
||||
type: 'string',
|
||||
description: 'Bounding box coordinates as west,south,east,north (WGS84)',
|
||||
example: '11.075683,49.416711,11.117589,49.454875',
|
||||
...apiPropertyOptions,
|
||||
}),
|
||||
optional ? Optional({}) : IsNotEmpty(),
|
||||
);
|
||||
};
|
||||
@@ -427,25 +427,3 @@ export function IsIPRange(options: IsIPRangeOptions, validationOptions?: Validat
|
||||
validationOptions,
|
||||
);
|
||||
}
|
||||
|
||||
@ValidatorConstraint({ name: 'isGreaterThanOrEqualTo' })
|
||||
export class IsGreaterThanOrEqualToConstraint implements ValidatorConstraintInterface {
|
||||
validate(value: unknown, args: ValidationArguments) {
|
||||
const relatedPropertyName = args.constraints?.[0] as string;
|
||||
const relatedValue = (args.object as Record<string, unknown>)[relatedPropertyName];
|
||||
if (!Number.isFinite(value) || !Number.isFinite(relatedValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Number(value) >= Number(relatedValue);
|
||||
}
|
||||
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
const relatedPropertyName = args.constraints?.[0] as string;
|
||||
return `${args.property} must be greater than or equal to ${relatedPropertyName}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const IsGreaterThanOrEqualTo = (property: string, validationOptions?: ValidationOptions) => {
|
||||
return Validate(IsGreaterThanOrEqualToConstraint, [property], validationOptions);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user