mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:06:56 -04:00
Merge branch 'main' of https://github.com/Pranav-8bit/immich into fix/user-removal-from-option-menu-on-the-top-in-shared-album
This commit is contained in:
commit
6c505334a8
@ -33,6 +33,7 @@
|
|||||||
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
|
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
|
||||||
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
|
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
|
||||||
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
||||||
|
<a href="readme_i18n/README_vi_VN.md">Tiếng Việt</a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.20",
|
"version": "2.2.22",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.20",
|
"version": "2.2.22",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
@ -52,7 +52,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.20",
|
"version": "2.2.22",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
|
@ -187,7 +187,7 @@ However, when the trash is emptied, the files will re-appear in the main timelin
|
|||||||
|
|
||||||
### How does smart search work?
|
### How does smart search work?
|
||||||
|
|
||||||
Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
|
Immich uses CLIP models. An ML model converts each image to an "embedding", which is essentially a string of numbers that semantically encodes what is in the image. The same is done for the text that you enter when you do a search, and that text embedding is then compared with those of the images to find similar ones. As such, there are no "tags", "labels", or "descriptions" generated that you can look at. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
|
||||||
|
|
||||||
### How does facial recognition work?
|
### How does facial recognition work?
|
||||||
|
|
||||||
@ -333,7 +333,11 @@ You may need to add mount points or docker volumes for the following internal co
|
|||||||
- `immich-machine-learning:/.cache`
|
- `immich-machine-learning:/.cache`
|
||||||
- `redis:/data`
|
- `redis:/data`
|
||||||
|
|
||||||
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`.
|
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION` and `/cache` for machine-learning.
|
||||||
|
|
||||||
|
:::note Docker Compose Volumes
|
||||||
|
The Docker Compose top level volume element does not support non-root access, all of the above volumes must be local volume mounts.
|
||||||
|
:::
|
||||||
|
|
||||||
For a further hardened system, you can add the following block to every container except for `immich_postgres`.
|
For a further hardened system, you can add the following block to every container except for `immich_postgres`.
|
||||||
|
|
||||||
|
8
docs/static/archived-versions.json
vendored
8
docs/static/archived-versions.json
vendored
@ -1,4 +1,12 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.116.2",
|
||||||
|
"url": "https://v1.116.2.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.116.1",
|
||||||
|
"url": "https://v1.116.1.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.116.0",
|
"label": "v1.116.0",
|
||||||
"url": "https://v1.116.0.archive.immich.app"
|
"url": "https://v1.116.0.archive.immich.app"
|
||||||
|
8
e2e/package-lock.json
generated
8
e2e/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
@ -45,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
"../cli": {
|
"../cli": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.20",
|
"version": "2.2.22",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -92,7 +92,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
ARG DEVICE=cpu
|
ARG DEVICE=cpu
|
||||||
|
|
||||||
FROM python:3.11-bookworm@sha256:157a371e60389919fe4a72dff71ce86eaa5234f59114c23b0b346d0d02c74d39 AS builder-cpu
|
FROM python:3.11-bookworm@sha256:e456ff58048f52f121025159e68bf16248c4122c8b96fadffd89331df50c9994 AS builder-cpu
|
||||||
|
|
||||||
FROM builder-cpu AS builder-openvino
|
FROM builder-cpu AS builder-openvino
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
|
|||||||
COPY poetry.lock pyproject.toml ./
|
COPY poetry.lock pyproject.toml ./
|
||||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||||
|
|
||||||
FROM python:3.11-slim-bookworm@sha256:669bbd08353610485a94d5d0c976b4b6498c55280fe42c00f7581f85ee9f3121 AS prod-cpu
|
FROM python:3.11-slim-bookworm@sha256:585cf0799407efc267fe1cce318322ec26e015ac1b3d77f2517d50bc3acfc232 AS prod-cpu
|
||||||
|
|
||||||
FROM prod-cpu AS prod-openvino
|
FROM prod-cpu AS prod-openvino
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM mambaorg/micromamba:bookworm-slim@sha256:5f32c5742e2248f2ca07ccae6861371321aba37372bf8e1a80d6f728f1ab4418 AS builder
|
FROM mambaorg/micromamba:bookworm-slim@sha256:e3797091302382ea841498bc93a7b0a50f7c1448333d5e946d2d1608d0c5f43d AS builder
|
||||||
|
|
||||||
ENV TRANSFORMERS_CACHE=/cache \
|
ENV TRANSFORMERS_CACHE=/cache \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
67
machine-learning/poetry.lock
generated
67
machine-learning/poetry.lock
generated
@ -680,13 +680,13 @@ test = ["pytest (>=6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-slim"
|
name = "fastapi-slim"
|
||||||
version = "0.114.2"
|
version = "0.115.0"
|
||||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi_slim-0.114.2-py3-none-any.whl", hash = "sha256:52ae76c53a30ad0fa96beb84c1bf4bef9c72e88c2f7c0473e836f01d7ac3ca6b"},
|
{file = "fastapi_slim-0.115.0-py3-none-any.whl", hash = "sha256:27ab44da95b622e68be7a19f06df1960a320b9d94e689b0adfc055bb26ee9be7"},
|
||||||
{file = "fastapi_slim-0.114.2.tar.gz", hash = "sha256:76d0a450826fb0fa740268be55ef04c44807da87a94fbbf5f16338b5a4a2d321"},
|
{file = "fastapi_slim-0.115.0.tar.gz", hash = "sha256:b4b962ca2aa0a31010dafdad3d4da99d368a5591223304c6fb385712fad7feb6"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -2037,22 +2037,22 @@ reference = "cuda12"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime-openvino"
|
name = "onnxruntime-openvino"
|
||||||
version = "1.18.0"
|
version = "1.19.0"
|
||||||
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "onnxruntime_openvino-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:565b874d21bcd48126da7d62f57db019f5ec0e1f82ae9b0740afa2ad91f8d331"},
|
{file = "onnxruntime_openvino-1.19.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8c5658da819b26d9f35f95204e1bdfb74a100a7533e74edab3af6316c1e316e8"},
|
||||||
{file = "onnxruntime_openvino-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f1931060f710a6c8e32121bb73044c4772ef5925802fc8776d3fe1e87ab3f75"},
|
{file = "onnxruntime_openvino-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb8de2a60cf78db6e201b0a489479995d166938e9c53b01ff342dc7f5f8251ff"},
|
||||||
{file = "onnxruntime_openvino-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb1723d386f70a8e26398d983ebe35d2c25ba56e9cdb382670ebbf1f5139f8ba"},
|
{file = "onnxruntime_openvino-1.19.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f3a0b954026286421b3a769c746c403e8f141f3887d1dd601beb7c4dbf77488a"},
|
||||||
{file = "onnxruntime_openvino-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:874a1e263dd86674593e5a879257650b06a8609c4d5768c3d8ed8dc4ae874b9c"},
|
{file = "onnxruntime_openvino-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:12330922ecdb694ea28dbdcf08c172e47a5a84fee603040691341336ee3e42bc"},
|
||||||
{file = "onnxruntime_openvino-1.18.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:597eb18f3de7ead69b08a242d74c4573b28bbfba40ca2a1a40f75bf7a834808e"},
|
{file = "onnxruntime_openvino-1.19.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:be00502b1a46ba1891cbe49049033745f71c0b99df6d24b979f5b4084b9567d0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
coloredlogs = "*"
|
coloredlogs = "*"
|
||||||
flatbuffers = "*"
|
flatbuffers = "*"
|
||||||
numpy = ">=1.26.4"
|
numpy = ">=1.21.6"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
protobuf = "*"
|
protobuf = "*"
|
||||||
sympy = "*"
|
sympy = "*"
|
||||||
@ -2576,18 +2576,15 @@ cli = ["click (>=5.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.9"
|
version = "0.0.10"
|
||||||
description = "A streaming multipart parser for Python"
|
description = "A streaming multipart parser for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"},
|
{file = "python_multipart-0.0.10-py3-none-any.whl", hash = "sha256:2b06ad9e8d50c7a8db80e3b56dab590137b323410605af2be20d62a5f1ba1dc8"},
|
||||||
{file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"},
|
{file = "python_multipart-0.0.10.tar.gz", hash = "sha256:46eb3c6ce6fdda5fb1a03c7e11d490e407c6930a2703fe7aef4da71c374688fa"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pywin32"
|
name = "pywin32"
|
||||||
version = "306"
|
version = "306"
|
||||||
@ -2834,29 +2831,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.6.6"
|
version = "0.6.8"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.6.6-py3-none-linux_armv6l.whl", hash = "sha256:f5bc5398457484fc0374425b43b030e4668ed4d2da8ee7fdda0e926c9f11ccfb"},
|
{file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"},
|
||||||
{file = "ruff-0.6.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:515a698254c9c47bb84335281a170213b3ee5eb47feebe903e1be10087a167ce"},
|
{file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"},
|
||||||
{file = "ruff-0.6.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6bb1b4995775f1837ab70f26698dd73852bbb82e8f70b175d2713c0354fe9182"},
|
{file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"},
|
||||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c546f412dfae8bb9cc4f27f0e45cdd554e42fecbb34f03312b93368e1cd0a6"},
|
{file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"},
|
||||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59627e97364329e4eae7d86fa7980c10e2b129e2293d25c478ebcb861b3e3fd6"},
|
{file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"},
|
||||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94c3f78c3d32190aafbb6bc5410c96cfed0a88aadb49c3f852bbc2aa9783a7d8"},
|
{file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"},
|
||||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:704da526c1e137f38c8a067a4a975fe6834b9f8ba7dbc5fd7503d58148851b8f"},
|
{file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"},
|
||||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efeede5815a24104579a0f6320660536c5ffc1c91ae94f8c65659af915fb9de9"},
|
{file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"},
|
||||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e368aef0cc02ca3593eae2fb8186b81c9c2b3f39acaaa1108eb6b4d04617e61f"},
|
{file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"},
|
||||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2653fc3b2a9315bd809725c88dd2446550099728d077a04191febb5ea79a4f79"},
|
{file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"},
|
||||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bb858cd9ce2d062503337c5b9784d7b583bcf9d1a43c4df6ccb5eab774fbafcb"},
|
{file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"},
|
||||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:488f8e15c01ea9afb8c0ba35d55bd951f484d0c1b7c5fd746ce3c47ccdedce68"},
|
{file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"},
|
||||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:aefb0bd15f1cfa4c9c227b6120573bb3d6c4ee3b29fb54a5ad58f03859bc43c6"},
|
{file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"},
|
||||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a4c0698cc780bcb2c61496cbd56b6a3ac0ad858c966652f7dbf4ceb029252fbe"},
|
{file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"},
|
||||||
{file = "ruff-0.6.6-py3-none-win32.whl", hash = "sha256:aadf81ddc8ab5b62da7aae78a91ec933cbae9f8f1663ec0325dae2c364e4ad84"},
|
{file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"},
|
||||||
{file = "ruff-0.6.6-py3-none-win_amd64.whl", hash = "sha256:0adb801771bc1f1b8cf4e0a6fdc30776e7c1894810ff3b344e50da82ef50eeb1"},
|
{file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"},
|
||||||
{file = "ruff-0.6.6-py3-none-win_arm64.whl", hash = "sha256:4b4d32c137bc781c298964dd4e52f07d6f7d57c03eae97a72d97856844aa510a"},
|
{file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"},
|
||||||
{file = "ruff-0.6.6.tar.gz", hash = "sha256:0fc030b6fd14814d69ac0196396f6761921bd20831725c7361e1b8100b818034"},
|
{file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.116.0"
|
version = "1.116.2"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 160,
|
"android.injected.version.code" => 161,
|
||||||
"android.injected.version.name" => "1.116.0",
|
"android.injected.version.name" => "1.116.2",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
@ -401,7 +401,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 175;
|
CURRENT_PROJECT_VERSION = 177;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -543,7 +543,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 175;
|
CURRENT_PROJECT_VERSION = 177;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -571,7 +571,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 175;
|
CURRENT_PROJECT_VERSION = 177;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
@ -58,11 +58,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.115.0</string>
|
<string>1.116.1</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>175</string>
|
<string>177</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Release"
|
desc "iOS Release"
|
||||||
lane :release do
|
lane :release do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.116.0"
|
version_number: "1.116.2"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
@ -4,4 +4,7 @@ abstract interface class IAssetMediaRepository {
|
|||||||
Future<List<String>> deleteAll(List<String> ids);
|
Future<List<String>> deleteAll(List<String> ids);
|
||||||
|
|
||||||
Future<Asset?> get(String id);
|
Future<Asset?> get(String id);
|
||||||
|
|
||||||
|
/// Obtaining the correct original filename of the asset
|
||||||
|
Future<String?> getOriginalFilename(String id);
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,8 @@ class CropImagePage extends HookWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
backgroundColor: context.scaffoldBackgroundColor,
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
body: LayoutBuilder(
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@ -153,6 +154,7 @@ class CropImagePage extends HookWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,12 +86,16 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
void handleAppPause() {
|
void handleAppPause() {
|
||||||
state = AppLifeCycleEnum.paused;
|
state = AppLifeCycleEnum.paused;
|
||||||
_wasPaused = true;
|
_wasPaused = true;
|
||||||
|
|
||||||
|
if (_ref.read(authenticationProvider).isAuthenticated) {
|
||||||
// Do not cancel backup if manual upload is in progress
|
// Do not cancel backup if manual upload is in progress
|
||||||
if (_ref.read(backupProvider.notifier).backupProgress !=
|
if (_ref.read(backupProvider.notifier).backupProgress !=
|
||||||
BackUpProgressEnum.manualInProgress) {
|
BackUpProgressEnum.manualInProgress) {
|
||||||
_ref.read(backupProvider.notifier).cancelBackup();
|
_ref.read(backupProvider.notifier).cancelBackup();
|
||||||
}
|
}
|
||||||
_ref.read(websocketProvider.notifier).disconnect();
|
_ref.read(websocketProvider.notifier).disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
ImmichLogger().flush();
|
ImmichLogger().flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,4 +43,17 @@ class AssetMediaRepository implements IAssetMediaRepository {
|
|||||||
asset.local = local;
|
asset.local = local;
|
||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getOriginalFilename(String id) async {
|
||||||
|
final entity = await AssetEntity.fromId(id);
|
||||||
|
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// titleAsync gets the correct original filename for some assets on iOS
|
||||||
|
// otherwise using the `entity.title` would return a random GUID
|
||||||
|
return await entity.titleAsync;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
|||||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
@ -368,6 +369,7 @@ class BackgroundService {
|
|||||||
BackupRepository backupAlbumRepository = BackupRepository(db);
|
BackupRepository backupAlbumRepository = BackupRepository(db);
|
||||||
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
||||||
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
||||||
|
AssetMediaRepository assetMediaRepository = AssetMediaRepository();
|
||||||
UserRepository userRepository = UserRepository(db);
|
UserRepository userRepository = UserRepository(db);
|
||||||
UserApiRepository userApiRepository =
|
UserApiRepository userApiRepository =
|
||||||
UserApiRepository(apiService.usersApi);
|
UserApiRepository(apiService.usersApi);
|
||||||
@ -409,6 +411,7 @@ class BackgroundService {
|
|||||||
albumService,
|
albumService,
|
||||||
albumMediaRepository,
|
albumMediaRepository,
|
||||||
fileMediaRepository,
|
fileMediaRepository,
|
||||||
|
assetMediaRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
||||||
|
@ -12,6 +12,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
|
|||||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||||
@ -21,6 +22,7 @@ import 'package:immich_mobile/providers/api.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
@ -40,6 +42,7 @@ final backupServiceProvider = Provider(
|
|||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
ref.watch(albumMediaRepositoryProvider),
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
ref.watch(fileMediaRepositoryProvider),
|
ref.watch(fileMediaRepositoryProvider),
|
||||||
|
ref.watch(assetMediaRepositoryProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -52,6 +55,7 @@ class BackupService {
|
|||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
final IAlbumMediaRepository _albumMediaRepository;
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
final IFileMediaRepository _fileMediaRepository;
|
final IFileMediaRepository _fileMediaRepository;
|
||||||
|
final IAssetMediaRepository _assetMediaRepository;
|
||||||
|
|
||||||
BackupService(
|
BackupService(
|
||||||
this._apiService,
|
this._apiService,
|
||||||
@ -60,6 +64,7 @@ class BackupService {
|
|||||||
this._albumService,
|
this._albumService,
|
||||||
this._albumMediaRepository,
|
this._albumMediaRepository,
|
||||||
this._fileMediaRepository,
|
this._fileMediaRepository,
|
||||||
|
this._assetMediaRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<List<String>?> getDeviceBackupAsset() async {
|
Future<List<String>?> getDeviceBackupAsset() async {
|
||||||
@ -329,7 +334,9 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
String originalFileName = asset.fileName;
|
String? originalFileName =
|
||||||
|
await _assetMediaRepository.getOriginalFilename(asset.localId!);
|
||||||
|
originalFileName ??= asset.fileName;
|
||||||
|
|
||||||
if (asset.local!.isLivePhoto) {
|
if (asset.local!.isLivePhoto) {
|
||||||
if (livePhotoFile == null) {
|
if (livePhotoFile == null) {
|
||||||
|
3
mobile/openapi/README.md
generated
3
mobile/openapi/README.md
generated
@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.116.0
|
- API version: 1.116.2
|
||||||
- Generator version: 7.8.0
|
- Generator version: 7.8.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
@ -416,6 +416,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [SystemConfigDto](doc//SystemConfigDto.md)
|
- [SystemConfigDto](doc//SystemConfigDto.md)
|
||||||
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
|
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
|
||||||
- [SystemConfigFacesDto](doc//SystemConfigFacesDto.md)
|
- [SystemConfigFacesDto](doc//SystemConfigFacesDto.md)
|
||||||
|
- [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md)
|
||||||
- [SystemConfigImageDto](doc//SystemConfigImageDto.md)
|
- [SystemConfigImageDto](doc//SystemConfigImageDto.md)
|
||||||
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
|
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
|
||||||
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
|
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
|
||||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -229,6 +229,7 @@ part 'model/stack_update_dto.dart';
|
|||||||
part 'model/system_config_dto.dart';
|
part 'model/system_config_dto.dart';
|
||||||
part 'model/system_config_f_fmpeg_dto.dart';
|
part 'model/system_config_f_fmpeg_dto.dart';
|
||||||
part 'model/system_config_faces_dto.dart';
|
part 'model/system_config_faces_dto.dart';
|
||||||
|
part 'model/system_config_generated_image_dto.dart';
|
||||||
part 'model/system_config_image_dto.dart';
|
part 'model/system_config_image_dto.dart';
|
||||||
part 'model/system_config_job_dto.dart';
|
part 'model/system_config_job_dto.dart';
|
||||||
part 'model/system_config_library_dto.dart';
|
part 'model/system_config_library_dto.dart';
|
||||||
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@ -512,6 +512,8 @@ class ApiClient {
|
|||||||
return SystemConfigFFmpegDto.fromJson(value);
|
return SystemConfigFFmpegDto.fromJson(value);
|
||||||
case 'SystemConfigFacesDto':
|
case 'SystemConfigFacesDto':
|
||||||
return SystemConfigFacesDto.fromJson(value);
|
return SystemConfigFacesDto.fromJson(value);
|
||||||
|
case 'SystemConfigGeneratedImageDto':
|
||||||
|
return SystemConfigGeneratedImageDto.fromJson(value);
|
||||||
case 'SystemConfigImageDto':
|
case 'SystemConfigImageDto':
|
||||||
return SystemConfigImageDto.fromJson(value);
|
return SystemConfigImageDto.fromJson(value);
|
||||||
case 'SystemConfigJobDto':
|
case 'SystemConfigJobDto':
|
||||||
|
118
mobile/openapi/lib/model/system_config_generated_image_dto.dart
generated
Normal file
118
mobile/openapi/lib/model/system_config_generated_image_dto.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 SystemConfigGeneratedImageDto {
|
||||||
|
/// Returns a new [SystemConfigGeneratedImageDto] instance.
|
||||||
|
SystemConfigGeneratedImageDto({
|
||||||
|
required this.format,
|
||||||
|
required this.quality,
|
||||||
|
required this.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageFormat format;
|
||||||
|
|
||||||
|
/// Minimum value: 1
|
||||||
|
/// Maximum value: 100
|
||||||
|
int quality;
|
||||||
|
|
||||||
|
/// Minimum value: 1
|
||||||
|
int size;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedImageDto &&
|
||||||
|
other.format == format &&
|
||||||
|
other.quality == quality &&
|
||||||
|
other.size == size;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(format.hashCode) +
|
||||||
|
(quality.hashCode) +
|
||||||
|
(size.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SystemConfigGeneratedImageDto[format=$format, quality=$quality, size=$size]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'format'] = this.format;
|
||||||
|
json[r'quality'] = this.quality;
|
||||||
|
json[r'size'] = this.size;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SystemConfigGeneratedImageDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SystemConfigGeneratedImageDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SystemConfigGeneratedImageDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SystemConfigGeneratedImageDto(
|
||||||
|
format: ImageFormat.fromJson(json[r'format'])!,
|
||||||
|
quality: mapValueOfType<int>(json, r'quality')!,
|
||||||
|
size: mapValueOfType<int>(json, r'size')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SystemConfigGeneratedImageDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SystemConfigGeneratedImageDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SystemConfigGeneratedImageDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SystemConfigGeneratedImageDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SystemConfigGeneratedImageDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SystemConfigGeneratedImageDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SystemConfigGeneratedImageDto-objects as value to a dart map
|
||||||
|
static Map<String, List<SystemConfigGeneratedImageDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SystemConfigGeneratedImageDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SystemConfigGeneratedImageDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'format',
|
||||||
|
'quality',
|
||||||
|
'size',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -15,64 +15,42 @@ class SystemConfigImageDto {
|
|||||||
SystemConfigImageDto({
|
SystemConfigImageDto({
|
||||||
required this.colorspace,
|
required this.colorspace,
|
||||||
required this.extractEmbedded,
|
required this.extractEmbedded,
|
||||||
required this.previewFormat,
|
required this.preview,
|
||||||
required this.previewSize,
|
required this.thumbnail,
|
||||||
required this.quality,
|
|
||||||
required this.thumbnailFormat,
|
|
||||||
required this.thumbnailSize,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Colorspace colorspace;
|
Colorspace colorspace;
|
||||||
|
|
||||||
bool extractEmbedded;
|
bool extractEmbedded;
|
||||||
|
|
||||||
ImageFormat previewFormat;
|
SystemConfigGeneratedImageDto preview;
|
||||||
|
|
||||||
/// Minimum value: 1
|
SystemConfigGeneratedImageDto thumbnail;
|
||||||
int previewSize;
|
|
||||||
|
|
||||||
/// Minimum value: 1
|
|
||||||
/// Maximum value: 100
|
|
||||||
int quality;
|
|
||||||
|
|
||||||
ImageFormat thumbnailFormat;
|
|
||||||
|
|
||||||
/// Minimum value: 1
|
|
||||||
int thumbnailSize;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto &&
|
||||||
other.colorspace == colorspace &&
|
other.colorspace == colorspace &&
|
||||||
other.extractEmbedded == extractEmbedded &&
|
other.extractEmbedded == extractEmbedded &&
|
||||||
other.previewFormat == previewFormat &&
|
other.preview == preview &&
|
||||||
other.previewSize == previewSize &&
|
other.thumbnail == thumbnail;
|
||||||
other.quality == quality &&
|
|
||||||
other.thumbnailFormat == thumbnailFormat &&
|
|
||||||
other.thumbnailSize == thumbnailSize;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(colorspace.hashCode) +
|
(colorspace.hashCode) +
|
||||||
(extractEmbedded.hashCode) +
|
(extractEmbedded.hashCode) +
|
||||||
(previewFormat.hashCode) +
|
(preview.hashCode) +
|
||||||
(previewSize.hashCode) +
|
(thumbnail.hashCode);
|
||||||
(quality.hashCode) +
|
|
||||||
(thumbnailFormat.hashCode) +
|
|
||||||
(thumbnailSize.hashCode);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]';
|
String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, preview=$preview, thumbnail=$thumbnail]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'colorspace'] = this.colorspace;
|
json[r'colorspace'] = this.colorspace;
|
||||||
json[r'extractEmbedded'] = this.extractEmbedded;
|
json[r'extractEmbedded'] = this.extractEmbedded;
|
||||||
json[r'previewFormat'] = this.previewFormat;
|
json[r'preview'] = this.preview;
|
||||||
json[r'previewSize'] = this.previewSize;
|
json[r'thumbnail'] = this.thumbnail;
|
||||||
json[r'quality'] = this.quality;
|
|
||||||
json[r'thumbnailFormat'] = this.thumbnailFormat;
|
|
||||||
json[r'thumbnailSize'] = this.thumbnailSize;
|
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,11 +65,8 @@ class SystemConfigImageDto {
|
|||||||
return SystemConfigImageDto(
|
return SystemConfigImageDto(
|
||||||
colorspace: Colorspace.fromJson(json[r'colorspace'])!,
|
colorspace: Colorspace.fromJson(json[r'colorspace'])!,
|
||||||
extractEmbedded: mapValueOfType<bool>(json, r'extractEmbedded')!,
|
extractEmbedded: mapValueOfType<bool>(json, r'extractEmbedded')!,
|
||||||
previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!,
|
preview: SystemConfigGeneratedImageDto.fromJson(json[r'preview'])!,
|
||||||
previewSize: mapValueOfType<int>(json, r'previewSize')!,
|
thumbnail: SystemConfigGeneratedImageDto.fromJson(json[r'thumbnail'])!,
|
||||||
quality: mapValueOfType<int>(json, r'quality')!,
|
|
||||||
thumbnailFormat: ImageFormat.fromJson(json[r'thumbnailFormat'])!,
|
|
||||||
thumbnailSize: mapValueOfType<int>(json, r'thumbnailSize')!,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -141,11 +116,8 @@ class SystemConfigImageDto {
|
|||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'colorspace',
|
'colorspace',
|
||||||
'extractEmbedded',
|
'extractEmbedded',
|
||||||
'previewFormat',
|
'preview',
|
||||||
'previewSize',
|
'thumbnail',
|
||||||
'quality',
|
|
||||||
'thumbnailFormat',
|
|
||||||
'thumbnailSize',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.116.0+160
|
version: 1.116.2+161
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
@ -7409,7 +7409,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@ -11654,6 +11654,28 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SystemConfigGeneratedImageDto": {
|
||||||
|
"properties": {
|
||||||
|
"format": {
|
||||||
|
"$ref": "#/components/schemas/ImageFormat"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"maximum": 100,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"format",
|
||||||
|
"quality",
|
||||||
|
"size"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SystemConfigImageDto": {
|
"SystemConfigImageDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"colorspace": {
|
"colorspace": {
|
||||||
@ -11662,34 +11684,18 @@
|
|||||||
"extractEmbedded": {
|
"extractEmbedded": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"previewFormat": {
|
"preview": {
|
||||||
"$ref": "#/components/schemas/ImageFormat"
|
"$ref": "#/components/schemas/SystemConfigGeneratedImageDto"
|
||||||
},
|
},
|
||||||
"previewSize": {
|
"thumbnail": {
|
||||||
"minimum": 1,
|
"$ref": "#/components/schemas/SystemConfigGeneratedImageDto"
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"quality": {
|
|
||||||
"maximum": 100,
|
|
||||||
"minimum": 1,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"thumbnailFormat": {
|
|
||||||
"$ref": "#/components/schemas/ImageFormat"
|
|
||||||
},
|
|
||||||
"thumbnailSize": {
|
|
||||||
"minimum": 1,
|
|
||||||
"type": "integer"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"colorspace",
|
"colorspace",
|
||||||
"extractEmbedded",
|
"extractEmbedded",
|
||||||
"previewFormat",
|
"preview",
|
||||||
"previewSize",
|
"thumbnail"
|
||||||
"quality",
|
|
||||||
"thumbnailFormat",
|
|
||||||
"thumbnailSize"
|
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.116.0
|
* 1.116.2
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
@ -1100,14 +1100,16 @@ export type SystemConfigFFmpegDto = {
|
|||||||
transcode: TranscodePolicy;
|
transcode: TranscodePolicy;
|
||||||
twoPass: boolean;
|
twoPass: boolean;
|
||||||
};
|
};
|
||||||
|
export type SystemConfigGeneratedImageDto = {
|
||||||
|
format: ImageFormat;
|
||||||
|
quality: number;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
export type SystemConfigImageDto = {
|
export type SystemConfigImageDto = {
|
||||||
colorspace: Colorspace;
|
colorspace: Colorspace;
|
||||||
extractEmbedded: boolean;
|
extractEmbedded: boolean;
|
||||||
previewFormat: ImageFormat;
|
preview: SystemConfigGeneratedImageDto;
|
||||||
previewSize: number;
|
thumbnail: SystemConfigGeneratedImageDto;
|
||||||
quality: number;
|
|
||||||
thumbnailFormat: ImageFormat;
|
|
||||||
thumbnailSize: number;
|
|
||||||
};
|
};
|
||||||
export type JobSettingsDto = {
|
export type JobSettingsDto = {
|
||||||
concurrency: number;
|
concurrency: number;
|
||||||
|
@ -9,15 +9,16 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="design/immich-logo.svg" width="150" title="Login com URL customizada">
|
<img src="../design/immich-logo-stacked-light.svg" width="150" title="Immich Logo">
|
||||||
</p>
|
</p>
|
||||||
<h3 align="center">Immich - Solução self-hosted de alta performance para backup de fotos e vídeos</h3>
|
<h3 align="center">Solução self-hosted de alta performance para backup de fotos e vídeos</h3>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://immich.app">
|
<a href="https://immich.app">
|
||||||
<img src="design/immich-screenshots.png" title="Captura de tela princial">
|
<img src="../design/immich-screenshots.png" title="Captura de tela princial">
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
||||||
<a href="../README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
@ -32,53 +33,58 @@
|
|||||||
<a href="README_ru_RU.md">Русский</a>
|
<a href="README_ru_RU.md">Русский</a>
|
||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Avisos
|
## Avisos
|
||||||
|
|
||||||
- ⚠️ Este projeto está sob **desenvolvimento constante**.
|
- ⚠️ Este projeto está sob **desenvolvimento constante**.
|
||||||
- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a compatibilidade com versões anteriores).
|
- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a
|
||||||
- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e vídeos.**
|
compatibilidade com versões anteriores).
|
||||||
- ⚠️ Sempre siga o plano [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) de backup para as suas mídias preciosas!
|
- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e
|
||||||
|
vídeos.**
|
||||||
|
- ⚠️ Sempre siga o plano
|
||||||
|
[3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) de backup
|
||||||
|
para as suas mídias preciosas!
|
||||||
|
|
||||||
## Conteúdo
|
> [!NOTE]
|
||||||
|
> Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
|
||||||
|
|
||||||
- [Documentação Oficial](https://immich.app/docs)
|
## Links
|
||||||
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
|
|
||||||
- [Demonstração](#demo)
|
- [Documentação](https://immich.app/docs)
|
||||||
- [Recursos](#features)
|
- [Sobre](https://immich.app/docs/overview/introduction)
|
||||||
- [Introdução](https://immich.app/docs/overview/introduction)
|
|
||||||
- [Instalação](https://immich.app/docs/install/requirements)
|
- [Instalação](https://immich.app/docs/install/requirements)
|
||||||
|
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
|
||||||
|
- [Demonstração](#demonstração)
|
||||||
|
- [Funcionalidades](#funcionalidades)
|
||||||
|
- [Traduções](https://immich.app/docs/developer/translations)
|
||||||
- [Diretrizes de Contribuição](https://immich.app/docs/overview/support-the-project)
|
- [Diretrizes de Contribuição](https://immich.app/docs/overview/support-the-project)
|
||||||
|
|
||||||
## Documentação
|
|
||||||
|
|
||||||
Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
|
|
||||||
|
|
||||||
## Demonstração
|
## Demonstração
|
||||||
|
|
||||||
Você pode acessar a demonstração web em https://demo.immich.app
|
Acesse a demonstração [aqui](https://demo.immich.app). A demonstração está
|
||||||
|
hospedada no Nível Gratuito da Oracle VM em Amsterdam com um processador 2.4Ghz
|
||||||
|
quad-core ARM64 e 24GB de RAM.
|
||||||
|
|
||||||
No aplicativo para dispositivos móveis, você pode usar `https://demo.immich.app/api` no campo `Server Endpoint URL`
|
No aplicativo para dispositivos móveis, você pode usar
|
||||||
|
`https://demo.immich.app/api` no campo `Server Endpoint URL`
|
||||||
|
|
||||||
```bash title="Credenciais de Demonstração"
|
### Credenciais de login
|
||||||
Credenciais de Demonstração
|
|
||||||
email: demo@immich.app
|
|
||||||
senha: demo
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
| Email | Senha |
|
||||||
Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
| --------------- | ----- |
|
||||||
```
|
| demo@immich.app | demo |
|
||||||
|
|
||||||
## Atividades
|
## Atividades
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Recursos
|
## Funcionalidades
|
||||||
|
|
||||||
|
| Funcionalidades | Aplicativo Móvel | Web |
|
||||||
| Recursos | Aplicativo Móvel | Web |
|
| :-------------------------------------------------- | ---------------- | --- |
|
||||||
|:----------------------------------------------------|------------------|-----|
|
|
||||||
| Fazer upload e visualizar fotos e vídeos | Sim | Sim |
|
| Fazer upload e visualizar fotos e vídeos | Sim | Sim |
|
||||||
| Backup automático ao abrir o aplicativo | Sim | N/A |
|
| Backup automático ao abrir o aplicativo | Sim | N/A |
|
||||||
| Prevenir a duplicação de arquivos | Sim | Sim |
|
| Prevenir a duplicação de arquivos | Sim | Sim |
|
||||||
@ -88,17 +94,17 @@ Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core AR
|
|||||||
| Criação de álbuns e álbuns compartilhados | Sim | Sim |
|
| Criação de álbuns e álbuns compartilhados | Sim | Sim |
|
||||||
| Barra de rolagem arrastável | Sim | Sim |
|
| Barra de rolagem arrastável | Sim | Sim |
|
||||||
| Suporta formatos RAW | Sim | Sim |
|
| Suporta formatos RAW | Sim | Sim |
|
||||||
| Visualização de metadados (EXIF, map) | Sim | Sim |
|
| Visualização de metadados (EXIF, mapa) | Sim | Sim |
|
||||||
| Pesquisar por metadados, objetos, rostos, and CLIP | Sim | Sim |
|
| Pesquisar por metadados, objetos, rostos, e CLIP | Sim | Sim |
|
||||||
| Funções administrativas (gerenciamento de usuários) | Não | Sim |
|
| Funções administrativas (gerenciamento de usuários) | Não | Sim |
|
||||||
| Backup em segundo plano | Sim | N/A |
|
| Backup em segundo plano | Sim | N/A |
|
||||||
| Virtual scroll | Sim | Sim |
|
| Rolagem virtual | Sim | Sim |
|
||||||
| Suporte OAuth | Sim | Sim |
|
| Suporte OAuth | Sim | Sim |
|
||||||
| Chaves de API | N/A | Sim |
|
| Chaves de API | N/A | Sim |
|
||||||
| Backup e visualização de LivePhoto/MotionPhoto | Sim | Sim |
|
| Backup e reprodução de LivePhoto/MotionPhoto | Sim | Sim |
|
||||||
| Visualização de imagens 360º | Não | Sim |
|
| Visualização de imagens 360º | Não | Sim |
|
||||||
| Estrutura de armazenamento definida pelo usuário | Sim | Sim |
|
| Estrutura de armazenamento definida pelo usuário | Sim | Sim |
|
||||||
| Compartilhar com o público | Não | Sim |
|
| Compartilhar com o público | Sim | Sim |
|
||||||
| Arquivo e Favoritos | Sim | Sim |
|
| Arquivo e Favoritos | Sim | Sim |
|
||||||
| Mapa Global | Sim | Sim |
|
| Mapa Global | Sim | Sim |
|
||||||
| Compartilhamento com parceiro | Sim | Sim |
|
| Compartilhamento com parceiro | Sim | Sim |
|
||||||
@ -108,6 +114,29 @@ Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core AR
|
|||||||
| Galeria em modo apenas leitura | Sim | Sim |
|
| Galeria em modo apenas leitura | Sim | Sim |
|
||||||
| Empilhamento de fotos | Sim | Sim |
|
| Empilhamento de fotos | Sim | Sim |
|
||||||
|
|
||||||
|
## Traduções
|
||||||
|
|
||||||
|
Leia mais sobre as traduções
|
||||||
|
[aqui](https://immich.app/docs/developer/translations).
|
||||||
|
|
||||||
|
<a href="https://hosted.weblate.org/engage/immich/">
|
||||||
|
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Status da tradução" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Atividade do repositório
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Histórico de estrelas
|
||||||
|
|
||||||
|
<a href="https://star-history.com/#immich-app/immich&Date">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
|
||||||
|
<img alt="Gráfico de histórico de estrelas" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
## Contribuidores
|
## Contribuidores
|
||||||
|
|
||||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||||
|
133
readme_i18n/README_vi_VN.md
Normal file
133
readme_i18n/README_vi_VN.md
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<p align="center">
|
||||||
|
<br/>
|
||||||
|
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="Giấy phép: AGPLv3"></a>
|
||||||
|
<a href="https://discord.immich.app">
|
||||||
|
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
|
||||||
|
</a>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="../design/immich-logo-stacked-light.svg" width="300" title="Đăng nhập bằng URL Tuỳ chỉnh">
|
||||||
|
</p>
|
||||||
|
<h3 align="center">Giải pháp quản lý ảnh và video tự lưu trữ hiệu suất cao</h3>
|
||||||
|
<br/>
|
||||||
|
<a href="https://immich.app">
|
||||||
|
<img src="../design/immich-screenshots.png" title="Ảnh chụp màn hình chính">
|
||||||
|
</a>
|
||||||
|
<br/>
|
||||||
|
<p align="center">
|
||||||
|
|
||||||
|
<a href="../README.md">English</a>
|
||||||
|
<a href="README_ca_ES.md">Català</a>
|
||||||
|
<a href="README_es_ES.md">Español</a>
|
||||||
|
<a href="README_fr_FR.md">Français</a>
|
||||||
|
<a href="README_it_IT.md">Italiano</a>
|
||||||
|
<a href="README_ja_JP.md">日本語</a>
|
||||||
|
<a href="README_ko_KR.md">한국어</a>
|
||||||
|
<a href="README_de_DE.md">Deutsch</a>
|
||||||
|
<a href="README_nl_NL.md">Nederlands</a>
|
||||||
|
<a href="README_tr_TR.md">Türkçe</a>
|
||||||
|
<a href="README_zh_CN.md">中文</a>
|
||||||
|
<a href="README_ru_RU.md">Русский</a>
|
||||||
|
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||||
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Tuyên bố miễn trừ trách nhiệm
|
||||||
|
|
||||||
|
- ⚠️ Dự án đang được phát triển **rất tích cực**.
|
||||||
|
- ⚠️ Dự kiến sẽ có lỗi và thay đổi đột ngột.
|
||||||
|
- ⚠️ **Không sử dụng ứng dụng như là cách duy nhất để lưu trữ ảnh và video của bạn.**
|
||||||
|
- ⚠️ Luôn tuân thủ kế hoạch sao lưu [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) cho những bức ảnh và video quý giá của bạn!
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Bạn có thể tìm thấy tài liệu chính, bao gồm hướng dẫn cài đặt, tại https://immich.app/.
|
||||||
|
|
||||||
|
## Liên kết
|
||||||
|
|
||||||
|
- [Tài liệu](https://immich.app/docs)
|
||||||
|
- [Giới thiệu](https://immich.app/docs/overview/introduction)
|
||||||
|
- [Cài đặt](https://immich.app/docs/install/requirements)
|
||||||
|
- [Lộ trình](https://immich.app/roadmap)
|
||||||
|
- [Demo](#demo)
|
||||||
|
- [Tính năng](#Tính-năng)
|
||||||
|
- [Dịch thuật](https://immich.app/docs/developer/translations)
|
||||||
|
- [Đóng góp](https://immich.app/docs/overview/support-the-project)
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
Truy cập bản demo [tại đây](https://demo.immich.app). Bản demo đang chạy trên máy ảo Oracle Free-tier ở Amsterdam với CPU ARM64 lõi tứ 2,4 GHz và RAM 24 GB.
|
||||||
|
|
||||||
|
Đối với ứng dụng di động, bạn có thể sử dụng `https://demo.immich.app/api` cho `Server Endpoint URL`
|
||||||
|
|
||||||
|
### Thông tin đăng nhập
|
||||||
|
|
||||||
|
| Email | Mật khẩu |
|
||||||
|
| --------------- | -------- |
|
||||||
|
| demo@immich.app | demo |
|
||||||
|
|
||||||
|
## Tính năng
|
||||||
|
|
||||||
|
| Tính năng | Mobile | Web |
|
||||||
|
| :--------------------------------------------------- | ------ | ----- |
|
||||||
|
| Tải lên và xem video, ảnh | Có | Có |
|
||||||
|
| Tự động sao lưu khi ứng dụng được mở | Có | N/A |
|
||||||
|
| Ngăn chặn sự trùng lặp nội dung | Có | Có |
|
||||||
|
| Album được chọn để sao lưu | Có | N/A |
|
||||||
|
| Tải ảnh và video xuống thiết bị cục bộ | Có | Có |
|
||||||
|
| Hỗ trợ nhiều người dùng | Có | Có |
|
||||||
|
| Album và Album được chia sẻ | Có | Có |
|
||||||
|
| Thanh cuộn có thể chà / kéo | Có | Có |
|
||||||
|
| Hỗ trợ định dạng raw | Có | Có |
|
||||||
|
| Xem metadata (EXIF, bản đồ) | Có | Có |
|
||||||
|
| Tìm kiếm theo metadata, đối tượng, khuôn mặt và CLIP | Có | Có |
|
||||||
|
| Chức năng quản trị (quản lý người dùng) | Không | Có |
|
||||||
|
| Sao lưu trong nền | Có | N/A |
|
||||||
|
| Cuộn ảo | Có | Có |
|
||||||
|
| Hỗ trợ OAuth | Có | Có |
|
||||||
|
| API Keys | N/A | Có |
|
||||||
|
| Sao lưu và phát lại Live Photo/Motion Photo | Có | Có |
|
||||||
|
| Hỗ trợ hiển thị hình ảnh 360 độ | Không | Có |
|
||||||
|
| Cấu trúc lưu trữ do người dùng xác định | Có | Có |
|
||||||
|
| Chia sẻ công khai | Có | Có |
|
||||||
|
| Lưu trữ và Yêu thích | Có | Có |
|
||||||
|
| Bản đồ toàn cầu | Có | Có |
|
||||||
|
| Chia sẻ đối tác | Có | Có |
|
||||||
|
| Nhận dạng khuôn mặt và phân cụm | Có | Có |
|
||||||
|
| Kỷ niệm (x năm trước) | Có | Có |
|
||||||
|
| Hỗ trợ ngoại tuyến | Có | Không |
|
||||||
|
| Thư viện chỉ đọc | Có | Có |
|
||||||
|
| Ảnh xếp chồng | Có | Có |
|
||||||
|
|
||||||
|
## Dịch thuật
|
||||||
|
|
||||||
|
Đọc thêm về dịch thuật [tại đây](https://immich.app/docs/developer/translations).
|
||||||
|
|
||||||
|
<a href="https://hosted.weblate.org/engage/immich/">
|
||||||
|
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Tình trạng dịch thuật" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Hoạt động của repository
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Lịch sử Đánh dấu sao
|
||||||
|
|
||||||
|
<a href="https://star-history.com/#immich-app/immich&Date">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
|
||||||
|
<img alt="Biểu đồ Lịch sử Đánh dấu" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Người đóng góp
|
||||||
|
|
||||||
|
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||||
|
</a>
|
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bullmq": "^10.0.1",
|
"@nestjs/bullmq": "^10.0.1",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -7,82 +7,20 @@ import { RedisOptions } from 'ioredis';
|
|||||||
import Joi, { Root } from 'joi';
|
import Joi, { Root } from 'joi';
|
||||||
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
||||||
import { ImmichHeader } from 'src/dtos/auth.dto';
|
import { ImmichHeader } from 'src/dtos/auth.dto';
|
||||||
|
import {
|
||||||
|
AudioCodec,
|
||||||
|
Colorspace,
|
||||||
|
CQMode,
|
||||||
|
ImageFormat,
|
||||||
|
LogLevel,
|
||||||
|
ToneMapping,
|
||||||
|
TranscodeHWAccel,
|
||||||
|
TranscodePolicy,
|
||||||
|
VideoCodec,
|
||||||
|
VideoContainer,
|
||||||
|
} from 'src/enum';
|
||||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||||
|
import { ImageOutputConfig } from 'src/interfaces/media.interface';
|
||||||
export enum TranscodePolicy {
|
|
||||||
ALL = 'all',
|
|
||||||
OPTIMAL = 'optimal',
|
|
||||||
BITRATE = 'bitrate',
|
|
||||||
REQUIRED = 'required',
|
|
||||||
DISABLED = 'disabled',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TranscodeTarget {
|
|
||||||
NONE,
|
|
||||||
AUDIO,
|
|
||||||
VIDEO,
|
|
||||||
ALL,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum VideoCodec {
|
|
||||||
H264 = 'h264',
|
|
||||||
HEVC = 'hevc',
|
|
||||||
VP9 = 'vp9',
|
|
||||||
AV1 = 'av1',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AudioCodec {
|
|
||||||
MP3 = 'mp3',
|
|
||||||
AAC = 'aac',
|
|
||||||
LIBOPUS = 'libopus',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum VideoContainer {
|
|
||||||
MOV = 'mov',
|
|
||||||
MP4 = 'mp4',
|
|
||||||
OGG = 'ogg',
|
|
||||||
WEBM = 'webm',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TranscodeHWAccel {
|
|
||||||
NVENC = 'nvenc',
|
|
||||||
QSV = 'qsv',
|
|
||||||
VAAPI = 'vaapi',
|
|
||||||
RKMPP = 'rkmpp',
|
|
||||||
DISABLED = 'disabled',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ToneMapping {
|
|
||||||
HABLE = 'hable',
|
|
||||||
MOBIUS = 'mobius',
|
|
||||||
REINHARD = 'reinhard',
|
|
||||||
DISABLED = 'disabled',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CQMode {
|
|
||||||
AUTO = 'auto',
|
|
||||||
CQP = 'cqp',
|
|
||||||
ICQ = 'icq',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Colorspace {
|
|
||||||
SRGB = 'srgb',
|
|
||||||
P3 = 'p3',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ImageFormat {
|
|
||||||
JPEG = 'jpeg',
|
|
||||||
WEBP = 'webp',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LogLevel {
|
|
||||||
VERBOSE = 'verbose',
|
|
||||||
DEBUG = 'debug',
|
|
||||||
LOG = 'log',
|
|
||||||
WARN = 'warn',
|
|
||||||
ERROR = 'error',
|
|
||||||
FATAL = 'fatal',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SystemConfig {
|
export interface SystemConfig {
|
||||||
ffmpeg: {
|
ffmpeg: {
|
||||||
@ -172,11 +110,8 @@ export interface SystemConfig {
|
|||||||
template: string;
|
template: string;
|
||||||
};
|
};
|
||||||
image: {
|
image: {
|
||||||
thumbnailFormat: ImageFormat;
|
thumbnail: ImageOutputConfig;
|
||||||
thumbnailSize: number;
|
preview: ImageOutputConfig;
|
||||||
previewFormat: ImageFormat;
|
|
||||||
previewSize: number;
|
|
||||||
quality: number;
|
|
||||||
colorspace: Colorspace;
|
colorspace: Colorspace;
|
||||||
extractEmbedded: boolean;
|
extractEmbedded: boolean;
|
||||||
};
|
};
|
||||||
@ -322,11 +257,16 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
thumbnailFormat: ImageFormat.WEBP,
|
thumbnail: {
|
||||||
thumbnailSize: 250,
|
format: ImageFormat.WEBP,
|
||||||
previewFormat: ImageFormat.JPEG,
|
size: 250,
|
||||||
previewSize: 1440,
|
|
||||||
quality: 80,
|
quality: 80,
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
format: ImageFormat.JPEG,
|
||||||
|
size: 1440,
|
||||||
|
quality: 80,
|
||||||
|
},
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
extractEmbedded: false,
|
extractEmbedded: false,
|
||||||
},
|
},
|
||||||
|
@ -54,11 +54,6 @@ export const resourcePaths = {
|
|||||||
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
|
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
|
||||||
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||||
|
|
||||||
export enum AuthType {
|
|
||||||
PASSWORD = 'password',
|
|
||||||
OAUTH = 'oauth',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
||||||
|
|
||||||
export const FACE_THUMBNAIL_SIZE = 250;
|
export const FACE_THUMBNAIL_SIZE = 250;
|
||||||
|
@ -33,16 +33,17 @@ import {
|
|||||||
UploadFieldName,
|
UploadFieldName,
|
||||||
} from 'src/dtos/asset-media.dto';
|
} from 'src/dtos/asset-media.dto';
|
||||||
import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
|
import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
|
||||||
|
import { RouteKey } from 'src/enum';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
|
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
|
||||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||||
import { FileUploadInterceptor, Route, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
|
import { FileUploadInterceptor, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
|
||||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||||
import { sendFile } from 'src/utils/file';
|
import { sendFile } from 'src/utils/file';
|
||||||
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
|
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Assets')
|
@ApiTags('Assets')
|
||||||
@Controller(Route.ASSET)
|
@Controller(RouteKey.ASSET)
|
||||||
export class AssetMediaController {
|
export class AssetMediaController {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
|
@ -14,13 +14,13 @@ import {
|
|||||||
} from 'src/dtos/asset.dto';
|
} from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MemoryLaneDto } from 'src/dtos/search.dto';
|
import { MemoryLaneDto } from 'src/dtos/search.dto';
|
||||||
|
import { RouteKey } from 'src/enum';
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
import { Route } from 'src/middleware/file-upload.interceptor';
|
|
||||||
import { AssetService } from 'src/services/asset.service';
|
import { AssetService } from 'src/services/asset.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Assets')
|
@ApiTags('Assets')
|
||||||
@Controller(Route.ASSET)
|
@Controller(RouteKey.ASSET)
|
||||||
export class AssetController {
|
export class AssetController {
|
||||||
constructor(private service: AssetService) {}
|
constructor(private service: AssetService) {}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
|
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AuthType } from 'src/constants';
|
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
ChangePasswordDto,
|
ChangePasswordDto,
|
||||||
@ -13,6 +12,7 @@ import {
|
|||||||
ValidateAccessTokenResponseDto,
|
ValidateAccessTokenResponseDto,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
||||||
|
import { AuthType } from 'src/enum';
|
||||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
|
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AuthType } from 'src/constants';
|
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
ImmichCookie,
|
ImmichCookie,
|
||||||
@ -11,6 +10,7 @@ import {
|
|||||||
OAuthConfigDto,
|
OAuthConfigDto,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
||||||
|
import { AuthType } from 'src/enum';
|
||||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
import { respondWithCookie } from 'src/utils/response';
|
import { respondWithCookie } from 'src/utils/response';
|
||||||
|
@ -21,15 +21,16 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
|||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||||
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
|
||||||
|
import { RouteKey } from 'src/enum';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||||
import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
|
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
import { sendFile } from 'src/utils/file';
|
import { sendFile } from 'src/utils/file';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Users')
|
@ApiTags('Users')
|
||||||
@Controller(Route.USER)
|
@Controller(RouteKey.USER)
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(
|
constructor(
|
||||||
private service: UserService,
|
private service: UserService,
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { dirname, join, resolve } from 'node:path';
|
import { dirname, join, resolve } from 'node:path';
|
||||||
import { ImageFormat } from 'src/config';
|
|
||||||
import { APP_MEDIA_LOCATION } from 'src/constants';
|
import { APP_MEDIA_LOCATION } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity';
|
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { AssetFileType } from 'src/enum';
|
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -16,14 +14,6 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
|
|
||||||
export enum StorageFolder {
|
|
||||||
ENCODED_VIDEO = 'encoded-video',
|
|
||||||
LIBRARY = 'library',
|
|
||||||
UPLOAD = 'upload',
|
|
||||||
PROFILE = 'profile',
|
|
||||||
THUMBNAILS = 'thumbs',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
|
export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
|
||||||
export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
|
export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
|
||||||
|
|
||||||
|
@ -4,8 +4,8 @@ import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces';
|
|||||||
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
||||||
|
import { MetadataKey } from 'src/enum';
|
||||||
import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface';
|
import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface';
|
||||||
import { Metadata } from 'src/middleware/auth.guard';
|
|
||||||
import { setUnion } from 'src/utils/set';
|
import { setUnion } from 'src/utils/set';
|
||||||
|
|
||||||
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
|
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
|
||||||
@ -141,7 +141,7 @@ export type EmitConfig = {
|
|||||||
/** lower value has higher priority, defaults to 0 */
|
/** lower value has higher priority, defaults to 0 */
|
||||||
priority?: number;
|
priority?: number;
|
||||||
};
|
};
|
||||||
export const OnEmit = (config: EmitConfig) => SetMetadata(Metadata.ON_EMIT_CONFIG, config);
|
export const OnEmit = (config: EmitConfig) => SetMetadata(MetadataKey.ON_EMIT_CONFIG, config);
|
||||||
|
|
||||||
type LifecycleRelease = 'NEXT_RELEASE' | string;
|
type LifecycleRelease = 'NEXT_RELEASE' | string;
|
||||||
type LifecycleMetadata = {
|
type LifecycleMetadata = {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
|
import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
|
||||||
import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
|
import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from 'src/enum';
|
||||||
import { EntityType } from 'src/enum';
|
|
||||||
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
|
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
|
||||||
|
@ -18,20 +18,20 @@ import {
|
|||||||
ValidatorConstraint,
|
ValidatorConstraint,
|
||||||
ValidatorConstraintInterface,
|
ValidatorConstraintInterface,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import { SystemConfig } from 'src/config';
|
||||||
|
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
|
||||||
import {
|
import {
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
CQMode,
|
CQMode,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
SystemConfig,
|
|
||||||
ToneMapping,
|
ToneMapping,
|
||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
VideoContainer,
|
VideoContainer,
|
||||||
} from 'src/config';
|
} from 'src/enum';
|
||||||
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
|
|
||||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||||
import { ValidateBoolean, validateCronExpression } from 'src/validation';
|
import { ValidateBoolean, validateCronExpression } from 'src/validation';
|
||||||
|
|
||||||
@ -473,26 +473,10 @@ export class SystemConfigThemeDto {
|
|||||||
customCss!: string;
|
customCss!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemConfigImageDto {
|
class SystemConfigGeneratedImageDto {
|
||||||
@IsEnum(ImageFormat)
|
@IsEnum(ImageFormat)
|
||||||
@ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
|
@ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
|
||||||
thumbnailFormat!: ImageFormat;
|
format!: ImageFormat;
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@Min(1)
|
|
||||||
@Type(() => Number)
|
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
thumbnailSize!: number;
|
|
||||||
|
|
||||||
@IsEnum(ImageFormat)
|
|
||||||
@ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
|
|
||||||
previewFormat!: ImageFormat;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@Min(1)
|
|
||||||
@Type(() => Number)
|
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
previewSize!: number;
|
|
||||||
|
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
@ -501,6 +485,24 @@ class SystemConfigImageDto {
|
|||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
quality!: number;
|
quality!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
size!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemConfigImageDto {
|
||||||
|
@Type(() => SystemConfigGeneratedImageDto)
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
thumbnail!: SystemConfigGeneratedImageDto;
|
||||||
|
|
||||||
|
@Type(() => SystemConfigGeneratedImageDto)
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
preview!: SystemConfigGeneratedImageDto;
|
||||||
|
|
||||||
@IsEnum(Colorspace)
|
@IsEnum(Colorspace)
|
||||||
@ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
|
@ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
|
||||||
colorspace!: Colorspace;
|
colorspace!: Colorspace;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { PathType } from 'src/enum';
|
||||||
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
|
|
||||||
@Entity('move_history')
|
@Entity('move_history')
|
||||||
@ -21,21 +22,3 @@ export class MoveEntity {
|
|||||||
@Column({ type: 'varchar' })
|
@Column({ type: 'varchar' })
|
||||||
newPath!: string;
|
newPath!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AssetPathType {
|
|
||||||
ORIGINAL = 'original',
|
|
||||||
PREVIEW = 'preview',
|
|
||||||
THUMBNAIL = 'thumbnail',
|
|
||||||
ENCODED_VIDEO = 'encoded_video',
|
|
||||||
SIDECAR = 'sidecar',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PersonPathType {
|
|
||||||
FACE = 'face',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum UserPathType {
|
|
||||||
PROFILE = 'profile',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PathType = AssetPathType | PersonPathType | UserPathType;
|
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
export enum AuthType {
|
||||||
|
PASSWORD = 'password',
|
||||||
|
OAUTH = 'oauth',
|
||||||
|
}
|
||||||
|
|
||||||
export enum AssetType {
|
export enum AssetType {
|
||||||
IMAGE = 'IMAGE',
|
IMAGE = 'IMAGE',
|
||||||
VIDEO = 'VIDEO',
|
VIDEO = 'VIDEO',
|
||||||
@ -148,6 +153,14 @@ export enum SharedLinkType {
|
|||||||
INDIVIDUAL = 'INDIVIDUAL',
|
INDIVIDUAL = 'INDIVIDUAL',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum StorageFolder {
|
||||||
|
ENCODED_VIDEO = 'encoded-video',
|
||||||
|
LIBRARY = 'library',
|
||||||
|
UPLOAD = 'upload',
|
||||||
|
PROFILE = 'profile',
|
||||||
|
THUMBNAILS = 'thumbs',
|
||||||
|
}
|
||||||
|
|
||||||
export enum SystemMetadataKey {
|
export enum SystemMetadataKey {
|
||||||
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
|
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
|
||||||
FACIAL_RECOGNITION_STATE = 'facial-recognition-state',
|
FACIAL_RECOGNITION_STATE = 'facial-recognition-state',
|
||||||
@ -198,3 +211,120 @@ export enum ManualJobName {
|
|||||||
TAG_CLEANUP = 'tag-cleanup',
|
TAG_CLEANUP = 'tag-cleanup',
|
||||||
USER_CLEANUP = 'user-cleanup',
|
USER_CLEANUP = 'user-cleanup',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AssetPathType {
|
||||||
|
ORIGINAL = 'original',
|
||||||
|
PREVIEW = 'preview',
|
||||||
|
THUMBNAIL = 'thumbnail',
|
||||||
|
ENCODED_VIDEO = 'encoded_video',
|
||||||
|
SIDECAR = 'sidecar',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PersonPathType {
|
||||||
|
FACE = 'face',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserPathType {
|
||||||
|
PROFILE = 'profile',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PathType = AssetPathType | PersonPathType | UserPathType;
|
||||||
|
|
||||||
|
export enum TranscodePolicy {
|
||||||
|
ALL = 'all',
|
||||||
|
OPTIMAL = 'optimal',
|
||||||
|
BITRATE = 'bitrate',
|
||||||
|
REQUIRED = 'required',
|
||||||
|
DISABLED = 'disabled',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TranscodeTarget {
|
||||||
|
NONE,
|
||||||
|
AUDIO,
|
||||||
|
VIDEO,
|
||||||
|
ALL,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VideoCodec {
|
||||||
|
H264 = 'h264',
|
||||||
|
HEVC = 'hevc',
|
||||||
|
VP9 = 'vp9',
|
||||||
|
AV1 = 'av1',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AudioCodec {
|
||||||
|
MP3 = 'mp3',
|
||||||
|
AAC = 'aac',
|
||||||
|
LIBOPUS = 'libopus',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VideoContainer {
|
||||||
|
MOV = 'mov',
|
||||||
|
MP4 = 'mp4',
|
||||||
|
OGG = 'ogg',
|
||||||
|
WEBM = 'webm',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TranscodeHWAccel {
|
||||||
|
NVENC = 'nvenc',
|
||||||
|
QSV = 'qsv',
|
||||||
|
VAAPI = 'vaapi',
|
||||||
|
RKMPP = 'rkmpp',
|
||||||
|
DISABLED = 'disabled',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ToneMapping {
|
||||||
|
HABLE = 'hable',
|
||||||
|
MOBIUS = 'mobius',
|
||||||
|
REINHARD = 'reinhard',
|
||||||
|
DISABLED = 'disabled',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CQMode {
|
||||||
|
AUTO = 'auto',
|
||||||
|
CQP = 'cqp',
|
||||||
|
ICQ = 'icq',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Colorspace {
|
||||||
|
SRGB = 'srgb',
|
||||||
|
P3 = 'p3',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ImageFormat {
|
||||||
|
JPEG = 'jpeg',
|
||||||
|
WEBP = 'webp',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
VERBOSE = 'verbose',
|
||||||
|
DEBUG = 'debug',
|
||||||
|
LOG = 'log',
|
||||||
|
WARN = 'warn',
|
||||||
|
ERROR = 'error',
|
||||||
|
FATAL = 'fatal',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MetadataKey {
|
||||||
|
AUTH_ROUTE = 'auth_route',
|
||||||
|
ADMIN_ROUTE = 'admin_route',
|
||||||
|
SHARED_ROUTE = 'shared_route',
|
||||||
|
API_KEY_SECURITY = 'api_key',
|
||||||
|
ON_EMIT_CONFIG = 'on_emit_config',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RouteKey {
|
||||||
|
ASSET = 'assets',
|
||||||
|
USER = 'users',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CacheControl {
|
||||||
|
PRIVATE_WITH_CACHE = 'private_with_cache',
|
||||||
|
PRIVATE_WITHOUT_CACHE = 'private_without_cache',
|
||||||
|
NONE = 'none',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PaginationMode {
|
||||||
|
LIMIT_OFFSET = 'limit-offset',
|
||||||
|
SKIP_TAKE = 'skip-take',
|
||||||
|
}
|
||||||
|
14
server/src/interfaces/config.interface.ts
Normal file
14
server/src/interfaces/config.interface.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { VectorExtension } from 'src/interfaces/database.interface';
|
||||||
|
|
||||||
|
export const IConfigRepository = 'IConfigRepository';
|
||||||
|
|
||||||
|
export interface EnvData {
|
||||||
|
database: {
|
||||||
|
skipMigrations: boolean;
|
||||||
|
vectorExtension: VectorExtension;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConfigRepository {
|
||||||
|
getEnv(): EnvData;
|
||||||
|
}
|
@ -116,7 +116,7 @@ export enum JobName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
||||||
export const JOBS_LIBRARY_PAGINATION_SIZE = 100_000;
|
export const JOBS_LIBRARY_PAGINATION_SIZE = 10_000;
|
||||||
|
|
||||||
export interface IBaseJob {
|
export interface IBaseJob {
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LogLevel } from 'src/config';
|
import { LogLevel } from 'src/enum';
|
||||||
|
|
||||||
export const ILoggerRepository = 'ILoggerRepository';
|
export const ILoggerRepository = 'ILoggerRepository';
|
||||||
|
|
||||||
@ -6,6 +6,7 @@ export interface ILoggerRepository {
|
|||||||
setAppName(name: string): void;
|
setAppName(name: string): void;
|
||||||
setContext(message: string): void;
|
setContext(message: string): void;
|
||||||
setLogLevel(level: LogLevel): void;
|
setLogLevel(level: LogLevel): void;
|
||||||
|
isLevelEnabled(level: LogLevel): boolean;
|
||||||
|
|
||||||
verbose(message: any, ...args: any): void;
|
verbose(message: any, ...args: any): void;
|
||||||
debug(message: any, ...args: any): void;
|
debug(message: any, ...args: any): void;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/config';
|
import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/enum';
|
||||||
|
|
||||||
export const IMediaRepository = 'IMediaRepository';
|
export const IMediaRepository = 'IMediaRepository';
|
||||||
|
|
||||||
@ -10,11 +10,14 @@ export interface CropOptions {
|
|||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThumbnailOptions {
|
export interface ImageOutputConfig {
|
||||||
size: number;
|
|
||||||
format: ImageFormat;
|
format: ImageFormat;
|
||||||
colorspace: string;
|
|
||||||
quality: number;
|
quality: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThumbnailOptions extends ImageOutputConfig {
|
||||||
|
colorspace: string;
|
||||||
crop?: CropOptions;
|
crop?: CropOptions;
|
||||||
processInvalidImages: boolean;
|
processInvalidImages: boolean;
|
||||||
}
|
}
|
||||||
@ -62,6 +65,10 @@ export interface TranscodeCommand {
|
|||||||
inputOptions: string[];
|
inputOptions: string[];
|
||||||
outputOptions: string[];
|
outputOptions: string[];
|
||||||
twoPass: boolean;
|
twoPass: boolean;
|
||||||
|
progress: {
|
||||||
|
frameCount: number;
|
||||||
|
percentInterval: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BitrateDistribution {
|
export interface BitrateDistribution {
|
||||||
@ -79,6 +86,10 @@ export interface VideoCodecHWConfig extends VideoCodecSWConfig {
|
|||||||
getSupportedCodecs(): Array<VideoCodec>;
|
getSupportedCodecs(): Array<VideoCodec>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProbeOptions {
|
||||||
|
countFrames: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IMediaRepository {
|
export interface IMediaRepository {
|
||||||
// image
|
// image
|
||||||
extract(input: string, output: string): Promise<boolean>;
|
extract(input: string, output: string): Promise<boolean>;
|
||||||
@ -87,6 +98,6 @@ export interface IMediaRepository {
|
|||||||
getImageDimensions(input: string): Promise<ImageDimensions>;
|
getImageDimensions(input: string): Promise<ImageDimensions>;
|
||||||
|
|
||||||
// video
|
// video
|
||||||
probe(input: string): Promise<VideoInfo>;
|
probe(input: string, options?: ProbeOptions): Promise<VideoInfo>;
|
||||||
transcode(input: string, output: string | Writable, command: TranscodeCommand): Promise<void>;
|
transcode(input: string, output: string | Writable, command: TranscodeCommand): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,18 @@ export interface ExifDuration {
|
|||||||
Scale?: number;
|
Scale?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TagsWithWrongTypes = 'FocalLength' | 'Duration' | 'Description' | 'ImageDescription' | 'RegionInfo';
|
type StringOrNumber = string | number;
|
||||||
|
|
||||||
|
type TagsWithWrongTypes =
|
||||||
|
| 'FocalLength'
|
||||||
|
| 'Duration'
|
||||||
|
| 'Description'
|
||||||
|
| 'ImageDescription'
|
||||||
|
| 'RegionInfo'
|
||||||
|
| 'TagsList'
|
||||||
|
| 'Keywords'
|
||||||
|
| 'HierarchicalSubject'
|
||||||
|
| 'ISO';
|
||||||
export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
|
export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
|
||||||
ContentIdentifier?: string;
|
ContentIdentifier?: string;
|
||||||
MotionPhoto?: number;
|
MotionPhoto?: number;
|
||||||
@ -20,10 +31,14 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
|
|||||||
EmbeddedVideoType?: string;
|
EmbeddedVideoType?: string;
|
||||||
EmbeddedVideoFile?: BinaryField;
|
EmbeddedVideoFile?: BinaryField;
|
||||||
MotionPhotoVideo?: BinaryField;
|
MotionPhotoVideo?: BinaryField;
|
||||||
|
TagsList?: StringOrNumber[];
|
||||||
|
HierarchicalSubject?: StringOrNumber[];
|
||||||
|
Keywords?: StringOrNumber | StringOrNumber[];
|
||||||
|
ISO?: number | number[];
|
||||||
|
|
||||||
// Type is wrong, can also be number.
|
// Type is wrong, can also be number.
|
||||||
Description?: string | number;
|
Description?: StringOrNumber;
|
||||||
ImageDescription?: string | number;
|
ImageDescription?: StringOrNumber;
|
||||||
|
|
||||||
// Extended properties for image regions, such as faces
|
// Extended properties for image regions, such as faces
|
||||||
RegionInfo?: {
|
RegionInfo?: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MoveEntity, PathType } from 'src/entities/move.entity';
|
import { MoveEntity } from 'src/entities/move.entity';
|
||||||
|
import { PathType } from 'src/enum';
|
||||||
|
|
||||||
export const IMoveRepository = 'IMoveRepository';
|
export const IMoveRepository = 'IMoveRepository';
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { CommandFactory } from 'nest-commander';
|
|||||||
import { fork } from 'node:child_process';
|
import { fork } from 'node:child_process';
|
||||||
import { Worker } from 'node:worker_threads';
|
import { Worker } from 'node:worker_threads';
|
||||||
import { ImmichAdminModule } from 'src/app.module';
|
import { ImmichAdminModule } from 'src/app.module';
|
||||||
import { LogLevel } from 'src/config';
|
import { LogLevel } from 'src/enum';
|
||||||
import { getWorkers } from 'src/utils/workers';
|
import { getWorkers } from 'src/utils/workers';
|
||||||
const immichApp = process.argv[2] || process.env.IMMICH_APP;
|
const immichApp = process.argv[2] || process.env.IMMICH_APP;
|
||||||
|
|
||||||
|
@ -11,19 +11,11 @@ import { Reflector } from '@nestjs/core';
|
|||||||
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto';
|
import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto';
|
||||||
import { Permission } from 'src/enum';
|
import { MetadataKey, Permission } from 'src/enum';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
|
|
||||||
export enum Metadata {
|
|
||||||
AUTH_ROUTE = 'auth_route',
|
|
||||||
ADMIN_ROUTE = 'admin_route',
|
|
||||||
SHARED_ROUTE = 'shared_route',
|
|
||||||
API_KEY_SECURITY = 'api_key',
|
|
||||||
ON_EMIT_CONFIG = 'on_emit_config',
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminRoute = { admin?: true };
|
type AdminRoute = { admin?: true };
|
||||||
type SharedLinkRoute = { sharedLink?: true };
|
type SharedLinkRoute = { sharedLink?: true };
|
||||||
type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
|
type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
|
||||||
@ -32,8 +24,8 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator =
|
|||||||
const decorators: MethodDecorator[] = [
|
const decorators: MethodDecorator[] = [
|
||||||
ApiBearerAuth(),
|
ApiBearerAuth(),
|
||||||
ApiCookieAuth(),
|
ApiCookieAuth(),
|
||||||
ApiSecurity(Metadata.API_KEY_SECURITY),
|
ApiSecurity(MetadataKey.API_KEY_SECURITY),
|
||||||
SetMetadata(Metadata.AUTH_ROUTE, options || {}),
|
SetMetadata(MetadataKey.AUTH_ROUTE, options || {}),
|
||||||
];
|
];
|
||||||
|
|
||||||
if ((options as SharedLinkRoute)?.sharedLink) {
|
if ((options as SharedLinkRoute)?.sharedLink) {
|
||||||
@ -85,7 +77,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const targets = [context.getHandler()];
|
const targets = [context.getHandler()];
|
||||||
|
|
||||||
const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(Metadata.AUTH_ROUTE, targets);
|
const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AUTH_ROUTE, targets);
|
||||||
if (!options) {
|
if (!options) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import multer, { StorageEngine, diskStorage } from 'multer';
|
|||||||
import { createHash, randomUUID } from 'node:crypto';
|
import { createHash, randomUUID } from 'node:crypto';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||||
|
import { RouteKey } from 'src/enum';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||||
import { AssetMediaService, UploadFile } from 'src/services/asset-media.service';
|
import { AssetMediaService, UploadFile } from 'src/services/asset-media.service';
|
||||||
@ -28,11 +29,6 @@ export function getFiles(files: UploadFiles) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Route {
|
|
||||||
ASSET = 'assets',
|
|
||||||
USER = 'users',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImmichFile extends Express.Multer.File {
|
export interface ImmichFile extends Express.Multer.File {
|
||||||
/** sha1 hash of file */
|
/** sha1 hash of file */
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@ -115,7 +111,7 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||||||
const context_ = context.switchToHttp();
|
const context_ = context.switchToHttp();
|
||||||
const route = this.reflect.get<string>(PATH_METADATA, context.getClass());
|
const route = this.reflect.get<string>(PATH_METADATA, context.getClass());
|
||||||
|
|
||||||
const handler: RequestHandler | null = this.getHandler(route as Route);
|
const handler: RequestHandler | null = this.getHandler(route as RouteKey);
|
||||||
if (handler) {
|
if (handler) {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve());
|
const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve());
|
||||||
@ -176,13 +172,13 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHandler(route: Route) {
|
private getHandler(route: RouteKey) {
|
||||||
switch (route) {
|
switch (route) {
|
||||||
case Route.ASSET: {
|
case RouteKey.ASSET: {
|
||||||
return this.handlers.assetUpload;
|
return this.handlers.assetUpload;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Route.USER: {
|
case RouteKey.USER: {
|
||||||
return this.handlers.userProfile;
|
return this.handlers.userProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class SeparateQualityForThumbnailAndPreview1727471863507 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
update system_metadata
|
||||||
|
set value = jsonb_set(value, '{image}', jsonb_strip_nulls(
|
||||||
|
jsonb_build_object(
|
||||||
|
'preview', jsonb_build_object(
|
||||||
|
'format', value->'image'->'previewFormat',
|
||||||
|
'quality', value->'image'->'quality',
|
||||||
|
'size', value->'image'->'previewSize'),
|
||||||
|
'thumbnail', jsonb_build_object(
|
||||||
|
'format', value->'image'->'thumbnailFormat',
|
||||||
|
'quality', value->'image'->'quality',
|
||||||
|
'size', value->'image'->'thumbnailSize'),
|
||||||
|
'extractEmbedded', value->'extractEmbedded',
|
||||||
|
'colorspace', value->'colorspace'
|
||||||
|
)))
|
||||||
|
where key = 'system-config'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
update system_metadata
|
||||||
|
set value = jsonb_set(value, '{image}', jsonb_strip_nulls(jsonb_build_object(
|
||||||
|
'previewFormat', value->'image'->'preview'->'format',
|
||||||
|
'previewSize', value->'image'->'preview'->'size',
|
||||||
|
'thumbnailFormat', value->'image'->'thumbnail'->'format',
|
||||||
|
'thumbnailSize', value->'image'->'thumbnail'->'size',
|
||||||
|
'extractEmbedded', value->'extractEmbedded',
|
||||||
|
'colorspace', value->'colorspace',
|
||||||
|
'quality', value->'image'->'preview'->'quality'
|
||||||
|
)))
|
||||||
|
where key = 'system-config'`);
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||||
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
import { AssetFileType, AssetOrder, AssetStatus, AssetType, PaginationMode } from 'src/enum';
|
||||||
import {
|
import {
|
||||||
AssetBuilderOptions,
|
AssetBuilderOptions,
|
||||||
AssetCreate,
|
AssetCreate,
|
||||||
@ -30,7 +30,7 @@ import {
|
|||||||
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
||||||
import { searchAssetBuilder } from 'src/utils/database';
|
import { searchAssetBuilder } from 'src/utils/database';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
||||||
import {
|
import {
|
||||||
Brackets,
|
Brackets,
|
||||||
FindOptionsOrder,
|
FindOptionsOrder,
|
||||||
|
15
server/src/repositories/config.repository.ts
Normal file
15
server/src/repositories/config.repository.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { getVectorExtension } from 'src/database.config';
|
||||||
|
import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ConfigRepository implements IConfigRepository {
|
||||||
|
getEnv(): EnvData {
|
||||||
|
return {
|
||||||
|
database: {
|
||||||
|
skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
|
||||||
|
vectorExtension: getVectorExtension(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
@ -39,6 +40,7 @@ import { AlbumRepository } from 'src/repositories/album.repository';
|
|||||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||||
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||||
import { EventRepository } from 'src/repositories/event.repository';
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
@ -74,6 +76,7 @@ export const repositories = [
|
|||||||
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
|
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
|
||||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||||
{ provide: IAuditRepository, useClass: AuditRepository },
|
{ provide: IAuditRepository, useClass: AuditRepository },
|
||||||
|
{ provide: IConfigRepository, useClass: ConfigRepository },
|
||||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||||
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
||||||
{ provide: IEventRepository, useClass: EventRepository },
|
{ provide: IEventRepository, useClass: EventRepository },
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
|
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
|
||||||
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
|
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
|
||||||
import { ClsService } from 'nestjs-cls';
|
import { ClsService } from 'nestjs-cls';
|
||||||
import { LogLevel } from 'src/config';
|
import { LogLevel } from 'src/enum';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { LogColor } from 'src/utils/logger';
|
import { LogColor } from 'src/utils/logger';
|
||||||
|
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { exiftool } from 'exiftool-vendored';
|
import { exiftool } from 'exiftool-vendored';
|
||||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
||||||
|
import { Duration } from 'luxon';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
import { promisify } from 'node:util';
|
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { Colorspace } from 'src/config';
|
import { Colorspace, LogLevel } from 'src/enum';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import {
|
import {
|
||||||
IMediaRepository,
|
IMediaRepository,
|
||||||
ImageDimensions,
|
ImageDimensions,
|
||||||
|
ProbeOptions,
|
||||||
ThumbnailOptions,
|
ThumbnailOptions,
|
||||||
TranscodeCommand,
|
TranscodeCommand,
|
||||||
VideoInfo,
|
VideoInfo,
|
||||||
@ -17,10 +18,22 @@ import {
|
|||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { handlePromiseError } from 'src/utils/misc';
|
import { handlePromiseError } from 'src/utils/misc';
|
||||||
|
|
||||||
const probe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
|
const probe = (input: string, options: string[]): Promise<FfprobeData> =>
|
||||||
|
new Promise((resolve, reject) =>
|
||||||
|
ffmpeg.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))),
|
||||||
|
);
|
||||||
sharp.concurrency(0);
|
sharp.concurrency(0);
|
||||||
sharp.cache({ files: 0 });
|
sharp.cache({ files: 0 });
|
||||||
|
|
||||||
|
type ProgressEvent = {
|
||||||
|
frames: number;
|
||||||
|
currentFps: number;
|
||||||
|
currentKbps: number;
|
||||||
|
targetSize: number;
|
||||||
|
timemark: string;
|
||||||
|
percent?: number;
|
||||||
|
};
|
||||||
|
|
||||||
@Instrumentation()
|
@Instrumentation()
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MediaRepository implements IMediaRepository {
|
export class MediaRepository implements IMediaRepository {
|
||||||
@ -65,8 +78,8 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
.toFile(output);
|
.toFile(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
async probe(input: string): Promise<VideoInfo> {
|
async probe(input: string, options?: ProbeOptions): Promise<VideoInfo> {
|
||||||
const results = await probe(input);
|
const results = await probe(input, options?.countFrames ? ['-count_packets'] : []); // gets frame count quickly: https://stackoverflow.com/a/28376817
|
||||||
return {
|
return {
|
||||||
format: {
|
format: {
|
||||||
formatName: results.format.format_name,
|
formatName: results.format.format_name,
|
||||||
@ -83,10 +96,10 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
width: stream.width || 0,
|
width: stream.width || 0,
|
||||||
codecName: stream.codec_name === 'h265' ? 'hevc' : stream.codec_name,
|
codecName: stream.codec_name === 'h265' ? 'hevc' : stream.codec_name,
|
||||||
codecType: stream.codec_type,
|
codecType: stream.codec_type,
|
||||||
frameCount: Number.parseInt(stream.nb_frames ?? '0'),
|
frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
|
||||||
rotation: Number.parseInt(`${stream.rotation ?? 0}`),
|
rotation: this.parseInt(stream.rotation),
|
||||||
isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67',
|
isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67',
|
||||||
bitrate: Number.parseInt(stream.bit_rate ?? '0'),
|
bitrate: this.parseInt(stream.bit_rate),
|
||||||
})),
|
})),
|
||||||
audioStreams: results.streams
|
audioStreams: results.streams
|
||||||
.filter((stream) => stream.codec_type === 'audio')
|
.filter((stream) => stream.codec_type === 'audio')
|
||||||
@ -94,7 +107,7 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
index: stream.index,
|
index: stream.index,
|
||||||
codecType: stream.codec_type,
|
codecType: stream.codec_type,
|
||||||
codecName: stream.codec_name,
|
codecName: stream.codec_name,
|
||||||
frameCount: Number.parseInt(stream.nb_frames ?? '0'),
|
frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -156,10 +169,37 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeCommand) {
|
private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeCommand) {
|
||||||
return ffmpeg(input, { niceness: 10 })
|
const ffmpegCall = ffmpeg(input, { niceness: 10 })
|
||||||
.inputOptions(options.inputOptions)
|
.inputOptions(options.inputOptions)
|
||||||
.outputOptions(options.outputOptions)
|
.outputOptions(options.outputOptions)
|
||||||
.output(output)
|
.output(output)
|
||||||
.on('error', (error, stdout, stderr) => this.logger.error(stderr || error));
|
.on('start', (command: string) => this.logger.debug(command))
|
||||||
|
.on('error', (error, _, stderr) => this.logger.error(stderr || error));
|
||||||
|
|
||||||
|
const { frameCount, percentInterval } = options.progress;
|
||||||
|
const frameInterval = Math.ceil(frameCount / (100 / percentInterval));
|
||||||
|
if (this.logger.isLevelEnabled(LogLevel.DEBUG) && frameCount && frameInterval) {
|
||||||
|
let lastProgressFrame: number = 0;
|
||||||
|
ffmpegCall.on('progress', (progress: ProgressEvent) => {
|
||||||
|
if (progress.frames - lastProgressFrame < frameInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastProgressFrame = progress.frames;
|
||||||
|
const percent = ((progress.frames / frameCount) * 100).toFixed(2);
|
||||||
|
const ms = Math.floor((frameCount - progress.frames) / progress.currentFps) * 1000;
|
||||||
|
const duration = ms ? Duration.fromMillis(ms).rescale().toHuman({ unitDisplay: 'narrow' }) : '';
|
||||||
|
const outputText = output instanceof Writable ? 'stream' : output.split('/').pop();
|
||||||
|
this.logger.debug(
|
||||||
|
`Transcoding ${percent}% done${duration ? `, estimated ${duration} remaining` : ''} for output ${outputText}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ffmpegCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseInt(value: string | number | undefined): number {
|
||||||
|
return Number.parseInt(value as string) || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { MoveEntity, PathType } from 'src/entities/move.entity';
|
import { MoveEntity } from 'src/entities/move.entity';
|
||||||
|
import { PathType } from 'src/enum';
|
||||||
import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface';
|
import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
@ -6,7 +6,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
|||||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { SourceType } from 'src/enum';
|
import { PaginationMode, SourceType } from 'src/enum';
|
||||||
import {
|
import {
|
||||||
AssetFaceId,
|
AssetFaceId,
|
||||||
DeleteAllFacesOptions,
|
DeleteAllFacesOptions,
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
UpdateFacesData,
|
UpdateFacesData,
|
||||||
} from 'src/interfaces/person.interface';
|
} from 'src/interfaces/person.interface';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
||||||
import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
|
import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
|
||||||
|
|
||||||
@Instrumentation()
|
@Instrumentation()
|
||||||
|
@ -8,7 +8,7 @@ import { ExifEntity } from 'src/entities/exif.entity';
|
|||||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||||
import { AssetType } from 'src/enum';
|
import { AssetType, PaginationMode } from 'src/enum';
|
||||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import {
|
import {
|
||||||
@ -23,7 +23,7 @@ import {
|
|||||||
} from 'src/interfaces/search.interface';
|
} from 'src/interfaces/search.interface';
|
||||||
import { asVector, searchAssetBuilder } from 'src/utils/database';
|
import { asVector, searchAssetBuilder } from 'src/utils/database';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
|
import { Paginated, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
|
||||||
import { isValidInteger } from 'src/validation';
|
import { isValidInteger } from 'src/validation';
|
||||||
import { Repository, SelectQueryBuilder } from 'typeorm';
|
import { Repository, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos
|
|||||||
import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
|
import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetStatus, AssetType } from 'src/enum';
|
import { AssetStatus, AssetType, CacheControl } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||||
@ -12,7 +12,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { fileStub } from 'test/fixtures/file.stub';
|
import { fileStub } from 'test/fixtures/file.stub';
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { extname } from 'node:path';
|
import { extname } from 'node:path';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import {
|
import {
|
||||||
AssetBulkUploadCheckResponseDto,
|
AssetBulkUploadCheckResponseDto,
|
||||||
AssetMediaResponseDto,
|
AssetMediaResponseDto,
|
||||||
@ -27,7 +27,7 @@ import {
|
|||||||
} from 'src/dtos/asset-media.dto';
|
} from 'src/dtos/asset-media.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetStatus, AssetType, Permission } from 'src/enum';
|
import { AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
@ -37,7 +37,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { requireAccess, requireUploadAccess } from 'src/utils/access';
|
import { requireAccess, requireUploadAccess } from 'src/utils/access';
|
||||||
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { fromChecksum } from 'src/utils/request';
|
import { fromChecksum } from 'src/utils/request';
|
||||||
import { QueryFailedError } from 'typeorm';
|
import { QueryFailedError } from 'typeorm';
|
||||||
|
@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import {
|
import {
|
||||||
AuditDeletesDto,
|
AuditDeletesDto,
|
||||||
AuditDeletesResponseDto,
|
AuditDeletesResponseDto,
|
||||||
@ -12,8 +12,15 @@ import {
|
|||||||
PathEntityType,
|
PathEntityType,
|
||||||
} from 'src/dtos/audit.dto';
|
} from 'src/dtos/audit.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
|
import {
|
||||||
import { AssetFileType, DatabaseAction, Permission } from 'src/enum';
|
AssetFileType,
|
||||||
|
AssetPathType,
|
||||||
|
DatabaseAction,
|
||||||
|
Permission,
|
||||||
|
PersonPathType,
|
||||||
|
StorageFolder,
|
||||||
|
UserPathType,
|
||||||
|
} from 'src/enum';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { Issuer, generators } from 'openid-client';
|
import { Issuer, generators } from 'openid-client';
|
||||||
import { AuthType } from 'src/constants';
|
|
||||||
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
||||||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
|
import { AuthType } from 'src/enum';
|
||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
|
@ -12,7 +12,7 @@ import { DateTime } from 'luxon';
|
|||||||
import { IncomingHttpHeaders } from 'node:http';
|
import { IncomingHttpHeaders } from 'node:http';
|
||||||
import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { AuthType, LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { UserCore } from 'src/cores/user.core';
|
import { UserCore } from 'src/cores/user.core';
|
||||||
import {
|
import {
|
||||||
@ -31,7 +31,7 @@ import {
|
|||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { Permission } from 'src/enum';
|
import { AuthType, Permission } from 'src/enum';
|
||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
import { DatabaseExtension, EXTENSION_NAMES, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
|
import {
|
||||||
|
DatabaseExtension,
|
||||||
|
EXTENSION_NAMES,
|
||||||
|
IDatabaseRepository,
|
||||||
|
VectorExtension,
|
||||||
|
} from 'src/interfaces/database.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { DatabaseService } from 'src/services/database.service';
|
import { DatabaseService } from 'src/services/database.service';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
describe(DatabaseService.name, () => {
|
describe(DatabaseService.name, () => {
|
||||||
let sut: DatabaseService;
|
let sut: DatabaseService;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let databaseMock: Mocked<IDatabaseRepository>;
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
let extensionRange: string;
|
let extensionRange: string;
|
||||||
@ -16,9 +24,11 @@ describe(DatabaseService.name, () => {
|
|||||||
let versionAboveRange: string;
|
let versionAboveRange: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
databaseMock = newDatabaseRepositoryMock();
|
databaseMock = newDatabaseRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
sut = new DatabaseService(databaseMock, loggerMock);
|
|
||||||
|
sut = new DatabaseService(configMock, databaseMock, loggerMock);
|
||||||
|
|
||||||
extensionRange = '0.2.x';
|
extensionRange = '0.2.x';
|
||||||
databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange);
|
databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange);
|
||||||
@ -33,11 +43,6 @@ describe(DatabaseService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
delete process.env.DB_SKIP_MIGRATIONS;
|
|
||||||
delete process.env.DB_VECTOR_EXTENSION;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(sut).toBeDefined();
|
expect(sut).toBeDefined();
|
||||||
});
|
});
|
||||||
@ -50,12 +55,12 @@ describe(DatabaseService.name, () => {
|
|||||||
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
|
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each([
|
describe.each(<Array<{ extension: VectorExtension; extensionName: string }>>[
|
||||||
{ extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] },
|
{ extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] },
|
||||||
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
|
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
|
||||||
])('should work with $extensionName', ({ extension, extensionName }) => {
|
])('should work with $extensionName', ({ extension, extensionName }) => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env.DB_VECTOR_EXTENSION = extensionName;
|
configMock.getEnv.mockReturnValue({ database: { skipMigrations: false, vectorExtension: extension } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should start up successfully with ${extension}`, async () => {
|
it(`should start up successfully with ${extension}`, async () => {
|
||||||
@ -236,18 +241,28 @@ describe(DatabaseService.name, () => {
|
|||||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
||||||
process.env.DB_SKIP_MIGRATIONS = 'true';
|
configMock.getEnv.mockReturnValue({
|
||||||
|
database: {
|
||||||
|
skipMigrations: true,
|
||||||
|
vectorExtension: DatabaseExtension.VECTORS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it(`should throw error if pgvector extension could not be created`, async () => {
|
it(`should throw error if pgvector extension could not be created`, async () => {
|
||||||
process.env.DB_VECTOR_EXTENSION = 'pgvector';
|
configMock.getEnv.mockReturnValue({
|
||||||
|
database: {
|
||||||
|
skipMigrations: true,
|
||||||
|
vectorExtension: DatabaseExtension.VECTOR,
|
||||||
|
},
|
||||||
|
});
|
||||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||||
installedVersion: null,
|
installedVersion: null,
|
||||||
availableVersion: minVersionInRange,
|
availableVersion: minVersionInRange,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { getVectorExtension } from 'src/database.config';
|
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import {
|
import {
|
||||||
DatabaseExtension,
|
DatabaseExtension,
|
||||||
DatabaseLock,
|
DatabaseLock,
|
||||||
@ -67,6 +67,7 @@ export class DatabaseService {
|
|||||||
private reconnection?: NodeJS.Timeout;
|
private reconnection?: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
@ -85,7 +86,8 @@ export class DatabaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => {
|
await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => {
|
||||||
const extension = getVectorExtension();
|
const envData = this.configRepository.getEnv();
|
||||||
|
const extension = envData.database.vectorExtension;
|
||||||
const name = EXTENSION_NAMES[extension];
|
const name = EXTENSION_NAMES[extension];
|
||||||
const extensionRange = this.databaseRepository.getExtensionVersionRange(extension);
|
const extensionRange = this.databaseRepository.getExtensionVersionRange(extension);
|
||||||
|
|
||||||
@ -116,7 +118,8 @@ export class DatabaseService {
|
|||||||
|
|
||||||
await this.checkReindexing();
|
await this.checkReindexing();
|
||||||
|
|
||||||
if (process.env.DB_SKIP_MIGRATIONS !== 'true') {
|
const { database } = this.configRepository.getEnv();
|
||||||
|
if (!database.skipMigrations) {
|
||||||
await this.databaseRepository.runMigrations();
|
await this.databaseRepository.runMigrations();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,23 @@
|
|||||||
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
|
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
|
import { GeneratedImageType, StorageCore } from 'src/cores/storage.core';
|
||||||
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import {
|
import {
|
||||||
|
AssetFileType,
|
||||||
|
AssetPathType,
|
||||||
|
AssetType,
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
ImageFormat,
|
LogLevel,
|
||||||
|
StorageFolder,
|
||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
TranscodeTarget,
|
TranscodeTarget,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
VideoContainer,
|
VideoContainer,
|
||||||
} from 'src/config';
|
} from 'src/enum';
|
||||||
import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
|
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import { AssetPathType } from 'src/entities/move.entity';
|
|
||||||
import { AssetFileType, AssetType } from 'src/enum';
|
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import {
|
import {
|
||||||
@ -29,7 +31,13 @@ import {
|
|||||||
QueueName,
|
QueueName,
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AudioStreamInfo, IMediaRepository, VideoFormat, VideoStreamInfo } from 'src/interfaces/media.interface';
|
import {
|
||||||
|
AudioStreamInfo,
|
||||||
|
IMediaRepository,
|
||||||
|
TranscodeCommand,
|
||||||
|
VideoFormat,
|
||||||
|
VideoStreamInfo,
|
||||||
|
} from 'src/interfaces/media.interface';
|
||||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
@ -166,18 +174,15 @@ export class MediaService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.previewFormat);
|
await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.preview.format);
|
||||||
await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
|
await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
|
||||||
await this.storageCore.moveAssetVideo(asset);
|
await this.storageCore.moveAssetVideo(asset);
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
|
async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [{ image }, [asset]] = await Promise.all([
|
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true, files: true });
|
||||||
this.configCore.getConfig({ withCache: true }),
|
|
||||||
this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
|
|
||||||
]);
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
@ -186,7 +191,7 @@ export class MediaService {
|
|||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
|
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW);
|
||||||
if (!previewPath) {
|
if (!previewPath) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@ -204,9 +209,9 @@ export class MediaService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
|
private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType) {
|
||||||
const { image, ffmpeg } = await this.configCore.getConfig({ withCache: true });
|
const { image, ffmpeg } = await this.configCore.getConfig({ withCache: true });
|
||||||
const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize;
|
const { size, format, quality } = image[type];
|
||||||
const path = StorageCore.getImagePath(asset, type, format);
|
const path = StorageCore.getImagePath(asset, type, format);
|
||||||
this.storageCore.ensureFolders(path);
|
this.storageCore.ensureFolders(path);
|
||||||
|
|
||||||
@ -217,13 +222,13 @@ export class MediaService {
|
|||||||
const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath));
|
const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize));
|
const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.preview.size));
|
||||||
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
|
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
|
||||||
const imageOptions = {
|
const imageOptions = {
|
||||||
format,
|
format,
|
||||||
size,
|
size,
|
||||||
colorspace,
|
colorspace,
|
||||||
quality: image.quality,
|
quality,
|
||||||
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
|
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -265,10 +270,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
|
async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [{ image }, [asset]] = await Promise.all([
|
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true, files: true });
|
||||||
this.configCore.getConfig({ withCache: true }),
|
|
||||||
this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
|
|
||||||
]);
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
@ -277,7 +279,7 @@ export class MediaService {
|
|||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
|
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL);
|
||||||
if (!thumbnailPath) {
|
if (!thumbnailPath) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@ -344,7 +346,9 @@ export class MediaService {
|
|||||||
const output = StorageCore.getEncodedVideoPath(asset);
|
const output = StorageCore.getEncodedVideoPath(asset);
|
||||||
this.storageCore.ensureFolders(output);
|
this.storageCore.ensureFolders(output);
|
||||||
|
|
||||||
const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input);
|
const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input, {
|
||||||
|
countFrames: this.logger.isLevelEnabled(LogLevel.DEBUG), // makes frame count more reliable for progress logs
|
||||||
|
});
|
||||||
const mainVideoStream = this.getMainStream(videoStreams);
|
const mainVideoStream = this.getMainStream(videoStreams);
|
||||||
const mainAudioStream = this.getMainStream(audioStreams);
|
const mainAudioStream = this.getMainStream(audioStreams);
|
||||||
if (!mainVideoStream || !format.formatName) {
|
if (!mainVideoStream || !format.formatName) {
|
||||||
@ -363,12 +367,14 @@ export class MediaService {
|
|||||||
this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
|
this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
|
||||||
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } });
|
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } });
|
||||||
await this.assetRepository.update({ id: asset.id, encodedVideoPath: null });
|
await this.assetRepository.update({ id: asset.id, encodedVideoPath: null });
|
||||||
|
} else {
|
||||||
|
this.logger.verbose(`Asset ${asset.id} does not require transcoding based on current policy, skipping`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
let command;
|
let command: TranscodeCommand;
|
||||||
try {
|
try {
|
||||||
const config = BaseConfig.create(ffmpeg, await this.getDevices(), await this.hasMaliOpenCL());
|
const config = BaseConfig.create(ffmpeg, await this.getDevices(), await this.hasMaliOpenCL());
|
||||||
command = config.getCommand(target, mainVideoStream, mainAudioStream);
|
command = config.getCommand(target, mainVideoStream, mainAudioStream);
|
||||||
@ -377,16 +383,20 @@ export class MediaService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Started encoding video ${asset.id} ${JSON.stringify(command)}`);
|
if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
|
||||||
|
this.logger.log(`Encoding video ${asset.id} without hardware acceleration`);
|
||||||
|
} else {
|
||||||
|
this.logger.log(`Encoding video ${asset.id} with ${ffmpeg.accel.toUpperCase()} acceleration`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.mediaRepository.transcode(input, output, command);
|
await this.mediaRepository.transcode(input, output, command);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
this.logger.error(error);
|
this.logger.error(`Error occurred during transcoding: ${error.message}`);
|
||||||
if (ffmpeg.accel !== TranscodeHWAccel.DISABLED) {
|
if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
|
||||||
this.logger.error(
|
return JobStatus.FAILED;
|
||||||
`Error occurred during transcoding. Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
|
||||||
const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED });
|
const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED });
|
||||||
command = config.getCommand(target, mainVideoStream, mainAudioStream);
|
command = config.getCommand(target, mainVideoStream, mainAudioStream);
|
||||||
await this.mediaRepository.transcode(input, output, command);
|
await this.mediaRepository.transcode(input, output, command);
|
||||||
@ -553,7 +563,7 @@ export class MediaService {
|
|||||||
const maliDeviceStat = await this.storageRepository.stat('/dev/mali0');
|
const maliDeviceStat = await this.storageRepository.stat('/dev/mali0');
|
||||||
this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
|
this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
|
||||||
} catch {
|
} catch {
|
||||||
this.logger.debug('OpenCL not available for transcoding, using CPU decoding instead.');
|
this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU decoding');
|
||||||
this.maliOpenCL = false;
|
this.maliOpenCL = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,7 +316,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle lists of numbers', async () => {
|
it('should handle lists of numbers', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
metadataMock.readTags.mockResolvedValue({ ISO: [160] as any });
|
metadataMock.readTags.mockResolvedValue({ ISO: [160] });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
@ -411,7 +411,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should extract tags from Keywords as a list with a number', async () => {
|
it('should extract tags from Keywords as a list with a number', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] as any[] });
|
metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] });
|
||||||
tagMock.upsertValue.mockResolvedValue(tagStub.parent);
|
tagMock.upsertValue.mockResolvedValue(tagStub.parent);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@ -467,6 +467,17 @@ describe(MetadataService.name, () => {
|
|||||||
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(3, { userId: 'user-id', value: 'TagA', parent: undefined });
|
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(3, { userId: 'user-id', value: 'TagA', parent: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent', 2024] });
|
||||||
|
tagMock.upsertValue.mockResolvedValue(tagStub.parent);
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined });
|
||||||
|
expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Mom/Dad'] });
|
metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Mom/Dad'] });
|
||||||
|
@ -11,6 +11,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core';
|
|||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { AssetType, SourceType } from 'src/enum';
|
import { AssetType, SourceType } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
@ -236,7 +237,7 @@ export class MetadataService {
|
|||||||
const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
|
const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
|
||||||
const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
|
const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
|
||||||
|
|
||||||
const exifData = {
|
const exifData: Partial<ExifEntity> = {
|
||||||
assetId: asset.id,
|
assetId: asset.id,
|
||||||
|
|
||||||
// dates
|
// dates
|
||||||
@ -264,7 +265,7 @@ export class MetadataService {
|
|||||||
make: exifTags.Make ?? null,
|
make: exifTags.Make ?? null,
|
||||||
model: exifTags.Model ?? null,
|
model: exifTags.Model ?? null,
|
||||||
fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)),
|
fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)),
|
||||||
iso: validate(exifTags.ISO),
|
iso: validate(exifTags.ISO) as number,
|
||||||
exposureTime: exifTags.ExposureTime ?? null,
|
exposureTime: exifTags.ExposureTime ?? null,
|
||||||
lensModel: exifTags.LensModel ?? null,
|
lensModel: exifTags.LensModel ?? null,
|
||||||
fNumber: validate(exifTags.FNumber),
|
fNumber: validate(exifTags.FNumber),
|
||||||
@ -395,13 +396,13 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) {
|
private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) {
|
||||||
const tags: Array<string | number> = [];
|
const tags: string[] = [];
|
||||||
if (exifTags.TagsList) {
|
if (exifTags.TagsList) {
|
||||||
tags.push(...exifTags.TagsList);
|
tags.push(...exifTags.TagsList.map(String));
|
||||||
} else if (exifTags.HierarchicalSubject) {
|
} else if (exifTags.HierarchicalSubject) {
|
||||||
tags.push(
|
tags.push(
|
||||||
...exifTags.HierarchicalSubject.map((tag) =>
|
...exifTags.HierarchicalSubject.map((tag) =>
|
||||||
tag
|
String(tag)
|
||||||
// convert | to /
|
// convert | to /
|
||||||
.replaceAll('/', '<PLACEHOLDER>')
|
.replaceAll('/', '<PLACEHOLDER>')
|
||||||
.replaceAll('|', '/')
|
.replaceAll('|', '/')
|
||||||
@ -413,10 +414,10 @@ export class MetadataService {
|
|||||||
if (!Array.isArray(keywords)) {
|
if (!Array.isArray(keywords)) {
|
||||||
keywords = [keywords];
|
keywords = [keywords];
|
||||||
}
|
}
|
||||||
tags.push(...keywords);
|
tags.push(...keywords.map(String));
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags: tags.map(String) });
|
const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags });
|
||||||
await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds: results.map((tag) => tag.id) });
|
await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds: results.map((tag) => tag.id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
import { Colorspace } from 'src/config';
|
|
||||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
|
import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { SourceType, SystemMetadataKey } from 'src/enum';
|
import { CacheControl, Colorspace, SourceType, SystemMetadataKey } from 'src/enum';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
@ -16,7 +15,7 @@ import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.inter
|
|||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { PersonService } from 'src/services/person.service';
|
import { PersonService } from 'src/services/person.service';
|
||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { faceStub } from 'test/fixtures/face.stub';
|
import { faceStub } from 'test/fixtures/face.stub';
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { ImageFormat } from 'src/config';
|
|
||||||
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
|
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
@ -23,9 +22,16 @@ import {
|
|||||||
} from 'src/dtos/person.dto';
|
} from 'src/dtos/person.dto';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { PersonPathType } from 'src/entities/move.entity';
|
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { AssetType, Permission, SourceType, SystemMetadataKey } from 'src/enum';
|
import {
|
||||||
|
AssetType,
|
||||||
|
CacheControl,
|
||||||
|
ImageFormat,
|
||||||
|
Permission,
|
||||||
|
PersonPathType,
|
||||||
|
SourceType,
|
||||||
|
SystemMetadataKey,
|
||||||
|
} from 'src/enum';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
@ -51,7 +57,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
|
import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
@ -568,7 +574,7 @@ export class PersonService {
|
|||||||
format: ImageFormat.JPEG,
|
format: ImageFormat.JPEG,
|
||||||
size: FACE_THUMBNAIL_SIZE,
|
size: FACE_THUMBNAIL_SIZE,
|
||||||
colorspace: image.colorspace,
|
colorspace: image.colorspace,
|
||||||
quality: image.quality,
|
quality: image.thumbnail.quality,
|
||||||
crop: this.getCrop({ old: { width: oldWidth, height: oldHeight }, new: { width, height } }, { x1, y1, x2, y2 }),
|
crop: this.getCrop({ old: { width: oldWidth, height: oldHeight }, new: { width, height } }, { x1, y1, x2, y2 }),
|
||||||
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
|
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
|
import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
|
||||||
import { serverVersion } from 'src/constants';
|
import { serverVersion } from 'src/constants';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
ServerStorageResponseDto,
|
ServerStorageResponseDto,
|
||||||
UsageByUserDto,
|
UsageByUserDto,
|
||||||
} from 'src/dtos/server.dto';
|
} from 'src/dtos/server.dto';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
|
@ -2,7 +2,7 @@ import { Stats } from 'node:fs';
|
|||||||
import { SystemConfig, defaults } from 'src/config';
|
import { SystemConfig, defaults } from 'src/config';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetPathType } from 'src/entities/move.entity';
|
import { AssetPathType } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
|
@ -13,12 +13,11 @@ import {
|
|||||||
supportedWeekTokens,
|
supportedWeekTokens,
|
||||||
supportedYearTokens,
|
supportedYearTokens,
|
||||||
} from 'src/constants';
|
} from 'src/constants';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetPathType } from 'src/entities/move.entity';
|
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
||||||
import { AssetType } from 'src/enum';
|
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
|
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { defaults, SystemConfig } from 'src/config';
|
||||||
import {
|
import {
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
CQMode,
|
|
||||||
Colorspace,
|
Colorspace,
|
||||||
|
CQMode,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
SystemConfig,
|
SystemMetadataKey,
|
||||||
ToneMapping,
|
ToneMapping,
|
||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
VideoContainer,
|
VideoContainer,
|
||||||
defaults,
|
} from 'src/enum';
|
||||||
} from 'src/config';
|
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
|
||||||
import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
|
import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
|
||||||
import { QueueName } from 'src/interfaces/job.interface';
|
import { QueueName } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -136,11 +135,16 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
thumbnailFormat: ImageFormat.WEBP,
|
thumbnail: {
|
||||||
thumbnailSize: 250,
|
size: 250,
|
||||||
previewFormat: ImageFormat.JPEG,
|
format: ImageFormat.WEBP,
|
||||||
previewSize: 1440,
|
|
||||||
quality: 80,
|
quality: 80,
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
size: 1440,
|
||||||
|
format: ImageFormat.JPEG,
|
||||||
|
quality: 80,
|
||||||
|
},
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
extractEmbedded: false,
|
extractEmbedded: false,
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { instanceToPlain } from 'class-transformer';
|
import { instanceToPlain } from 'class-transformer';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { LogLevel, SystemConfig, defaults } from 'src/config';
|
import { SystemConfig, defaults } from 'src/config';
|
||||||
import {
|
import {
|
||||||
supportedDayTokens,
|
supportedDayTokens,
|
||||||
supportedHourTokens,
|
supportedHourTokens,
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { OnEmit, OnServerEvent } from 'src/decorators';
|
import { OnEmit, OnServerEvent } from 'src/decorators';
|
||||||
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
|
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
|
||||||
|
import { LogLevel } from 'src/enum';
|
||||||
import { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
|
import { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { UserMetadataKey } from 'src/enum';
|
import { CacheControl, UserMetadataKey } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||||
@ -9,7 +9,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
|
@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nes
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
|
import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
@ -11,7 +11,7 @@ import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
|||||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { UserMetadataKey } from 'src/enum';
|
import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
@ -19,7 +19,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { ModuleRef, Reflector } from '@nestjs/core';
|
import { ModuleRef, Reflector } from '@nestjs/core';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { EmitConfig } from 'src/decorators';
|
import { EmitConfig } from 'src/decorators';
|
||||||
|
import { MetadataKey } from 'src/enum';
|
||||||
import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface';
|
import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { Metadata } from 'src/middleware/auth.guard';
|
|
||||||
import { services } from 'src/services';
|
import { services } from 'src/services';
|
||||||
|
|
||||||
type Item<T extends EmitEvent> = {
|
type Item<T extends EmitEvent> = {
|
||||||
@ -35,7 +35,7 @@ export const setupEventHandlers = (moduleRef: ModuleRef) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = reflector.get<EmitConfig>(Metadata.ON_EMIT_CONFIG, handler);
|
const options = reflector.get<EmitConfig>(MetadataKey.ON_EMIT_CONFIG, handler);
|
||||||
if (!options) {
|
if (!options) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { NextFunction, Response } from 'express';
|
|||||||
import { access, constants } from 'node:fs/promises';
|
import { access, constants } from 'node:fs/promises';
|
||||||
import { basename, extname, isAbsolute } from 'node:path';
|
import { basename, extname, isAbsolute } from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
import { CacheControl } from 'src/enum';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ImmichReadStream } from 'src/interfaces/storage.interface';
|
import { ImmichReadStream } from 'src/interfaces/storage.interface';
|
||||||
import { isConnectionAborted } from 'src/utils/misc';
|
import { isConnectionAborted } from 'src/utils/misc';
|
||||||
@ -19,12 +20,6 @@ export function getLivePhotoMotionFilename(stillName: string, motionName: string
|
|||||||
return getFileNameWithoutExtension(stillName) + extname(motionName);
|
return getFileNameWithoutExtension(stillName) + extname(motionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CacheControl {
|
|
||||||
PRIVATE_WITH_CACHE = 'private_with_cache',
|
|
||||||
PRIVATE_WITHOUT_CACHE = 'private_without_cache',
|
|
||||||
NONE = 'none',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ImmichFileResponse {
|
export class ImmichFileResponse {
|
||||||
public readonly path!: string;
|
public readonly path!: string;
|
||||||
public readonly contentType!: string;
|
public readonly contentType!: string;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/config';
|
|
||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
|
import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/enum';
|
||||||
import {
|
import {
|
||||||
AudioStreamInfo,
|
AudioStreamInfo,
|
||||||
BitrateDistribution,
|
BitrateDistribution,
|
||||||
@ -80,6 +80,7 @@ export class BaseConfig implements VideoCodecSWConfig {
|
|||||||
inputOptions: this.getBaseInputOptions(videoStream),
|
inputOptions: this.getBaseInputOptions(videoStream),
|
||||||
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
|
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
|
||||||
twoPass: this.eligibleForTwoPass(),
|
twoPass: this.eligibleForTwoPass(),
|
||||||
|
progress: { frameCount: videoStream.frameCount, percentInterval: 5 },
|
||||||
} as TranscodeCommand;
|
} as TranscodeCommand;
|
||||||
if ([TranscodeTarget.ALL, TranscodeTarget.VIDEO].includes(target)) {
|
if ([TranscodeTarget.ALL, TranscodeTarget.VIDEO].includes(target)) {
|
||||||
const filters = this.getFilterOptions(videoStream);
|
const filters = this.getFilterOptions(videoStream);
|
||||||
|
@ -13,8 +13,8 @@ import path from 'node:path';
|
|||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { CLIP_MODEL_INFO, isDev, serverVersion } from 'src/constants';
|
import { CLIP_MODEL_INFO, isDev, serverVersion } from 'src/constants';
|
||||||
import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto';
|
import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto';
|
||||||
|
import { MetadataKey } from 'src/enum';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { Metadata } from 'src/middleware/auth.guard';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns a list of strings representing the keys of the object in dot notation
|
* @returns a list of strings representing the keys of the object in dot notation
|
||||||
@ -210,7 +210,7 @@ export const useSwagger = (app: INestApplication, force = false) => {
|
|||||||
in: 'header',
|
in: 'header',
|
||||||
name: ImmichHeader.API_KEY,
|
name: ImmichHeader.API_KEY,
|
||||||
},
|
},
|
||||||
Metadata.API_KEY_SECURITY,
|
MetadataKey.API_KEY_SECURITY,
|
||||||
)
|
)
|
||||||
.addServer('/api')
|
.addServer('/api')
|
||||||
.build();
|
.build();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { PaginationMode } from 'src/enum';
|
||||||
import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
|
import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
export interface PaginationOptions {
|
export interface PaginationOptions {
|
||||||
@ -6,11 +7,6 @@ export interface PaginationOptions {
|
|||||||
skip?: number;
|
skip?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PaginationMode {
|
|
||||||
LIMIT_OFFSET = 'limit-offset',
|
|
||||||
SKIP_TAKE = 'skip-take',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginatedBuilderOptions {
|
export interface PaginatedBuilderOptions {
|
||||||
take: number;
|
take: number;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
|
14
server/test/repositories/config.repository.mock.ts
Normal file
14
server/test/repositories/config.repository.mock.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
|
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||||
|
import { Mocked, vitest } from 'vitest';
|
||||||
|
|
||||||
|
export const newConfigRepositoryMock = (): Mocked<IConfigRepository> => {
|
||||||
|
return {
|
||||||
|
getEnv: vitest.fn().mockReturnValue({
|
||||||
|
database: {
|
||||||
|
skipMigration: false,
|
||||||
|
vectorExtension: DatabaseExtension.VECTORS,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
@ -6,7 +6,7 @@ export const newLoggerRepositoryMock = (): Mocked<ILoggerRepository> => {
|
|||||||
setLogLevel: vitest.fn(),
|
setLogLevel: vitest.fn(),
|
||||||
setContext: vitest.fn(),
|
setContext: vitest.fn(),
|
||||||
setAppName: vitest.fn(),
|
setAppName: vitest.fn(),
|
||||||
|
isLevelEnabled: vitest.fn(),
|
||||||
verbose: vitest.fn(),
|
verbose: vitest.fn(),
|
||||||
debug: vitest.fn(),
|
debug: vitest.fn(),
|
||||||
log: vitest.fn(),
|
log: vitest.fn(),
|
||||||
|
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/icu-messageformat-parser": "^2.7.8",
|
"@formatjs/icu-messageformat-parser": "^2.7.8",
|
||||||
@ -74,7 +74,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.116.0",
|
"version": "1.116.2",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||||
|
@ -6,6 +6,12 @@ interface Options {
|
|||||||
onEscape?: () => void;
|
onEscape?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls a function when a click occurs outside of the element, or when the escape key is pressed.
|
||||||
|
* @param node
|
||||||
|
* @param options Object containing onOutclick and onEscape functions
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export function clickOutside(node: HTMLElement, options: Options = {}): ActionReturn {
|
export function clickOutside(node: HTMLElement, options: Options = {}): ActionReturn {
|
||||||
const { onOutclick, onEscape } = options;
|
const { onOutclick, onEscape } = options;
|
||||||
|
|
||||||
|
@ -2,6 +2,11 @@ interface Options {
|
|||||||
onFocusOut?: (event: FocusEvent) => void;
|
onFocusOut?: (event: FocusEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls a function when focus leaves the element.
|
||||||
|
* @param node
|
||||||
|
* @param options Object containing onFocusOut function
|
||||||
|
*/
|
||||||
export function focusOutside(node: HTMLElement, options: Options = {}) {
|
export function focusOutside(node: HTMLElement, options: Options = {}) {
|
||||||
const { onFocusOut } = options;
|
const { onFocusOut } = options;
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/** Focus the given element when it is mounted. */
|
||||||
export const initInput = (element: HTMLInputElement) => {
|
export const initInput = (element: HTMLInputElement) => {
|
||||||
element.focus();
|
element.focus();
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,9 @@ type OnIntersectCallback = (entryOrElement: IntersectionObserverEntry | HTMLElem
|
|||||||
type OnSeparateCallback = (element: HTMLElement) => unknown;
|
type OnSeparateCallback = (element: HTMLElement) => unknown;
|
||||||
type IntersectionObserverActionProperties = {
|
type IntersectionObserverActionProperties = {
|
||||||
key?: string;
|
key?: string;
|
||||||
|
/** Function to execute when the element leaves the viewport */
|
||||||
onSeparate?: OnSeparateCallback;
|
onSeparate?: OnSeparateCallback;
|
||||||
|
/** Function to execute when the element enters the viewport */
|
||||||
onIntersect?: OnIntersectCallback;
|
onIntersect?: OnIntersectCallback;
|
||||||
|
|
||||||
root?: Element | Document | null;
|
root?: Element | Document | null;
|
||||||
@ -112,6 +114,12 @@ function _intersectionObserver(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitors an element's visibility in the viewport and calls functions when it enters or leaves (based on a threshold).
|
||||||
|
* @param element
|
||||||
|
* @param properties One or multiple configurations for the IntersectionObserver(s)
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export function intersectionObserver(
|
export function intersectionObserver(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
properties: IntersectionObserverActionProperties | IntersectionObserverActionProperties[],
|
properties: IntersectionObserverActionProperties | IntersectionObserverActionProperties[],
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user