Author SHA1 Message Date
Codex 8119a5ee22 Finalize local12 formatting and document guarded browser APIs 2026-10-27 23:27:18 +03:00
Codex 54881128d6 Adapt local features and dependency locks for SvelteKit 3 and local12 2026-10-27 23:17:33 +03:00
renovate[bot]andCodex d15be0dd83 chore(deps): update rust crate libc to v0.2.191 (#32753)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 12381a3b4bca8634f785c23cd4c6bf4063477b3b)
2026-10-27 23:12:52 +03:00
Eduard Ereza MartínezandCodex 47cc38061e fix(docs): update README_ca_ES.md to match the English version (#30300)
* Update README_ca_ES.md to match the English version

* fix(docs): update README_ca_ES.md to match the English version

(cherry picked from commit 0fe886d1b573d579fdb3fd243a477e0e6ac08f52)
2026-10-27 23:12:52 +03:00
MertandCodex db411a4497 feat(server): add jxl as target format (#32732)
(cherry picked from commit 9560989cb97fb93cd6ab74f7811acce133a9b768)
2026-10-27 23:12:52 +03:00
renovate[bot]andCodex f1a09c1389 chore(deps): update dependency fastapi to v0.160.0 (#32756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit cadb945108d7a547845ba5279c745fca64ba2cf8)
2026-10-27 23:12:52 +03:00
renovate[bot]andCodex c8222bba94 chore(deps): update dependency terragrunt to v1.2.0 (#32757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit e50ada17c817499a131eefb58a112850dfd22ea6)
2026-10-27 23:12:51 +03:00
TimonandCodex af66681b99 chore: bump openapi-generator version to v7.26 (#32454)
(cherry picked from commit b3c1b3d260b0186a9d6e82277f0e57a6e6d88738)
2026-10-27 23:12:42 +03:00
bo0tzzandCodex a93bbdf3e7 feat: inline hashes in helmet CSP (#30529)
* feat: inline hashes in helmet CSP

* tests

* fix: svelte3 merge

* chore: clean up

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
(cherry picked from commit 02a5d4b2060dad6f5779c196b7bb2b43f871ffb2)
2026-10-27 23:12:41 +03:00
bo0tzzandCodex 1707635743 fix(server): require update access on the motion video when linking a live photo (#32606)
* fix(server): require update access on the motion video when linking a live photo

* chore: allow linking with existing shared link assets

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
(cherry picked from commit a7afb4c9e9d2f099f9204a41f62e17b54fca3b17)
2026-10-27 23:12:41 +03:00
109 changed files with 1206 additions and 808 deletions
+21 -26
View File
@@ -35,40 +35,35 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742635"
[[tools.terragrunt]]
version = "1.0.3"
version = "1.2.0"
backend = "aqua:gruntwork-io/terragrunt"
[tools.terragrunt."platforms.linux-arm64"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
[tools.terragrunt."platforms.linux-arm64-musl"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
checksum = "sha256:1e13b7f0646fa7962bc6c5bfba14a30d6a589325b82561e21b66be5981d86395"
checksum_source = "mise-versions"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437526"
[tools.terragrunt."platforms.linux-x64"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
[tools.terragrunt."platforms.linux-x64-musl"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
checksum = "sha256:23b5f2f9203d34919f7fd270a4b635b721b4903beca9762a1e4853b159bc9be8"
checksum_source = "mise-versions"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437263"
[tools.terragrunt."platforms.macos-arm64"]
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654084"
checksum = "sha256:79790d841ce71fe94c689fb6f5034afd31ad53d5904937c7a10934cca9d9734b"
checksum_source = "mise-versions"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436516"
[tools.terragrunt."platforms.macos-x64"]
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406653970"
checksum = "sha256:0b0e63e80509eb1ffb4f89b892bc6fbf7728f27f0a73974400f1bd82a80b8627"
checksum_source = "mise-versions"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436142"
[tools.terragrunt."platforms.windows-x64"]
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654412"
checksum = "sha256:c9a146b9aad456a289614023ad5778123d819af3cf9efd1c6130f196cfcb1999"
checksum_source = "mise-versions"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_windows_amd64.exe.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662438255"
+1 -1
View File
@@ -1,5 +1,5 @@
[tools]
terragrunt = "1.1.6"
terragrunt = "1.2.0"
opentofu = "1.13.2"
[tasks."tg:fmt"]
+1 -1
View File
@@ -355,7 +355,7 @@ describe('/asset', () => {
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ livePhotoVideoId: asset.id });
expect(body).toEqual(errorDto.badRequest('Live photo video does not belong to the user'));
expect(body).toEqual(errorDto.badRequest('Not found or no asset.update access'));
expect(status).toEqual(400);
});
+2 -2
View File
@@ -87,7 +87,7 @@
"failed_job_command": "Command {command} failed for job: {job}",
"force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.",
"image_format": "Format",
"image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.",
"image_format_description": "WebP produces smaller files than JPEG, but is slower to encode. JPEG XL is the best option for high-quality compression, but not all browsers and apps can display it.",
"image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in",
"image_fullsize_enabled": "Enable full-size image generation",
"image_fullsize_enabled_description": "Generate full-size image for non-web-friendly formats. When \"Prefer embedded preview\" is enabled, embedded previews are used directly without conversion. Does not affect web-friendly formats like JPEG.",
@@ -101,7 +101,7 @@
"image_preview_quality_description": "Preview quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness. Setting a low value may affect machine learning quality.",
"image_preview_title": "Preview Settings",
"image_progressive": "Progressive",
"image_progressive_description": "Encode JPEG images progressively for gradual loading display. This has no effect on WebP images.",
"image_progressive_description": "Encode JPEG images progressively for gradual loading display. This has no effect on other formats.",
"image_quality": "Quality",
"image_resolution": "Resolution",
"image_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes and can reduce app responsiveness.",
@@ -7,10 +7,14 @@ import numpy as np
import orjson
from numpy.typing import NDArray
from PIL import Image
from pillow_jxl import JpegXLImagePlugin
_PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling}
_PUNCTUATION_TRANS = str.maketrans("", "", string.punctuation)
# Requests are already decoded in parallel, so don't start a thread per core for each one
JpegXLImagePlugin.DECODE_THREADS = 0
def resize_pil(img: Image.Image, size: int, resample: Image.Resampling = Image.Resampling.BICUBIC) -> Image.Image:
if img.width < img.height:
+1
View File
@@ -15,6 +15,7 @@ dependencies = [
"opencv-python-headless>=4.7.0.72,<5.0",
"orjson>=3.9.5",
"pillow>=13,<14",
"pillow-jxl-plugin>=1.3.8,<2.0",
"pydantic>=2.0.0,<3",
"pydantic-settings>=2.5.2,<3",
"python-multipart>=0.0.6,<1.0",
+53 -3
View File
@@ -436,7 +436,7 @@ wheels = [
[[package]]
name = "fastapi"
version = "0.148.0"
version = "0.162.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
@@ -446,9 +446,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/da/e962004c32f34fb630e8393456cadcd08d4300b6fa0099ec0ae5052b8418/fastapi-0.148.0.tar.gz", hash = "sha256:06ee16c50e429daf993a58ecbb5306d284c4c1ee8135380af39a5e1cfa715187", size = 482594, upload-time = "2026-10-19T12:26:05.615Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e9/eb/2cf8c80abf09b2800aef0a83b41066fbfda8ee5495b7a08b9d1af40f7046/fastapi-0.162.0.tar.gz", hash = "sha256:6a22c1e2e6e32e0633ee7e170f00009a29b8228b16feb219ee42f32fd8d88a8f", size = 527867, upload-time = "2026-10-23T18:47:33.264Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/8e/7d284c1f06093a5e96e1704369a4053b93a9cf4d41b5b40f5dbf94bde819/fastapi-0.148.0-py3-none-any.whl", hash = "sha256:2848088aa482c875c180ef92b7139fec345393d09cdcf258ba4b8555ab068a3b", size = 145110, upload-time = "2026-10-19T12:26:03.9Z" },
{ url = "https://files.pythonhosted.org/packages/b4/4f/3f2c25c9b43970b5e9a08beb5e04776247edea684ddc0930f4f02f0bb6cb/fastapi-0.162.0-py3-none-any.whl", hash = "sha256:3785963de305dd09912426cb81f2d230fce98ab3521cdfe97265c8d14983128c", size = 148912, upload-time = "2026-10-23T18:47:31.734Z" },
]
[[package]]
@@ -842,6 +842,7 @@ dependencies = [
{ name = "opencv-python-headless" },
{ name = "orjson" },
{ name = "pillow" },
{ name = "pillow-jxl-plugin" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "python-multipart" },
@@ -936,6 +937,7 @@ requires-dist = [
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
{ name = "orjson", specifier = ">=3.9.5" },
{ name = "pillow", specifier = ">=13,<14" },
{ name = "pillow-jxl-plugin", specifier = ">=1.3.8,<2.0" },
{ name = "pydantic", specifier = ">=2.0.0,<3" },
{ name = "pydantic-settings", specifier = ">=2.5.2,<3" },
{ name = "python-multipart", specifier = ">=0.0.6,<1.0" },
@@ -1802,6 +1804,54 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/37/66b12dc476041fe523bca29c9d58fbf1ea92a4eaf73acfb9e993948f1191/pillow-13.0.0-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:567f20f1c75897d7663830da6ea8af719e03d712b1d9ee7f813319f9ece42e5a", size = 7366030, upload-time = "2026-10-16T22:18:06.638Z" },
]
[[package]]
name = "pillow-jxl-plugin"
version = "1.3.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "pillow" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/75/810608d489c2d453e16a0866de5edea2bedd728e8830da22a1ec633f3b80/pillow_jxl_plugin-1.3.8.tar.gz", hash = "sha256:4430fd7757899742079c548a7d2537d6d63cf0f1d323180095bc0f6f03d9d4fa", size = 1602233, upload-time = "2026-07-17T03:23:50.387Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/bb/093aced52ebaafcd050eb21ab07ddcab7efffaa687e264f64d3f0423a4ba/pillow_jxl_plugin-1.3.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f6fd0fa12dd0d98bb55df6bbfff7ad913e4ea27306fbdf5e8543fe1fa40772c", size = 2361426, upload-time = "2026-07-17T03:22:56.906Z" },
{ url = "https://files.pythonhosted.org/packages/8e/9f/f272fd205ab511475b135772e85bcaa71e5f3d081e6604b08e6e1d3982a6/pillow_jxl_plugin-1.3.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01b632dbca07b59f7d6e106dad3ac8391e9ffdab1f94f2068a3e8edff1bbd180", size = 2660368, upload-time = "2026-07-17T03:22:48.663Z" },
{ url = "https://files.pythonhosted.org/packages/74/1a/3445d2b4f6d6e4e26f10e9ccd75b60a14295b2d8eb280f0b1bd0bd7caac3/pillow_jxl_plugin-1.3.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6459d131af2cbccab20742f6fe43a3022b021dcd1ee20b05f99b1fb54d0ec525", size = 2788244, upload-time = "2026-07-17T03:22:21.809Z" },
{ url = "https://files.pythonhosted.org/packages/26/f2/3855e83fd1e4043bd7ab286d3f0ca434fbf935598fae57f1af2777620891/pillow_jxl_plugin-1.3.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b9a546ac71757f53f0ca2b14ea8f99c33b70e298a7873477f4548e2d83f817", size = 2500550, upload-time = "2026-07-17T03:22:30.993Z" },
{ url = "https://files.pythonhosted.org/packages/f0/13/d9befae0e705474e550b16c90bb6d33c3f07837af63a0e8ce4c84fb546fb/pillow_jxl_plugin-1.3.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd2afd5153e2a7ef230a3e48a679a9e1cb3e59032f79c78bb2f78010a4e9bfc1", size = 2504336, upload-time = "2026-07-17T03:22:40.143Z" },
{ url = "https://files.pythonhosted.org/packages/52/2c/3e671c8dd84f6ed4e47d588df1dc15aae61b0df0302f4bfff7b5c743045b/pillow_jxl_plugin-1.3.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b987fb202dd1c71d7f6e103363afd1128d347ffcf540d8416c257c8f1041ff3", size = 9727025, upload-time = "2026-07-17T03:23:06.554Z" },
{ url = "https://files.pythonhosted.org/packages/27/89/a63b75d9c05deb0017ff51ddf3674cef19dbfe9c2fb74117d7e4a66b9a93/pillow_jxl_plugin-1.3.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c05cf62a992f48434a6b876fe010e5cae12bd54039854ce9ae0f05c13a58c7ad", size = 8386443, upload-time = "2026-07-17T03:23:19.195Z" },
{ url = "https://files.pythonhosted.org/packages/c7/4f/b6ead8f5a50103e2c6b264a6d4778b8fcd08006c957b2ad0bccbbb306e97/pillow_jxl_plugin-1.3.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0392e5349b67522130ecff9d8dca4713e4c84662a310d2170291b229965e8fd1", size = 9032022, upload-time = "2026-07-17T03:23:30.993Z" },
{ url = "https://files.pythonhosted.org/packages/ea/53/b9aa7c156427ad4d94d8c90bd87cb502e7d6326d2689c309aa574baf1ab7/pillow_jxl_plugin-1.3.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:88b06a4e5fc0d882b188824990b6571e201ad775d22a09487c8d5ab8aed9c3b7", size = 9233286, upload-time = "2026-07-17T03:23:42.022Z" },
{ url = "https://files.pythonhosted.org/packages/32/e3/b25194909cfef9081f6a1f0e40e699e95ea96b211cae30bbf0f965d6b450/pillow_jxl_plugin-1.3.8-cp312-cp312-win32.whl", hash = "sha256:3345a45a09ba3149001c3e9a1b6991be8aab72c33097c0633e414a254cd283fa", size = 4552597, upload-time = "2026-07-17T03:24:08.129Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c9/01efd23cb4827f8a1731e1a7652023a06375e933f85e3169e0761d916d6f/pillow_jxl_plugin-1.3.8-cp312-cp312-win_amd64.whl", hash = "sha256:5b1c0d7a0c7e451014b288f0504545bff0f543f2ee1581af628e01e885b75f50", size = 4937252, upload-time = "2026-07-17T03:24:00.294Z" },
{ url = "https://files.pythonhosted.org/packages/db/ef/b3aee5725df66399f529c41d97f71893e2b38f665c9278a99519112f03f3/pillow_jxl_plugin-1.3.8-cp312-cp312-win_arm64.whl", hash = "sha256:230a1d086ef5b66055e0dbc9837914becf1d1ca58d45d1404660de0a11fb63c3", size = 2330507, upload-time = "2026-07-17T03:23:53.22Z" },
{ url = "https://files.pythonhosted.org/packages/b9/10/18bb2d6ed82184e2427f7501a17c1b413eb50bdcd4ebb42bbfb187bb4290/pillow_jxl_plugin-1.3.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:fe24e957016dcb0c2ec35f72ff3bb8ae78a9468b282f0d423485556de492975c", size = 2361184, upload-time = "2026-07-17T03:22:58.376Z" },
{ url = "https://files.pythonhosted.org/packages/89/3e/5e1156e5c5b402d84ef924153e046ba937ac46e8b14decb3ab6e01a6f40e/pillow_jxl_plugin-1.3.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d648ef965002bee0c98dbf144b9b3c994146856fa4d3c54008b75dda10f69915", size = 2659163, upload-time = "2026-07-17T03:22:50.003Z" },
{ url = "https://files.pythonhosted.org/packages/e9/fe/e610196142343f80c98502fd204f28e901a1101db6988be6ccc40c0e8ca0/pillow_jxl_plugin-1.3.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34685b27d01e55aabb09d9436d4ddb1f446e8805da82ffd6448c07bbf563354c", size = 2787568, upload-time = "2026-07-17T03:22:23.469Z" },
{ url = "https://files.pythonhosted.org/packages/6e/18/a862b140f82be55ce62c681b63c38bf61c962f0c27e32797ebebc0d47548/pillow_jxl_plugin-1.3.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:288eb07090c91891ee0dc782786d937af8e53a117835606d7982c29e31072b90", size = 2499957, upload-time = "2026-07-17T03:22:32.414Z" },
{ url = "https://files.pythonhosted.org/packages/59/29/d430cd5f82c08d2950e292d195081e9dd4e2a038a7e1722d79ba7c6e7ed9/pillow_jxl_plugin-1.3.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b39ac44cbe5a98b8a364ed98a16c8c1e197ee997e32731be4be727c55e629e4", size = 2504367, upload-time = "2026-07-17T03:22:41.501Z" },
{ url = "https://files.pythonhosted.org/packages/f4/68/6806df96fdd625943c795beca932ac033c255a323b63b051903df96935b6/pillow_jxl_plugin-1.3.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0e1550f30768a0aa3f756da10b7807f6fac7fd2e5a18991eb8e00ec5908089d8", size = 9726010, upload-time = "2026-07-17T03:23:08.579Z" },
{ url = "https://files.pythonhosted.org/packages/12/df/d12f42d6e31bb47e2002fcba2db9d92aad19dc3db9ae7d4b2b76152e268a/pillow_jxl_plugin-1.3.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b13f860c280d9d5770f49a1909926f15c6e26e50935da52ecb78700f7c377857", size = 8386305, upload-time = "2026-07-17T03:23:20.983Z" },
{ url = "https://files.pythonhosted.org/packages/92/ee/4dc11ad5a90aa6d2eeae6eb0be2376cde0c365423036a078746b3476eca2/pillow_jxl_plugin-1.3.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:273908ffb85f68a2374b0934700939c572f32b355c9d128d4e3b6924e72ef274", size = 9031514, upload-time = "2026-07-17T03:23:32.948Z" },
{ url = "https://files.pythonhosted.org/packages/ae/76/b3125f13c9551862ad3ae7137d86959cde159b759083f3e7b5e720525fd0/pillow_jxl_plugin-1.3.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2be721646718af722f2ca5c1d6b06bf056a61a0db355cf944df53c3723119d4b", size = 9233009, upload-time = "2026-07-17T03:23:44.276Z" },
{ url = "https://files.pythonhosted.org/packages/13/16/1aa669ae22a6c36810a925bbbfb1f4521df1d0402180052331b39ef8dee0/pillow_jxl_plugin-1.3.8-cp313-cp313-win32.whl", hash = "sha256:a2716f1367ed9647da36ce15882a12b4d424892cb5a03ab4b0783039d701c7c2", size = 4551381, upload-time = "2026-07-17T03:24:09.552Z" },
{ url = "https://files.pythonhosted.org/packages/0e/e7/823f58aaea0d969f876f593a39c85c39fefe1b8ee9ca10a6ffbafe1a254a/pillow_jxl_plugin-1.3.8-cp313-cp313-win_amd64.whl", hash = "sha256:2388374b3493df9add4ef3d97cc70d6a5f51ed188dfe10d2b83ebc55e82e48d4", size = 4936720, upload-time = "2026-07-17T03:24:01.9Z" },
{ url = "https://files.pythonhosted.org/packages/3e/12/e04b73832d1b8af3c5c4646127949a362f9ca3d30f42b05156ca5ffc61ea/pillow_jxl_plugin-1.3.8-cp313-cp313-win_arm64.whl", hash = "sha256:48d13d81d1c4933e5d42dd09366a9827583808be0a2fe842abd7358653c7d92f", size = 2330087, upload-time = "2026-07-17T03:23:54.604Z" },
{ url = "https://files.pythonhosted.org/packages/66/30/c37d161345302ebda07f29d8230b15ba83b43c586f14fec98d088b18e982/pillow_jxl_plugin-1.3.8-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:30b72accb8505a46b05f709871da40f09975aea3a6b6f3cb1d646ff0a9aaba5e", size = 2362140, upload-time = "2026-07-17T03:22:59.694Z" },
{ url = "https://files.pythonhosted.org/packages/a7/be/01613af0307c31e77aced40bdf9cf3d25ce1333b3e99bb85575bdcbfeca5/pillow_jxl_plugin-1.3.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b5bbc933487207e634802f21990c365b6e61473cea34f6621420f4eda8ffd1df", size = 2660381, upload-time = "2026-07-17T03:22:51.351Z" },
{ url = "https://files.pythonhosted.org/packages/c5/8b/89636d315a31eb805681e60e569fe38d5eef3d09f13b1b6f576574387942/pillow_jxl_plugin-1.3.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1d06fd4eb038f431db27f1ab2e483fd6967bafb21a7757e1bd5f76b6fd93649", size = 2787875, upload-time = "2026-07-17T03:22:24.736Z" },
{ url = "https://files.pythonhosted.org/packages/2b/e0/7b9bdc388d89773a5f9ae74ff0be3b06080ef1a60be57b5897d929012622/pillow_jxl_plugin-1.3.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f65361ce18db59e8949c60599bfa46b7de187e28917fc39b5d816ab4b81cb16d", size = 2500834, upload-time = "2026-07-17T03:22:33.928Z" },
{ url = "https://files.pythonhosted.org/packages/24/25/0903ba5a4c8d948df9217e48182ff7d872098ddf86fe512402569f4041cc/pillow_jxl_plugin-1.3.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b881a40737c647a24270b73cd3f506dd16d634202e5214cb492f9270bd1b02", size = 2504805, upload-time = "2026-07-17T03:22:42.911Z" },
{ url = "https://files.pythonhosted.org/packages/4b/ef/77a15b2f3227854d4ef2563250ea57b03538ed962f406f1248c9caf045aa/pillow_jxl_plugin-1.3.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4049e48f09947a38047fc5d70817c6dffa90ffb5f14585c8c00e8b534d17c9b", size = 9725768, upload-time = "2026-07-17T03:23:10.845Z" },
{ url = "https://files.pythonhosted.org/packages/c6/95/486fc1fabd1e6cb8b520dce9898b51f2e29e991ba54e632381fe7f287300/pillow_jxl_plugin-1.3.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6ce20ef0887efb6dace5245f714f94b4f070ef39a1ec45161b35879c48faf30a", size = 8386447, upload-time = "2026-07-17T03:23:22.87Z" },
{ url = "https://files.pythonhosted.org/packages/05/28/7979927eb5dc79e40b31e4e07d074057722656743d324182f4a68056c2a9/pillow_jxl_plugin-1.3.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:845e61add39272ca8545f4a0e6ead977ebb7e89fa017014bb8e17273a7a09c6c", size = 9032664, upload-time = "2026-07-17T03:23:34.78Z" },
{ url = "https://files.pythonhosted.org/packages/92/b7/f606ed6fcb5fd9e177ce2b15a613a405ac22ceafc2c03bb3f58cd500ca04/pillow_jxl_plugin-1.3.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:915b8fee6a97aaf5486d0d625f26faae2e9f0d9b5892f1076f5a29b8103125a4", size = 9233868, upload-time = "2026-07-17T03:23:46.277Z" },
{ url = "https://files.pythonhosted.org/packages/a4/6d/d10a4426fc3b394d2222cb694774451e67ab052df2e15a488dfac22be561/pillow_jxl_plugin-1.3.8-cp314-cp314-win32.whl", hash = "sha256:d63d1c3566a8c9ebdd1ec6b75da816b9475d95c4f69656827d6d3957d2594ecb", size = 4554691, upload-time = "2026-07-17T03:24:11.39Z" },
{ url = "https://files.pythonhosted.org/packages/47/77/0f0297c265f1743784b6d1df719e62e49dabd1d9ede8efb0a9ba93174293/pillow_jxl_plugin-1.3.8-cp314-cp314-win_amd64.whl", hash = "sha256:6b23c83aabf4615b92ae609293d8597ca1a8d14d01ab16c703bb3a3df359796b", size = 4939714, upload-time = "2026-07-17T03:24:03.288Z" },
{ url = "https://files.pythonhosted.org/packages/ca/cf/2db398fb1a41c46fd382bad96ee90283589eaed0ec2c50cfe4afda3df72c/pillow_jxl_plugin-1.3.8-cp314-cp314-win_arm64.whl", hash = "sha256:60c36bec3d8d4ef23cf2aed0f792213f999312dd3d8cd5225ae1494c430e16f6", size = 2330667, upload-time = "2026-07-17T03:23:55.913Z" },
]
[[package]]
name = "pluggy"
version = "1.5.0"
+78 -117
View File
@@ -1,29 +1,22 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
lockfile_version = 3
[[tools."github:extism/cli"]]
version = "1.6.3"
backend = "github:extism/cli"
specifiers = ["1.6.3"]
[tools."github:extism/cli"."platforms.linux-arm64"]
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
[tools."github:extism/cli"."platforms.linux-arm64-musl"]
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
[tools."github:extism/cli"."platforms.linux-x64"]
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
[tools."github:extism/cli"."platforms.linux-x64-musl"]
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
[tools."github:extism/cli"."platforms.macos-arm64"]
checksum = "sha256:b4ddbc575b5ac000115247f781723f9b9f284ed87b29c600539d72161b5b29fc"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-arm64.tar.gz"
@@ -42,27 +35,18 @@ url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694035"
[[tools."github:extism/js-pdk"]]
version = "1.7.0"
backend = "github:extism/js-pdk"
specifiers = ["v1.7.0"]
[tools."github:extism/js-pdk"."platforms.linux-arm64"]
checksum = "sha256:025f4050b199d68413c159bde1187271ae270021a9f7171e7beb509922821f2a"
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-linux-v1.7.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632503"
[tools."github:extism/js-pdk"."platforms.linux-arm64-musl"]
checksum = "sha256:025f4050b199d68413c159bde1187271ae270021a9f7171e7beb509922821f2a"
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-linux-v1.7.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632503"
[tools."github:extism/js-pdk"."platforms.linux-x64"]
checksum = "sha256:63b72da2f5e88655522dc21477de549f238a2f40546a69ce4e0fce7e78654035"
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-linux-v1.7.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632991"
[tools."github:extism/js-pdk"."platforms.linux-x64-musl"]
checksum = "sha256:63b72da2f5e88655522dc21477de549f238a2f40546a69ce4e0fce7e78654035"
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-linux-v1.7.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632991"
[tools."github:extism/js-pdk"."platforms.macos-arm64"]
checksum = "sha256:12c01c2bb2240a6a05a4f8babe680c793c397d84e6a1497a7ca1312e78a475c3"
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-macos-v1.7.0.gz"
@@ -81,6 +65,7 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518636347"
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
specifiers = ["7.1.3-6"]
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
@@ -90,18 +75,33 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
specifiers = ["7.1.3-6"]
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
specifiers = ["7.1.3-6"]
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
specifiers = ["7.1.3-6"]
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz"
@@ -114,61 +114,31 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
specifiers = ["7.1.3-6"]
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
[[tools."github:webassembly/binaryen"]]
version = "version_124"
backend = "github:webassembly/binaryen"
specifiers = ["version_124"]
[tools."github:webassembly/binaryen"."platforms.linux-arm64"]
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
[tools."github:webassembly/binaryen"."platforms.linux-arm64-musl"]
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
[tools."github:webassembly/binaryen"."platforms.linux-x64"]
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
[tools."github:webassembly/binaryen"."platforms.linux-x64-musl"]
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
[tools."github:webassembly/binaryen"."platforms.macos-arm64"]
checksum = "sha256:86a2c960ff62c6d2ea6009d1f89745c22c70100d394a095eab45eb941bdaa24c"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-arm64-macos.tar.gz"
@@ -187,9 +157,10 @@ url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288
[[tools.java]]
version = "21.0.2"
backend = "core:java"
specifiers = ["21.0.2"]
[tools.java.options]
shorthand_vendor = "openjdk"
shorthand_vendor = "temurin"
[tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
@@ -245,12 +216,16 @@ checksum = "sha256:d2046d28c2c3dcd211ac5145c0bf30e2047ef9dfb306693c8c6e2eccd8b95
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-x64.zip"
[[tools."npm:@openapitools/openapi-generator-cli"]]
version = "2.41.0"
version = "2.42.0"
aube = { path = ".mise/locks/npm-openapitools-openapi-generator-cli/2.42.0", digest = "sha256:ef9f5cf15f958d31297bb706ee83d1347633603f3079431938665c7bc4136856" }
backend = "npm:@openapitools/openapi-generator-cli"
specifiers = ["2.42.0"]
[[tools."npm:oazapfts"]]
version = "7.5.0"
aube = { path = ".mise/locks/npm-oazapfts/7.5.0", digest = "sha256:74214614b0338eaa2baee026d615afde514311c5c76e274f420f8de8b1f0e017" }
backend = "npm:oazapfts"
specifiers = ["7.5.0"]
[[tools.opentofu]]
version = "1.13.2"
@@ -263,22 +238,12 @@ checksum_source = "mise-versions"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742799"
[tools.opentofu."platforms.linux-arm64-musl"]
checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688"
[tools.opentofu."platforms.linux-x64"]
checksum = "sha256:ac05d9952662c7db28dd57e93f0a68018837bfdf9342384ec0987977c16a9645"
checksum_source = "mise-versions"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742636"
[tools.opentofu."platforms.linux-x64-musl"]
checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723"
[tools.opentofu."platforms.macos-arm64"]
checksum = "sha256:0d1cdc85f38992055b48e7fb161304a2ddfe2e5ee0ff68ffec9d0349c8bd1b59"
checksum_source = "mise-versions"
@@ -298,75 +263,71 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742635"
[[tools.pnpm]]
version = "12.14.0"
version = "12.15.0"
backend = "aqua:pnpm/pnpm"
specifiers = ["12.14.0"]
specifiers = ["12.15.0"]
[tools.pnpm."platforms.linux-arm64"]
checksum = "sha256:cfc780b9e690549e6633e1045fb1fb6ef84e390711e2099e6ccbae91c32456ea"
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957409"
checksum = "sha256:d7ce93a2c7d847d3a34bd3f559b60c14f578b1320c17cc352a2c5c5fdf956117"
url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173352"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-x64"]
checksum = "sha256:3910c3852136bed5bc1511b9137a45fd4357ed6b3970d9d08893641e9005867e"
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-linux-x64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957410"
checksum = "sha256:55b5ed0092bd11176c1669598e231ae52e7cedac59de6efd0d415730f37d0598"
url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-x64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173360"
provenance = "github-attestations"
[tools.pnpm."platforms.macos-arm64"]
checksum = "sha256:50f48e0e17508952790a3fbf58fcf14f3b82efa5f594ca04ad904e29aaf854b0"
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-darwin-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957413"
checksum = "sha256:6a41a2877773e97f19832067187cc6fc82e186d8a2cf91fec4b4c1a5fbd6ef27"
url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-darwin-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173343"
provenance = "github-attestations"
[tools.pnpm."platforms.macos-x64"]
checksum = "sha256:2c412e07382049b4edb04fa6da78f3d4ef7810b75853c80544dcfb7c0ed79880"
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-darwin-x64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957411"
checksum = "sha256:2d7b336884574aee8a765bee48591212e9caf4fcf985fb3279588c1fb73fb47f"
url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-darwin-x64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173347"
provenance = "github-attestations"
[tools.pnpm."platforms.windows-x64"]
checksum = "sha256:6f0f38e917c8ce76ef31db528c357e2bfa354a1bcb16464e3cdec39e6530d784"
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-win32-x64.zip"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957404"
checksum = "sha256:84cf69db1be19e95bb4e5bef329291a046f2821b390d87e00f5690809d8a2ed7"
url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-win32-x64.zip"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173351"
provenance = "github-attestations"
[[tools.terragrunt]]
version = "1.1.1"
version = "1.2.0"
backend = "aqua:gruntwork-io/terragrunt"
specifiers = ["1.2.0"]
[tools.terragrunt."platforms.linux-arm64"]
checksum = "sha256:a374a7993ff3d99665a7e014007d3647ec7f0465c9d55c85e9f94c932e73cea2"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786148"
[tools.terragrunt."platforms.linux-arm64-musl"]
checksum = "sha256:a374a7993ff3d99665a7e014007d3647ec7f0465c9d55c85e9f94c932e73cea2"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786148"
checksum = "sha256:1e13b7f0646fa7962bc6c5bfba14a30d6a589325b82561e21b66be5981d86395"
checksum_source = "mise-versions"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437526"
[tools.terragrunt."platforms.linux-x64"]
checksum = "sha256:ce90077ac31ef17a2ba10d11d45f36c6501997a8f4f79d703bb7daba37032f53"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785968"
[tools.terragrunt."platforms.linux-x64-musl"]
checksum = "sha256:ce90077ac31ef17a2ba10d11d45f36c6501997a8f4f79d703bb7daba37032f53"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785968"
checksum = "sha256:23b5f2f9203d34919f7fd270a4b635b721b4903beca9762a1e4853b159bc9be8"
checksum_source = "mise-versions"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437263"
[tools.terragrunt."platforms.macos-arm64"]
checksum = "sha256:9ec8f678b9ae6c81d5d9d77b94cbf6349ce639d5938694b4adc9dea73e416794"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_darwin_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785628"
checksum = "sha256:79790d841ce71fe94c689fb6f5034afd31ad53d5904937c7a10934cca9d9734b"
checksum_source = "mise-versions"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436516"
[tools.terragrunt."platforms.macos-x64"]
checksum = "sha256:73e768a69fa44a60f9d9174f54aaf179327e08706f633c9c79d5f5c9622c91c2"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_darwin_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785520"
checksum = "sha256:0b0e63e80509eb1ffb4f89b892bc6fbf7728f27f0a73974400f1bd82a80b8627"
checksum_source = "mise-versions"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436142"
[tools.terragrunt."platforms.windows-x64"]
checksum = "sha256:dd50a324691e072a3ac879e9ec43d6310e2936c929fda658eb2811fefcc75115"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_windows_amd64.exe.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786421"
checksum = "sha256:c9a146b9aad456a289614023ad5778123d819af3cf9efd1c6130f196cfcb1999"
checksum_source = "mise-versions"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_windows_amd64.exe.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662438255"
+1 -1
View File
@@ -17,7 +17,7 @@ config_roots = [
[tools]
node = "24.22.1"
pnpm = "12.15.0"
terragrunt = "1.1.6"
terragrunt = "1.2.0"
opentofu = "1.13.2"
"npm:@openapitools/openapi-generator-cli" = "2.42.0"
"npm:oazapfts" = "7.5.0"
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 3.3.5-local.11+3030503
version: 3.3.5-local.12+3030504
environment:
sdk: '>=3.12.0 <4.0.0'
+2 -2
View File
@@ -251,9 +251,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.190"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "linux-raw-sys"
+1
View File
@@ -23133,6 +23133,7 @@
"description": "Image format",
"enum": [
"jpeg",
"jxl",
"webp"
],
"type": "string"
+1 -1
View File
@@ -2,6 +2,6 @@
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.25.0"
"version": "7.26.0"
}
}
+1
View File
@@ -8214,6 +8214,7 @@ export enum Colorspace {
}
export enum ImageFormat {
Jpeg = "jpeg",
Jxl = "jxl",
Webp = "webp"
}
export enum LogLevel {
+441 -362
View File
File diff suppressed because it is too large Load Diff
+84 -48
View File
@@ -1,22 +1,23 @@
<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=Llicència&logoColor=000000&labelColor=ececec" alt="Llicència: MIT"></a>
<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=Llic%C3%A8ncia&logoColor=000000&labelColor=ececec" alt="Llicència: 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" atl="Discord"/>
<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/>
<br/>
<br/>
</p>
<p align="center">
<img src="../design/immich-logo-stacked-light.svg" width="300" title="Iniciar sessió amb URL personalitzada">
<img src="../design/immich-logo-stacked-light.svg" width="300" title="Inicia la sessió amb un URL personalitzat">
</p>
<h3 align="center">Immich - Solució de còpia de seguretat d'alta rendiment per a fotos i vídeos auto-allotjada</h3>
<h3 align="center">Solució de gestió de fotos i vídeos autoallotjada i d’alt rendiment </h3>
<br/>
<a href="https://immich.app">
<img src="../design/immich-screenshots.png" title="Captura de pantalla principal">
</a>
<br/>
<p align="center">
<a href="../README.md">English</a>
<a href="README_es_ES.md">Español</a>
@@ -36,64 +37,99 @@
<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>
<a href="README_th_TH.md">ภาษาไทย</a>
<a href="README_ml_IN.md">മലയാളം</a>
</p>
> [!WARNING]
> ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos!
> ⚠️ Seguiu sempre un pla de còpies de seguretat [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) per a les vostres valuoses fotos i vídeos!
>
> [!NOTE]
> Podeu trobar la documentació principal, incloent les guies d'instal·lació, a https://immich.app/.
> Podeu trobar la documentació principal, incloent-hi les guies d’instal·lació, a https://immich.app/.
## Contingut
## Enllaços
- [Documentació](https://docs.immich.app/)
- [Introducció](https://docs.immich.app/overview/introduction)
- [Quant a](https://docs.immich.app/overview/introduction)
- [Instal·lació](https://docs.immich.app/install/requirements)
- [Mapa de ruta](https://immich.app/roadmap)
- [Demo](#demo)
- [Full de ruta](https://immich.app/roadmap)
- [Demostració](#demostració)
- [Funcionalitats](#funcionalitats)
- [Traduccions](https://docs.immich.app/developer/translations)
- [Directrius de contribució](https://docs.immich.app/overview/support-the-project)
- [Contribucions](https://docs.immich.app/overview/support-the-project)
## Demo
## Demostració
Podeu accedir a la demostració web a https://demo.immich.app. Per a l'aplicació mòbil, podeu utilitzar `https://demo.immich.app` com a "URL de punt final del servidor".
Accediu a la demostració [aquí](https://demo.immich.app). Per a l’aplicació mòbil, podeu utilitzar `https://demo.immich.app` com a `URL de l’endpoint del servidor`.
```bash title="Credencials de la demo"
Les credencials
email: demo@immich.app
contrasenya: demo
```
### Credencials d’inici de sessió
| Adreça electrònica | Contrasenya |
| ------------------ | ----------- |
| demo@immich.app | demo |
# Funcionalitats
| Característiques | Mòbil | Web |
| -------------------------------------------- | ------ | --- |
| Pujar i veure vídeos i fotos | Sí | Sí |
| Còpia de seguretat automàtica en obrir l'aplicació | Sí | N/A |
| Selecció d'àlbums per a la còpia de seguretat | Sí | N/A |
| Descarregar fotos i vídeos a l'aparell local | Sí | Sí |
| Suport per a múltiples usuaris | Sí | Sí |
| Àlbums i àlbums compartits | Sí | Sí |
| Barra de desplaçament amb funció de rasclet/arrossegament | Sí | Sí |
| Suport per a formats raw | Sí | Sí |
| Visualització de metadades (EXIF, mapa) | Sí | Sí |
| Cerca per metadades, objectes, cares i CLIP | Sí | Sí |
| Funcions administratives (gestió d'usuaris) | No | Sí |
| Còpia de seguretat en segon pla | Sí | N/A |
| Desplaçament virtual | Sí | Sí |
| Suport per a OAuth | Sí | Sí |
| Claus d'API | N/A | Sí |
| Còpia de seguretat i reproducció de LivePhoto | iOS | Sí |
| Estructura d'emmagatzematge definida per l'usuari | Sí | Sí |
| Compartició pública | No | Sí |
| Arxiu i preferits | Sí | Sí |
| Mapa global | No | Sí |
| Compartició amb associats | Sí | Sí |
| Reconeixement facial i agrupament | Sí | Sí |
| Records (fa x anys) | Sí | Sí |
| Suport fora de línia | Sí | No |
| Galeria de només lectura | Sí | Sí |
| Funcionalitats | Mòbil | Web |
| --------------------------------------------------------------- | ----- | --- |
| Pujada i visualització de vídeos i fotos | Sí | Sí |
| Còpia al servidor automàtica en obrir l’aplicació | Sí | N/A |
| Sistema per a evitar la duplicació de recursos | Sí | Sí |
| Selecció d’àlbums per a la còpia al servidor | Sí | N/A |
| Baixada de fotos i vídeos al dispositiu local | Sí | Sí |
| Suport per a múltiples usuaris | Sí | Sí |
| Àlbums i àlbums compartits | Sí | Sí |
| Barra de desplaçament amb arrossegament interactiu | Sí | Sí |
| Suport per a formats RAW | Sí | Sí |
| Visualització de metadades (EXIF, mapa) | Sí | Sí |
| Cerca per metadades, objectes, cares i CLIP | Sí | Sí |
| Funcions administratives (gestió d’usuaris) | No | Sí |
| Còpia al servidor en segon pla | Sí | N/A |
| Desplaçament virtual | Sí | Sí |
| Suport per a OAuth | Sí | Sí |
| Claus de l’API | N/A | Sí |
| Còpia al servidor i reproducció de Live Photo/fotos en moviment | Sí | Sí |
| Suport per a la visualització d’imatges en 360 graus | No | Sí |
| Estructura d’emmagatzematge definida per l’usuari | Sí | Sí |
| Compartició pública | Sí | Sí |
| Arxiu i preferits | Sí | Sí |
| Mapa global | Sí | Sí |
| Compartició amb companys | Sí | Sí |
| Reconeixement facial i agrupament | Sí | Sí |
| Records (fa x anys) | Sí | Sí |
| Suport per a mode fora de línia | Sí | No |
| Galeria de només lectura | Sí | Sí |
| Fotos apilades | Sí | Sí |
| Etiquetes | No | Sí |
| Visualització de carpetes | Sí | Sí |
## Traduccions
Obtingueu més informació sobre les traduccions [aquí](https://docs.immich.app/developer/translations).
<a href="https://hosted.weblate.org/engage/immich/">
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Estat de les traduccions" />
</a>
## Activitat al repositori
![Activitats](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imatge d’analítiques de Repobeats")
## Historial d’estrelles
<a href="https://star-history.com/#immich-app/immich&type=date&legend=top-left">
<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àfic de l’historial d’estrelles" src="https://api.star-history.com/svg?repos=immich-app/immich&type=date" width="100%" />
</picture>
</a>
## Contribuïdors
<a href="https://github.com/immich-app/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a>
+3 -2
View File
@@ -2,8 +2,9 @@
"contentSecurityPolicy": {
"directives": {
"default-src": ["'self'"],
"script-src": ["'self'", "'wasm-unsafe-eval'", "'unsafe-inline'", "https://www.gstatic.com"],
"style-src": ["'self'", "'unsafe-inline'"],
"script-src": ["'self'", "'wasm-unsafe-eval'", "https://www.gstatic.com"],
"style-src": ["'self'"],
"style-src-attr": ["'unsafe-inline'"],
"img-src": ["'self'", "data:", "blob:"],
"media-src": ["'self'", "data:", "blob:"],
"connect-src": [
+3
View File
@@ -64,6 +64,9 @@ export const excludePaths = ['/.well-known/immich', '/.well-known/webauthn', '/c
export const FACE_THUMBNAIL_SIZE = 250;
// The fastest JPEG XL tier skips smoothing filters, which also keeps small thumbnails sharper
export const THUMBNAIL_DECODING_TIER = 4;
type ModelInfo = { dimSize: number };
export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
RN101__openai: { dimSize: 512 },
+1
View File
@@ -610,6 +610,7 @@ export const ColorspaceSchema = z.enum(Colorspace).describe('Colorspace').meta({
export enum ImageFormat {
Jpeg = 'jpeg',
Jxl = 'jxl',
Webp = 'webp',
}
@@ -1,5 +1,5 @@
import { ImmichTelemetry } from 'src/enum.js';
import { ConfigRepository, clearEnvCache } from 'src/repositories/config.repository.js';
import { ConfigRepository, applyCspHashes, clearEnvCache } from 'src/repositories/config.repository.js';
const getEnv = () => {
clearEnvCache();
@@ -325,3 +325,35 @@ describe('getEnv', () => {
});
});
});
describe('applyCspHashes', () => {
const manifest = { 'script-src': [`'sha256-script'`], 'style-src': [`'sha256-style'`] };
it('should append the hashes of the inline content', () => {
const directives = applyCspHashes({ 'script-src': [`'self'`], 'style-src': [`'self'`] }, manifest);
expect(directives).toEqual({
'script-src': [`'self'`, `'sha256-script'`],
'style-src': [`'self'`, `'sha256-style'`],
});
});
it('should skip a directive that allows unsafe-inline, which a hash would disable', () => {
const directives = applyCspHashes({ 'script-src': [`'self'`, `'unsafe-inline'`] }, manifest);
expect(directives['script-src']).toEqual([`'self'`, `'unsafe-inline'`]);
});
it('should skip a directive the policy does not declare', () => {
const directives = applyCspHashes({ 'script-src': [`'self'`] }, manifest);
expect(directives).not.toHaveProperty('style-src');
});
it('should not mutate the directives it is given', () => {
const directives = { 'script-src': [`'self'`] };
applyCspHashes(directives, manifest);
expect(directives).toEqual({ 'script-src': [`'self'`] });
});
});
+43 -4
View File
@@ -7,7 +7,7 @@ import { HelmetOptions } from 'helmet';
import { RedisOptions } from 'ioredis';
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces/index.js';
import { readFileSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import type { VectorExtension } from 'src/types.js';
import { IWorker, citiesFile } from 'src/constants.js';
@@ -152,7 +152,22 @@ const asSet = <T>(value: string | undefined, defaults: T[]) => {
return new Set(values.length === 0 ? defaults : (values as T[]));
};
const resolveHelmetFile = (helmetFile: 'true' | 'false' | string | undefined) => {
type CspOptions = Exclude<NonNullable<HelmetOptions['contentSecurityPolicy']>, boolean>;
type CspDirectives = NonNullable<CspOptions['directives']>;
export const applyCspHashes = (directives: CspDirectives, manifest: Record<string, string[]>): CspDirectives => {
const merged = { ...directives };
for (const [directive, hashes] of Object.entries(manifest)) {
const sources = merged[directive];
if (Array.isArray(sources) && !sources.includes(`'unsafe-inline'`)) {
merged[directive] = [...sources, ...hashes];
}
}
return merged;
};
const resolveHelmetFile = (helmetFile: 'true' | 'false' | string | undefined, webRoot: string) => {
// default is off
if (!helmetFile || helmetFile === 'false') {
return;
@@ -160,11 +175,35 @@ const resolveHelmetFile = (helmetFile: 'true' | 'false' | string | undefined) =>
helmetFile = helmetFile === 'true' ? join(import.meta.dirname, '..', '..', 'helmet.json') : helmetFile;
let config: HelmetOptions;
try {
return JSON.parse(readFileSync(helmetFile).toString()) as HelmetOptions;
config = JSON.parse(readFileSync(helmetFile).toString()) as HelmetOptions;
} catch (error) {
throw new Error(`Failed to read helmet file: ${helmetFile}`, { cause: error });
}
const contentSecurityPolicy = config.contentSecurityPolicy;
if (!contentSecurityPolicy || typeof contentSecurityPolicy === 'boolean' || !contentSecurityPolicy.directives) {
return config;
}
// merge with csp build-time metadata
const manifestFile = join(webRoot, '.csp.json');
if (!existsSync(join(webRoot, 'index.html')) || !existsSync(manifestFile)) {
return config;
}
let manifest: Record<string, string[]>;
try {
manifest = JSON.parse(readFileSync(manifestFile).toString()) as Record<string, string[]>;
} catch (error) {
throw new Error(`Failed to read CSP manifest: ${manifestFile}`, { cause: error });
}
const directives = applyCspHashes(contentSecurityPolicy.directives, manifest);
return { ...config, contentSecurityPolicy: { ...contentSecurityPolicy, directives } };
};
const getEnv = (): EnvData => {
@@ -311,7 +350,7 @@ const getEnv = (): EnvData => {
},
helmet: {
config: resolveHelmetFile(dto.IMMICH_HELMET_FILE),
config: resolveHelmetFile(dto.IMMICH_HELMET_FILE, folders.web),
},
licensePublicKey: isProd ? productionKeys : stagingKeys,
@@ -331,7 +331,13 @@ describe(MediaRepository.name, () => {
const decoded = await sut.decodeImage(buffer, { colorspace: Colorspace.P3, processInvalidImages: false });
await sut.generateThumbnail(
decoded,
{ colorspace: Colorspace.P3, quality: 80, format: ImageFormat.Webp, processInvalidImages: false },
{
colorspace: Colorspace.P3,
quality: 80,
format: ImageFormat.Webp,
processInvalidImages: false,
decodingTier: 0,
},
file,
);
@@ -186,6 +186,7 @@ export class MediaRepository {
// this is default in libvips (except the threshold is 90), but we need to set it manually in sharp
chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0',
progressive: options.progressive,
decodingTier: options.decodingTier,
})
.toFile(output);
}
@@ -422,6 +422,7 @@ describe(AssetMediaService.name, () => {
.owner(authStub.user1.user)
.build();
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([motionAsset.id]));
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
mocks.asset.create.mockResolvedValueOnce(asset);
@@ -439,6 +440,7 @@ describe(AssetMediaService.name, () => {
it('should hide the linked motion asset', async () => {
const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build();
const asset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([motionAsset.id]));
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
mocks.asset.create.mockResolvedValueOnce(asset);
@@ -456,6 +458,63 @@ describe(AssetMediaService.name, () => {
});
});
it('should link a live photo video that is already in the shared link', async () => {
const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build();
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
const auth = AuthFactory.from(authStub.user1.user).sharedLink({ allowUpload: true }).build();
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([motionAsset.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([motionAsset.id]));
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
mocks.asset.create.mockResolvedValueOnce(asset);
mocks.sharedLink.addAssets.mockResolvedValue([]);
await expect(
sut.uploadAsset(auth, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill),
).resolves.toEqual({
status: AssetMediaStatus.CREATED,
id: asset.id,
});
expect(mocks.access.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
auth.sharedLink!.id,
new Set([motionAsset.id]),
);
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([motionAsset.id]), false);
expect(mocks.asset.getById).toHaveBeenCalledWith(motionAsset.id);
expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, visibility: AssetVisibility.Hidden });
expect(mocks.asset.create).toHaveBeenCalledWith(expect.objectContaining({ livePhotoVideoId: motionAsset.id }));
});
it('should not link a live photo video that is not in the shared link', async () => {
const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build();
const auth = AuthFactory.from(authStub.user1.user).sharedLink({ allowUpload: true }).build();
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set());
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([motionAsset.id]));
await expect(
sut.uploadAsset(auth, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill),
).rejects.toThrow('Not found or no asset.update permission');
expect(mocks.asset.getById).not.toHaveBeenCalled();
expect(mocks.asset.update).not.toHaveBeenCalled();
expect(mocks.asset.create).not.toHaveBeenCalled();
});
it('should not link a live photo video in the shared link that is owned by another user', async () => {
const motionAsset = AssetFactory.from({ type: AssetType.Video }).build();
const auth = AuthFactory.from(authStub.user1.user).sharedLink({ allowUpload: true }).build();
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([motionAsset.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set());
await expect(
sut.uploadAsset(auth, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill),
).rejects.toThrow('Not found or no asset.update permission');
expect(mocks.asset.getById).not.toHaveBeenCalled();
expect(mocks.asset.update).not.toHaveBeenCalled();
expect(mocks.asset.create).not.toHaveBeenCalled();
});
it('should handle a sidecar file', async () => {
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build();
mocks.asset.getById.mockResolvedValueOnce(getForAsset(asset));
@@ -142,6 +142,19 @@ export class AssetMediaService extends BaseService {
this.requireQuota(auth, file.size);
if (dto.livePhotoVideoId) {
if (auth.sharedLink) {
const ids = new Set([dto.livePhotoVideoId]);
const [assetIds1, assetIds2] = await Promise.all([
this.accessRepository.asset.checkSharedLinkAccess(auth.sharedLink.id, ids),
this.accessRepository.asset.checkOwnerAccess(auth.user.id, ids, false),
]);
if (!assetIds1.intersection(assetIds2).has(dto.livePhotoVideoId)) {
throw new BadRequestException(`Not found or no ${Permission.AssetUpdate} permission`);
}
} else {
await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [dto.livePhotoVideoId] });
}
await onBeforeLink(
{ asset: this.assetRepository, event: this.eventRepository },
{ userId: auth.user.id, livePhotoVideoId: dto.livePhotoVideoId },
+7 -2
View File
@@ -262,7 +262,9 @@ describe(AssetService.name, () => {
const auth = AuthFactory.create();
const motionAsset = AssetFactory.from().owner(auth.user).build();
const asset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.access.asset.checkOwnerAccess
.mockResolvedValueOnce(new Set([asset.id]))
.mockResolvedValueOnce(new Set([motionAsset.id]));
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
await expect(
@@ -315,7 +317,10 @@ describe(AssetService.name, () => {
it('should link a live video', async () => {
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Timeline });
const stillAsset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([stillAsset.id]));
mocks.access.asset.checkOwnerAccess
.mockResolvedValueOnce(new Set([stillAsset.id]))
.mockResolvedValueOnce(new Set([motionAsset.id]))
.mockResolvedValue(new Set([stillAsset.id]));
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
mocks.asset.getById.mockResolvedValueOnce(getForAsset(stillAsset));
mocks.asset.update.mockResolvedValue(getForAsset(stillAsset));
+1
View File
@@ -91,6 +91,7 @@ export class AssetService extends BaseService {
let previousMotion: { id: string } | null = null;
if (rest.livePhotoVideoId) {
await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [rest.livePhotoVideoId] });
await onBeforeLink(repos, { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId });
} else if (rest.livePhotoVideoId === null) {
const asset = await this.findOrFail(id);
+63
View File
@@ -454,6 +454,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
expect.any(String),
);
@@ -467,6 +468,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 4,
},
expect.any(String),
);
@@ -551,6 +553,30 @@ describe(MediaService.name, () => {
]);
});
it('should generate WebP video thumbnails when JPEG XL is configured', async () => {
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
mocks.systemMetadata.get.mockResolvedValue({
image: { preview: { format: ImageFormat.Jxl }, thumbnail: { format: ImageFormat.Jxl } },
});
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
...getForGenerateThumbnail(asset),
...probeStub.videoStream2160p,
});
await sut.handleGenerateThumbnails({ id: asset.id });
expect(mocks.media.transcode).toHaveBeenCalledWith(
'/original/path.ext',
expect.stringMatching(/_preview\.webp$/),
expect.any(Object),
);
expect(mocks.media.transcode).toHaveBeenCalledWith(
'/original/path.ext',
expect.stringMatching(/_thumbnail\.webp$/),
expect.any(Object),
);
});
it('should tonemap thumbnail for hdr video', async () => {
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
@@ -726,6 +752,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
previewPath,
);
@@ -739,6 +766,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 4,
},
thumbnailPath,
);
@@ -774,6 +802,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
previewPath,
);
@@ -787,6 +816,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 4,
},
thumbnailPath,
);
@@ -870,6 +900,24 @@ describe(MediaService.name, () => {
]);
});
it('should never set isProgressive for JPEG XL', async () => {
const asset = AssetFactory.from().exif().build();
mocks.systemMetadata.get.mockResolvedValue({
image: {
preview: { format: ImageFormat.Jxl, progressive: true },
thumbnail: { format: ImageFormat.Jxl, progressive: true },
},
});
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
expect.objectContaining({ type: AssetFileType.Preview, isProgressive: false }),
expect.objectContaining({ type: AssetFileType.Thumbnail, isProgressive: false }),
]);
});
it('should never set isProgressive for videos', async () => {
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
mocks.systemMetadata.get.mockResolvedValue({
@@ -1079,6 +1127,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
expect.any(String),
);
@@ -1114,6 +1163,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
expect.any(String),
);
@@ -1127,6 +1177,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
expect.any(String),
);
@@ -1160,6 +1211,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
expect.any(String),
);
@@ -1173,6 +1225,7 @@ describe(MediaService.name, () => {
size: 1440,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
expect.any(String),
);
@@ -1210,6 +1263,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
expect.any(String),
);
@@ -1274,6 +1328,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
expect.any(String),
);
@@ -1316,6 +1371,7 @@ describe(MediaService.name, () => {
progressive: false,
processInvalidImages: false,
edits: [],
decodingTier: 0,
},
expect.any(String),
);
@@ -1577,6 +1633,7 @@ describe(MediaService.name, () => {
],
processInvalidImages: false,
size: 250,
decodingTier: 4,
},
expect.any(String),
);
@@ -1630,6 +1687,7 @@ describe(MediaService.name, () => {
],
processInvalidImages: false,
size: 250,
decodingTier: 4,
},
expect.any(String),
);
@@ -1678,6 +1736,7 @@ describe(MediaService.name, () => {
],
processInvalidImages: false,
size: 250,
decodingTier: 4,
},
expect.any(String),
);
@@ -1722,6 +1781,7 @@ describe(MediaService.name, () => {
],
processInvalidImages: false,
size: 250,
decodingTier: 4,
},
expect.any(String),
);
@@ -1766,6 +1826,7 @@ describe(MediaService.name, () => {
],
processInvalidImages: false,
size: 250,
decodingTier: 4,
},
expect.any(String),
);
@@ -1810,6 +1871,7 @@ describe(MediaService.name, () => {
],
processInvalidImages: false,
size: 250,
decodingTier: 4,
},
expect.any(String),
);
@@ -1859,6 +1921,7 @@ describe(MediaService.name, () => {
],
processInvalidImages: false,
size: 250,
decodingTier: 4,
},
expect.any(String),
);
+18 -10
View File
@@ -10,7 +10,7 @@ import type {
VideoInterfaces,
VideoStreamInfo,
} from 'src/types.js';
import { FACE_THUMBNAIL_SIZE } from 'src/constants.js';
import { FACE_THUMBNAIL_SIZE, THUMBNAIL_DECODING_TIER } from 'src/constants.js';
import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core.js';
import { AssetFile } from 'src/database.js';
import { OnEvent, OnJob } from 'src/decorators.js';
@@ -303,14 +303,14 @@ export class MediaService extends BaseService {
fileType: AssetFileType.Preview,
format: previewFormat,
isEdited: useEdits,
isProgressive: !!image.preview.progressive && previewFormat !== ImageFormat.Webp,
isProgressive: !!image.preview.progressive && previewFormat === ImageFormat.Jpeg,
isTransparent,
});
const thumbnailFile = this.getImageFile(asset, {
fileType: AssetFileType.Thumbnail,
format: thumbnailFormat,
isEdited: useEdits,
isProgressive: !!image.thumbnail.progressive && thumbnailFormat !== ImageFormat.Webp,
isProgressive: !!image.thumbnail.progressive && thumbnailFormat === ImageFormat.Jpeg,
isTransparent,
});
this.storageCore.ensureFolders(previewFile.path);
@@ -318,8 +318,13 @@ export class MediaService extends BaseService {
// generate final images
const decoded = { data, info };
const baseOptions = { colorspace, processInvalidImages: false, edits: useEdits ? asset.edits : [] };
const thumbnailOptions = { ...image.thumbnail, ...baseOptions, format: thumbnailFormat };
const previewOptions = { ...image.preview, ...baseOptions, format: previewFormat };
const thumbnailOptions = {
...image.thumbnail,
...baseOptions,
format: thumbnailFormat,
decodingTier: THUMBNAIL_DECODING_TIER,
} as const;
const previewOptions = { ...image.preview, ...baseOptions, format: previewFormat, decodingTier: 0 } as const;
const promises = [
this.mediaRepository.generateThumbhash(decoded, baseOptions),
this.mediaRepository.generateThumbnail(decoded, thumbnailOptions, thumbnailFile.path),
@@ -335,7 +340,7 @@ export class MediaService extends BaseService {
fileType: AssetFileType.FullSize,
format: fullsizeFormat,
isEdited: useEdits,
isProgressive: !!image.fullsize.progressive && fullsizeFormat !== ImageFormat.Webp,
isProgressive: !!image.fullsize.progressive && fullsizeFormat === ImageFormat.Jpeg,
isTransparent,
});
const fullsizeOptions = {
@@ -343,14 +348,15 @@ export class MediaService extends BaseService {
format: fullsizeFormat,
quality: image.fullsize.quality,
progressive: image.fullsize.progressive,
};
decodingTier: 0,
} as const;
promises.push(this.mediaRepository.generateThumbnail(decoded, fullsizeOptions, fullsizeFile.path));
} else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.Jpeg) {
fullsizeFile = this.getImageFile(asset, {
fileType: AssetFileType.FullSize,
format: extracted.format,
isEdited: false,
isProgressive: !!image.fullsize.progressive && image.fullsize.format !== ImageFormat.Webp,
isProgressive: !!image.fullsize.progressive && image.fullsize.format === ImageFormat.Jpeg,
isTransparent,
});
this.storageCore.ensureFolders(fullsizeFile.path);
@@ -444,6 +450,7 @@ export class MediaService extends BaseService {
),
},
],
decodingTier: THUMBNAIL_DECODING_TIER,
};
await this.mediaRepository.generateThumbnail(decoded, thumbnailOptions, thumbnailPath);
@@ -493,14 +500,15 @@ export class MediaService extends BaseService {
private async generateVideoThumbnails(asset: ThumbnailAsset, { ffmpeg, image }: SystemConfig) {
const previewFile = this.getImageFile(asset, {
fileType: AssetFileType.Preview,
format: image.preview.format,
// TODO: write JPEG XL for video thumbnails
format: image.preview.format === ImageFormat.Jxl ? ImageFormat.Webp : image.preview.format,
isEdited: false,
isProgressive: false,
isTransparent: false,
});
const thumbnailFile = this.getImageFile(asset, {
fileType: AssetFileType.Thumbnail,
format: image.thumbnail.format,
format: image.thumbnail.format === ImageFormat.Jxl ? ImageFormat.Webp : image.thumbnail.format,
isEdited: false,
isProgressive: false,
isTransparent: false,
+4 -11
View File
@@ -43,20 +43,15 @@ export type DeepPartial<T> = T extends Date
export type RepositoryInterface<T extends object> = Pick<T, keyof T>;
export type FullsizeImageOptions = {
format: ImageFormat;
quality: number;
enabled: boolean;
progressive?: boolean;
};
export type ImageOptions = {
format: ImageFormat;
quality: number;
size: number;
progressive?: boolean;
decodingTier: 0 | 1 | 2 | 3 | 4;
};
export type FullsizeImageOptions = ImageOptions & { enabled: boolean };
export type RawImageInfo = {
width: number;
height: number;
@@ -84,9 +79,7 @@ export type TransformOptions = {
edits?: AssetEditActionItem[];
};
export type GenerateThumbnailOptions = Pick<ImageOptions, 'format' | 'quality' | 'progressive'> &
ImageColorOptions &
TransformOptions;
export type GenerateThumbnailOptions = ImageOptions & ImageColorOptions & TransformOptions;
export type GenerateThumbhashOptions = ImageColorOptions & Pick<TransformOptions, 'edits'>;
export interface VideoStreamInfo {
+1 -4
View File
@@ -153,11 +153,8 @@ export const onBeforeLink = async (
if (motionAsset.type !== AssetType.Video) {
throw new BadRequestException('Live photo video must be a video');
}
if (motionAsset.ownerId !== userId) {
throw new BadRequestException('Live photo video does not belong to the user');
}
if (!motionAsset || motionAsset.visibility !== AssetVisibility.Timeline) {
if (motionAsset.visibility !== AssetVisibility.Timeline) {
return;
}
+2
View File
@@ -1,4 +1,5 @@
import { join } from 'node:path';
import { THUMBNAIL_DECODING_TIER } from 'src/constants.js';
import { StorageCore } from 'src/cores/storage.core.js';
import { SystemConfig } from 'src/dtos/config.dto.js';
import { StorageFolder } from 'src/enum.js';
@@ -33,6 +34,7 @@ export const generateProfileImage = async (
progressive: image.thumbnail.progressive,
size: image.thumbnail.size,
processInvalidImages: false,
decodingTier: THUMBNAIL_DECODING_TIER,
},
outputPath,
);
+1
View File
@@ -114,6 +114,7 @@
"factory.ts": "^1.4.1",
"globals": "^17.0.0",
"happy-dom": "^21.0.0",
"parse5": "^8.0.1",
"prettier": "^3.7.4",
"prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^4.0.0",
+1 -1
View File
@@ -2,7 +2,7 @@ import Image from '#lib/components/Image.svelte';
import { cancelImageUrl } from '#lib/utils/sw-messaging.ts';
import { fireEvent, render } from '@testing-library/svelte';
vi.mock('#lib/utils/sw-messaging.js', () => ({
vi.mock('#lib/utils/sw-messaging.ts', () => ({
cancelImageUrl: vi.fn(),
}));
@@ -1,4 +1,4 @@
import { renderWithTooltips } from '#tests/helpers';
import { renderWithTooltips } from '#tests/helpers.ts';
import userEvent from '@testing-library/user-event';
import SharedLinkFormFields from './SharedLinkFormFields.svelte';
@@ -2,7 +2,7 @@ import { sdkMock } from '#lib/__mocks__/sdk.mock.ts';
import '@testing-library/jest-dom';
import { render, waitFor, type RenderResult } from '@testing-library/svelte';
import { init, register, waitLocale } from 'svelte-i18n';
import { albumFactory } from '#test-data/factories/album-factory';
import { albumFactory } from '#test-data/factories/album-factory.ts';
import AlbumCard from '../AlbumCard.svelte';
describe('AlbumCard component', () => {
@@ -1,9 +1,9 @@
import AlbumCover from '#lib/components/album-page/AlbumCover.svelte';
import { getAssetMediaUrl } from '#lib/utils.ts';
import { render } from '@testing-library/svelte';
import { albumFactory } from '#test-data/factories/album-factory';
import { albumFactory } from '#test-data/factories/album-factory.ts';
vi.mock('#lib/utils.js');
vi.mock('#lib/utils.ts');
describe('AlbumCover component', () => {
it('renders an image when the album has a thumbnail', () => {
@@ -2,10 +2,10 @@ import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte';
import { init, register, waitLocale } from 'svelte-i18n';
import { writable } from 'svelte/store';
import { albumFactory } from '#test-data/factories/album-factory';
import { albumFactory } from '#test-data/factories/album-factory.ts';
import AlbumSummary from '../AlbumSummary.svelte';
vitest.mock('#lib/stores/preferences.store.js', () => ({
vitest.mock('#lib/stores/preferences.store.ts', () => ({
locale: writable('en'),
}));
@@ -2,15 +2,15 @@ import { getAnimateMock } from '#lib/__mocks__/animate.mock.ts';
import { getResizeObserverMock } from '#lib/__mocks__/resize-observer.mock.ts';
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
import { SlideshowState, slideshowStore } from '#lib/stores/slideshow.store.ts';
import { renderWithTooltips } from '#tests/helpers';
import { renderWithTooltips } from '#tests/helpers.ts';
import { updateAsset } from '@immich/sdk';
import { fireEvent, waitFor } from '@testing-library/svelte';
import { assetFactory } from '#test-data/factories/asset-factory';
import { preferencesFactory } from '#test-data/factories/preferences-factory';
import { userAdminFactory } from '#test-data/factories/user-factory';
import { assetFactory } from '#test-data/factories/asset-factory.ts';
import { preferencesFactory } from '#test-data/factories/preferences-factory.ts';
import { userAdminFactory } from '#test-data/factories/user-factory.ts';
import AssetViewer from './AssetViewer.svelte';
vi.mock('#lib/managers/feature-flags-manager.svelte.js', () => ({
vi.mock('#lib/managers/feature-flags-manager.svelte.ts', () => ({
featureFlagsManager: {
init: vi.fn(),
loadFeatureFlags: vi.fn(),
@@ -18,7 +18,7 @@ vi.mock('#lib/managers/feature-flags-manager.svelte.js', () => ({
} as never,
}));
vi.mock('#lib/stores/ocr.svelte.js', () => ({
vi.mock('#lib/stores/ocr.svelte.ts', () => ({
ocrManager: {
clear: vi.fn(),
getAssetOcr: vi.fn(),
@@ -2,15 +2,15 @@ import { getResizeObserverMock } from '#lib/__mocks__/resize-observer.mock.ts';
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
import { queryClient } from '#lib/queries/client.ts';
import { getUserConfigQuery } from '#lib/queries/config-user.query.ts';
import { renderWithTooltips } from '#tests/helpers';
import { renderWithTooltips } from '#tests/helpers.ts';
import type { UserConfigDto } from '@immich/sdk';
import '@testing-library/jest-dom';
import { assetFactory } from '#test-data/factories/asset-factory';
import { preferencesFactory } from '#test-data/factories/preferences-factory';
import { userAdminFactory } from '#test-data/factories/user-factory';
import { assetFactory } from '#test-data/factories/asset-factory.ts';
import { preferencesFactory } from '#test-data/factories/preferences-factory.ts';
import { userAdminFactory } from '#test-data/factories/user-factory.ts';
import AssetViewerNavBar from './AssetViewerNavBar.svelte';
vi.mock(import('#lib/managers/feature-flags-manager.svelte.js'), function () {
vi.mock(import('#lib/managers/feature-flags-manager.svelte.ts'), function () {
return {
featureFlagsManager: {
init: vi.fn(),
@@ -1,7 +1,7 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { assetFactory } from '#test-data/factories/asset-factory';
import { assetFactory } from '#test-data/factories/asset-factory.ts';
import DetailPanelDescription from './DetailPanelDescription.svelte';
describe('DetailPanelDescription', () => {
@@ -43,7 +43,7 @@
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import './immich-time-range';
import './immich-time-range.ts';
import { createPreloadedQuery } from '#lib/queries/client.ts';
import { getPublicConfigQuery } from '#lib/queries/config-public.query.ts';
@@ -1,10 +1,10 @@
import { renderWithTooltips } from '#tests/helpers';
import { renderWithTooltips } from '#tests/helpers.ts';
import type { AssetResponseDto } from '@immich/sdk';
import '@testing-library/jest-dom';
import { assetFactory } from '#test-data/factories/asset-factory';
import { assetFactory } from '#test-data/factories/asset-factory.ts';
import DeleteAction from './DeleteAction.svelte';
vi.mock(import('#lib/managers/feature-flags-manager.svelte.js'), () => {
vi.mock(import('#lib/managers/feature-flags-manager.svelte.ts'), () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { featureFlagsManager: { init: vi.fn(), loadFeatureFlags: vi.fn(), value: { trash: true } } as any };
});
@@ -11,7 +11,7 @@
import { IconButton, modalManager, toastManager } from '@immich/ui';
import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { OnAction, PreAction } from './action';
import type { OnAction, PreAction } from './action.ts';
interface Props {
asset: AssetResponseDto;
@@ -7,7 +7,7 @@
import { toastManager } from '@immich/ui';
import { mdiHistory } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { OnAction } from './action';
import type { OnAction } from './action.ts';
interface Props {
asset: AssetResponseDto;
@@ -7,7 +7,7 @@
import { modalManager } from '@immich/ui';
import { mdiLockOpenVariantOutline, mdiLockOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { OnAction, PreAction } from './action';
import type { OnAction, PreAction } from './action.ts';
interface Props {
asset: TimelineAsset;
@@ -1,7 +1,7 @@
import ImageThumbnail from '#lib/components/assets/thumbnail/ImageThumbnail.svelte';
import { fireEvent, render } from '@testing-library/svelte';
vi.mock('#lib/utils/sw-messaging.js', () => ({
vi.mock('#lib/utils/sw-messaging.ts', () => ({
cancelImageUrl: vi.fn(),
}));
@@ -2,9 +2,9 @@ import { getIntersectionObserverMock } from '#lib/__mocks__/intersection-observe
import Thumbnail from '#lib/components/assets/thumbnail/Thumbnail.svelte';
import { getTabbable } from '#lib/utils/focus-util.ts';
import { render } from '@testing-library/svelte';
import { assetFactory } from '#test-data/factories/asset-factory';
import { assetFactory } from '#test-data/factories/asset-factory.ts';
vi.mock('#lib/utils/navigation.js', () => ({
vi.mock('#lib/utils/navigation.ts', () => ({
currentUrlReplaceAssetId: vi.fn(),
isSharedLinkRoute: vi.fn().mockReturnValue(false),
}));
@@ -1,11 +1,11 @@
import { locale } from '#lib/stores/preferences.store.ts';
import { renderWithTooltips } from '#tests/helpers';
import { renderWithTooltips } from '#tests/helpers.ts';
import { screen } from '@testing-library/svelte';
import { DateTime } from 'luxon';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import MaintenanceBackupEntry from './MaintenanceBackupEntry.svelte';
vi.mock('#lib/services/database-backups.service.js', () => ({
vi.mock('#lib/services/database-backups.service.ts', () => ({
getDatabaseBackupActions: () => ({
Download: { type: 'command', title: 'Download', onAction: vi.fn() },
Delete: { type: 'command', title: 'Delete', onAction: vi.fn() },
@@ -1,7 +1,7 @@
import { MemoryType, type MemoryResponseDto } from '@immich/sdk';
import { cleanup, fireEvent, render } from '@testing-library/svelte';
import { tick } from 'svelte';
import { assetFactory } from '@test-data/factories/asset-factory';
import { assetFactory } from '#test-data/factories/asset-factory.ts';
import MemoriesLane from './__test__/MemoriesLaneHarness.svelte';
const date = '2026-10-18';
@@ -1,9 +1,9 @@
<script lang="ts">
import MemoryCard from '$lib/components/memories/MemoryCard.svelte';
import { Route } from '$lib/route';
import { getAssetMediaUrl, memoryLaneTitle } from '$lib/utils';
import { getAltText } from '$lib/utils/thumbnail-util';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import MemoryCard from '#lib/components/memories/MemoryCard.svelte';
import { Route } from '#lib/route.ts';
import { getAssetMediaUrl, memoryLaneTitle } from '#lib/utils.ts';
import { getAltText } from '#lib/utils/thumbnail-util.ts';
import { toTimelineAsset } from '#lib/utils/timeline-util.ts';
import { MemoryType, type MemoryResponseDto } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiChevronLeft, mdiChevronRight, mdiPause, mdiPlay } from '@mdi/js';
@@ -1,5 +1,5 @@
<script lang="ts">
import MemoriesLane from '$lib/components/memories/MemoriesLane.svelte';
import MemoriesLane from '#lib/components/memories/MemoriesLane.svelte';
import { TooltipProvider } from '@immich/ui';
import type { ComponentProps } from 'svelte';
@@ -5,7 +5,7 @@ import {
} from '#lib/components/shared-components/album-selection/album-selection-utils.ts';
import { AlbumSortBy, SortOrder } from '#lib/stores/preferences.store.ts';
import type { AlbumResponseDto } from '@immich/sdk';
import { albumFactory } from '#test-data/factories/album-factory';
import { albumFactory } from '#test-data/factories/album-factory.ts';
// Some helper functions to make tests below more readable
const createNewAlbumRow = (selected: boolean) => ({
@@ -46,7 +46,7 @@
Popup,
ScaleControl,
} from 'svelte-maplibre';
import type { SelectionBBox } from './types';
import type { SelectionBBox } from './types.ts';
import { createPreloadedQuery } from '#lib/queries/client.ts';
import { getPublicConfigQuery } from '#lib/queries/config-public.query.ts';
@@ -10,7 +10,7 @@
import { t } from 'svelte-i18n';
import SearchFilters from './SearchFilters.svelte';
import { searchManager } from '#lib/managers/search-manager.svelte.ts';
import { getSearchTypePlaceholder, isPopoverContent } from './search-bar-utils';
import { getSearchTypePlaceholder, isPopoverContent } from './search-bar-utils.ts';
type Props = {
grayTheme: boolean;
@@ -1,5 +1,5 @@
<script lang="ts">
import { getSearchDatePreset, SearchDatePreset } from './search-bar-utils';
import { getSearchDatePreset, SearchDatePreset } from './search-bar-utils.ts';
import { DatePicker, Text } from '@immich/ui';
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
@@ -34,7 +34,7 @@
getSearchPlacesTitle,
getSearchTagsTitle,
getSearchTypeTitle,
} from './search-bar-utils';
} from './search-bar-utils.ts';
import { onMount } from 'svelte';
import { searchManager } from '#lib/managers/search-manager.svelte.ts';
import SearchButton from './SearchButton.svelte';
@@ -1,6 +1,6 @@
<script lang="ts">
import ImageThumbnail from '#lib/components/assets/thumbnail/ImageThumbnail.svelte';
import { getPeople, getSearchPeopleTitle } from './search-bar-utils';
import { getPeople, getSearchPeopleTitle } from './search-bar-utils.ts';
import SingleGridRow from '#lib/components/shared-components/SingleGridRow.svelte';
import SearchBar from '#lib/elements/SearchBar.svelte';
import { getPeopleThumbnailUrl } from '#lib/utils.ts';
@@ -7,8 +7,8 @@ import type { TagResponseDto } from '@immich/sdk';
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { preferencesFactory } from '#test-data/factories/preferences-factory';
import { userAdminFactory } from '#test-data/factories/user-factory';
import { preferencesFactory } from '#test-data/factories/preferences-factory.ts';
import { userAdminFactory } from '#test-data/factories/user-factory.ts';
const tag = (id: string, value: string): TagResponseDto => ({
id,
@@ -8,7 +8,7 @@
import { SvelteSet } from 'svelte/reactivity';
import { mdiClose } from '@mdi/js';
import SearchButton from './SearchButton.svelte';
import { getSearchTagsTitle } from './search-bar-utils';
import { getSearchTagsTitle } from './search-bar-utils.ts';
import { searchManager } from '#lib/managers/search-manager.svelte.ts';
interface Props {
@@ -1,5 +1,5 @@
<script lang="ts">
import { getSearchTypeTitle } from './search-bar-utils';
import { getSearchTypeTitle } from './search-bar-utils.ts';
import SearchButton from './SearchButton.svelte';
import { searchManager } from '#lib/managers/search-manager.svelte.ts';
import { Text } from '@immich/ui';
@@ -1,7 +1,7 @@
import { sdkMock } from '#lib/__mocks__/sdk.mock.ts';
import RecentAlbums from '#lib/components/shared-components/side-bar/RecentAlbums.svelte';
import { render, screen } from '@testing-library/svelte';
import { albumFactory } from '#test-data/factories/album-factory';
import { albumFactory } from '#test-data/factories/album-factory.ts';
describe('RecentAlbums component', () => {
it('sorts albums by most recently updated', async () => {
@@ -1,5 +1,5 @@
import SideBarSection from '#lib/components/sidebar/Sidebar.svelte';
import { sidebarStore } from '#lib/stores/sidebar.svelte.js';
import { sidebarStore } from '#lib/stores/sidebar.svelte.ts';
import { fireEvent, render, screen } from '@testing-library/svelte';
import { vi } from 'vitest';
@@ -11,11 +11,11 @@ const mocks = vi.hoisted(() => {
};
});
vi.mock('#lib/stores/media-query-manager.svelte.js', () => ({
vi.mock('#lib/stores/media-query-manager.svelte.ts', () => ({
mediaQueryManager: mocks.mediaQueryManager,
}));
vi.mock('#lib/stores/sidebar.svelte.js', () => ({
vi.mock('#lib/stores/sidebar.svelte.ts', () => ({
sidebarStore: {
isOpen: false,
reset: vi.fn(),
@@ -1,9 +1,9 @@
import { AssetMultiSelectManager } from '#lib/managers/asset-multi-select-manager.svelte.ts';
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
import { AssetVisibility } from '@immich/sdk';
import { timelineAssetFactory } from '#test-data/factories/asset-factory';
import { preferencesFactory } from '#test-data/factories/preferences-factory';
import { userAdminFactory } from '#test-data/factories/user-factory';
import { timelineAssetFactory } from '#test-data/factories/asset-factory.ts';
import { preferencesFactory } from '#test-data/factories/preferences-factory.ts';
import { userAdminFactory } from '#test-data/factories/user-factory.ts';
describe('AssetMultiSelectManager', () => {
let sut: AssetMultiSelectManager;
@@ -1,8 +1,8 @@
import { setDifference, type TimelineDate } from '#lib/utils/timeline-util.ts';
import { AssetOrder } from '@immich/sdk';
import type { TimelineDay } from './timeline-day.svelte';
import type { TimelineMonth } from './timeline-month.svelte';
import type { TimelineAsset } from './types';
import type { TimelineDay } from './timeline-day.svelte.ts';
import type { TimelineMonth } from './timeline-month.svelte.ts';
import type { TimelineAsset } from './types.ts';
export class GroupInsertionCache {
#lookupCache: {
@@ -1,6 +1,6 @@
import { TUNABLES } from '#lib/utils/tunables.ts';
import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
import { TimelineManager } from '../timeline-manager.svelte.ts';
import type { TimelineMonth } from '../timeline-month.svelte.ts';
const {
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
@@ -1,6 +1,6 @@
import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
import type { UpdateGeometryOptions } from '../types';
import { TimelineManager } from '../timeline-manager.svelte.ts';
import type { TimelineMonth } from '../timeline-month.svelte.ts';
import type { UpdateGeometryOptions } from '../types.ts';
export function updateGeometry(timelineManager: TimelineManager, month: TimelineMonth, options: UpdateGeometryOptions) {
const { invalidateHeight, noDefer = false } = options;
@@ -1,9 +1,9 @@
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
import { toISOYearMonthUTC } from '#lib/utils/timeline-util.ts';
import { getTimeBucket } from '@immich/sdk';
import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
import type { TimelineManagerOptions } from '../types';
import { TimelineManager } from '../timeline-manager.svelte.ts';
import type { TimelineMonth } from '../timeline-month.svelte.ts';
import type { TimelineManagerOptions } from '../types.ts';
export async function loadFromTimeBuckets(
timelineManager: TimelineManager,
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import type { TimelineMonth } from '../timeline-month.svelte';
import { findClosestTimelineMonthForDate } from './search-support.svelte';
import type { TimelineMonth } from '../timeline-month.svelte.ts';
import { findClosestTimelineMonthForDate } from './search-support.svelte.ts';
function createMockTimelineMonth(year: number, month: number): TimelineMonth {
return {
@@ -1,9 +1,9 @@
import { getOrderingDate, plainDateTimeCompare, type TimelineYearMonth } from '#lib/utils/timeline-util.ts';
import { AssetOrder, AssetOrderBy, type AssetResponseDto } from '@immich/sdk';
import { DateTime } from 'luxon';
import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte';
import type { AssetDescriptor, Direction, TimelineAsset } from '../types';
import { TimelineManager } from '../timeline-manager.svelte.ts';
import type { TimelineMonth } from '../timeline-month.svelte.ts';
import type { AssetDescriptor, Direction, TimelineAsset } from '../types.ts';
export async function getAssetWithOffset(
timelineManager: TimelineManager,
@@ -4,9 +4,9 @@ import { getOrderingDate, plainDateTimeCompare } from '#lib/utils/timeline-util.
import { TUNABLES } from '#lib/utils/tunables.ts';
import { AssetOrder, AssetOrderBy } from '@immich/sdk';
import { SvelteSet } from 'svelte/reactivity';
import type { TimelineMonth } from './timeline-month.svelte';
import type { Direction, MoveAsset, TimelineAsset } from './types';
import { ViewerAsset } from './viewer-asset.svelte';
import type { TimelineMonth } from './timeline-month.svelte.ts';
import type { Direction, MoveAsset, TimelineAsset } from './types.ts';
import { ViewerAsset } from './viewer-asset.svelte.ts';
const {
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
@@ -5,9 +5,9 @@ import { AbortError } from '#lib/utils.ts';
import { fromISODateTimeUTCToObject } from '#lib/utils/timeline-util.ts';
import { AssetOrderBy, AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
import { tick } from 'svelte';
import { assetFactory, timelineAssetFactory, toResponseDto } from '#test-data/factories/asset-factory';
import { TimelineManager } from './timeline-manager.svelte';
import type { TimelineAsset } from './types';
import { assetFactory, timelineAssetFactory, toResponseDto } from '#test-data/factories/asset-factory.ts';
import { TimelineManager } from './timeline-manager.svelte.ts';
import type { TimelineAsset } from './types.ts';
async function getAssets(timelineManager: TimelineManager) {
const assets = [];
@@ -35,9 +35,9 @@ import {
} from '@immich/sdk';
import { clamp, isEqual } from 'lodash-es';
import { SvelteDate, SvelteSet } from 'svelte/reactivity';
import { isMismatched, updateObject } from './internal/utils.svelte';
import { TimelineDay } from './timeline-day.svelte';
import { TimelineMonth } from './timeline-month.svelte';
import { isMismatched, updateObject } from './internal/utils.svelte.ts';
import { TimelineDay } from './timeline-day.svelte.ts';
import { TimelineMonth } from './timeline-month.svelte.ts';
import type {
AssetDescriptor,
Direction,
@@ -46,7 +46,7 @@ import type {
TimelineAsset,
TimelineManagerOptions,
Viewport,
} from './types';
} from './types.ts';
type ViewportTopMonthIntersection = {
month: TimelineMonth | undefined;
@@ -22,11 +22,11 @@ import { AssetOrder, AssetOrderBy, type TimeBucketAssetResponseDto } from '@immi
import { t } from 'svelte-i18n';
import { SvelteSet } from 'svelte/reactivity';
import { get } from 'svelte/store';
import { GroupInsertionCache } from './group-insertion-cache.svelte';
import { TimelineDay } from './timeline-day.svelte';
import type { TimelineManager } from './timeline-manager.svelte';
import type { AssetDescriptor, Direction, MoveAsset, TimelineAsset } from './types';
import { ViewerAsset } from './viewer-asset.svelte';
import { GroupInsertionCache } from './group-insertion-cache.svelte.ts';
import { TimelineDay } from './timeline-day.svelte.ts';
import type { TimelineManager } from './timeline-manager.svelte.ts';
import type { AssetDescriptor, Direction, MoveAsset, TimelineAsset } from './types.ts';
import { ViewerAsset } from './viewer-asset.svelte.ts';
export class TimelineMonth {
#viewportProximity: ViewportProximity = $state(ViewportProximity.FarFromViewport);
@@ -1,4 +1,4 @@
import type { TimelineAsset } from './types';
import type { TimelineAsset } from './types.ts';
export const assetsSnapshot = (assets: TimelineAsset[]) => assets.map((asset) => $state.snapshot(asset));
@@ -1,5 +1,5 @@
import type { CommonPosition } from '#lib/utils/layout-utils.ts';
import type { TimelineAsset } from './types';
import type { TimelineAsset } from './types.ts';
export class ViewerAsset {
position: CommonPosition | undefined = $state.raw();
@@ -7,7 +7,7 @@ import { calcNewDate } from '#lib/modals/timezone-utils.ts';
import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { DateTime } from 'luxon';
import { userAdminFactory } from '#test-data/factories/user-factory';
import { userAdminFactory } from '#test-data/factories/user-factory.ts';
import AssetSelectionChangeDateModal from './AssetSelectionChangeDateModal.svelte';
describe('DateSelectionModal component', () => {
@@ -7,7 +7,7 @@
SlideshowNavigation,
SlideshowState,
slideshowStore,
} from '../stores/slideshow.store';
} from '../stores/slideshow.store.ts';
const {
slideshowDelay,
+14 -14
View File
@@ -1,3 +1,15 @@
import { eventManager } from '#lib/managers/event-manager.svelte.ts';
import { executeMutation, queryClient } from '#lib/queries/client.ts';
import { getAdminConfigQuery, updateAdminConfigMutation } from '#lib/queries/config-admin.query.ts';
import { getPublicConfigQuery } from '#lib/queries/config-public.query.ts';
import {
deleteLibraryMutation,
getAllLibrariesQuery,
getLibraryStatisticsQuery,
updateLibraryMutation,
} from '#lib/queries/library.query.ts';
import { getServerConfigQuery, getStorageQuery } from '#lib/queries/server-info.query.ts';
import { handleError } from '#lib/utils/handle-error.ts';
import {
deleteLibrary,
getAdminConfig,
@@ -7,22 +19,10 @@ import {
type AdminConfigDto,
type LibraryResponseDto,
} from '@immich/sdk';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { executeMutation, queryClient } from '$lib/queries/client';
import { getAdminConfigQuery, updateAdminConfigMutation } from '$lib/queries/config-admin.query';
import { getPublicConfigQuery } from '$lib/queries/config-public.query';
import {
deleteLibraryMutation,
getAllLibrariesQuery,
getLibraryStatisticsQuery,
updateLibraryMutation,
} from '$lib/queries/library.query';
import { getServerConfigQuery, getStorageQuery } from '$lib/queries/server.query';
import { handleError } from '$lib/utils/handle-error';
vi.mock('@immich/ui', () => ({ toastManager: { primary: vi.fn(), info: vi.fn() } }));
vi.mock('$lib/utils/handle-error', () => ({ handleError: vi.fn() }));
vi.mock('$lib/utils/i18n', () => ({ getFormatter: () => Promise.resolve((key: string) => key) }));
vi.mock('#lib/utils/handle-error.ts', () => ({ handleError: vi.fn() }));
vi.mock('#lib/utils/i18n.ts', () => ({ getFormatter: () => Promise.resolve((key: string) => key) }));
vi.mock('@immich/sdk', () => ({
createLibrary: vi.fn(),
deleteLibrary: vi.fn(),
+14 -14
View File
@@ -1,3 +1,14 @@
import { eventManager } from '#lib/managers/event-manager.svelte.ts';
import { executeMutation, queryClient } from '#lib/queries/client.ts';
import {
createUserAdminMutation,
deleteUserAdminMutation,
getUserAdminQuery,
restoreUserAdminMutation,
searchUsersAdminQuery,
updateUserAdminMutation,
} from '#lib/queries/user-admin.query.ts';
import { handleError } from '#lib/utils/handle-error.ts';
import {
createUserAdmin,
deleteUserAdmin,
@@ -7,22 +18,11 @@ import {
type UserAdminResponseDto,
} from '@immich/sdk';
import { QueryObserver } from '@tanstack/svelte-query';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { executeMutation, queryClient } from '$lib/queries/client';
import {
createUserAdminMutation,
deleteUserAdminMutation,
getUserAdminQuery,
restoreUserAdminMutation,
searchUsersAdminQuery,
updateUserAdminMutation,
} from '$lib/queries/user-admin.query';
import { handleError } from '$lib/utils/handle-error';
vi.mock('$lib', () => ({ getHeatmapRange: vi.fn() }));
vi.mock('#lib', () => ({ getHeatmapRange: vi.fn() }));
vi.mock('@immich/ui', () => ({ toastManager: { primary: vi.fn(), info: vi.fn() } }));
vi.mock('$lib/utils/handle-error', () => ({ handleError: vi.fn() }));
vi.mock('$lib/utils/i18n', () => ({ getFormatter: () => Promise.resolve((key: string) => key) }));
vi.mock('#lib/utils/handle-error.ts', () => ({ handleError: vi.fn() }));
vi.mock('#lib/utils/i18n.ts', () => ({ getFormatter: () => Promise.resolve((key: string) => key) }));
vi.mock('@immich/sdk', () => ({
CalendarHeatmapType: { Upload: 'upload' },
createUserAdmin: vi.fn(),
+7 -7
View File
@@ -7,10 +7,10 @@ import { getFormatter } from '#lib/utils/i18n.ts';
import { getAssetInfo, type UserConfigDto } from '@immich/sdk';
import { toastManager } from '@immich/ui';
import { vitest } from 'vitest';
import { assetFactory } from '#test-data/factories/asset-factory';
import { preferencesFactory } from '#test-data/factories/preferences-factory';
import { sharedLinkFactory } from '#test-data/factories/shared-link-factory';
import { userAdminFactory } from '#test-data/factories/user-factory';
import { assetFactory } from '#test-data/factories/asset-factory.ts';
import { preferencesFactory } from '#test-data/factories/preferences-factory.ts';
import { sharedLinkFactory } from '#test-data/factories/shared-link-factory.ts';
import { userAdminFactory } from '#test-data/factories/user-factory.ts';
vitest.mock('@immich/ui', () => ({
toastManager: {
@@ -18,15 +18,15 @@ vitest.mock('@immich/ui', () => ({
},
}));
vitest.mock('#lib/utils/i18n.js', () => ({
vitest.mock('#lib/utils/i18n.ts', () => ({
getFormatter: vitest.fn(),
getPreferredLocale: vitest.fn(),
}));
vitest.mock('@immich/sdk');
vitest.mock('#lib/utils.js', async () => {
const originalModule = await vitest.importActual('#lib/utils.js');
vitest.mock('#lib/utils.ts', async () => {
const originalModule = await vitest.importActual('#lib/utils.ts');
return {
...originalModule,
sleep: vitest.fn(),
@@ -1,7 +1,7 @@
import { asUrl } from '#lib/services/shared-link.service.ts';
import { sharedLinkFactory } from '#test-data/factories/shared-link-factory';
import { sharedLinkFactory } from '#test-data/factories/shared-link-factory.ts';
vi.mock(import('#lib/queries/client.js'), async (importOriginal) => ({
vi.mock(import('#lib/queries/client.ts'), async (importOriginal) => ({
...(await importOriginal()),
readPreloadedQuery: vi.fn().mockReturnValue({ server: { externalDomain: 'http://localhost:2283' } }),
}));
@@ -1,20 +1,20 @@
import { getUserAdminActions } from '#lib/services/user-admin.service.ts';
import { UserStatus, type UserAdminResponseDto } from '@immich/sdk';
import type { MessageFormatter } from 'svelte-i18n';
import { getUserAdminActions } from '$lib/services/user-admin.service';
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('@immich/ui', () => ({ modalManager: { show: vi.fn(), showDialog: vi.fn() } }));
vi.mock('$lib/managers/auth-manager.svelte', () => ({ authManager: { user: { id: 'self' } } }));
vi.mock('$lib/modals/PasswordResetSuccessModal.svelte', () => ({ default: {} }));
vi.mock('$lib/modals/UserDeleteConfirmModal.svelte', () => ({ default: {} }));
vi.mock('$lib/modals/UserRestoreConfirmModal.svelte', () => ({ default: {} }));
vi.mock('$lib/queries/client', () => ({
vi.mock('#lib/managers/auth-manager.svelte.ts', () => ({ authManager: { user: { id: 'self' } } }));
vi.mock('#lib/modals/PasswordResetSuccessModal.svelte', () => ({ default: {} }));
vi.mock('#lib/modals/UserDeleteConfirmModal.svelte', () => ({ default: {} }));
vi.mock('#lib/modals/UserRestoreConfirmModal.svelte', () => ({ default: {} }));
vi.mock('#lib/queries/client.ts', () => ({
executeMutation: vi.fn(),
readPreloadedQuery: () => ({ user: { deleteDelay: 7 } }),
}));
vi.mock('$lib/queries/user-admin.query', () => ({ updateUserAdminMutation: vi.fn() }));
vi.mock('$lib/route', () => ({ Route: { newUser: vi.fn(), viewUser: vi.fn(), editUser: vi.fn() } }));
vi.mock('$lib/utils/i18n', () => ({ getFormatter: vi.fn() }));
vi.mock('#lib/queries/user-admin.query.ts', () => ({ updateUserAdminMutation: vi.fn() }));
vi.mock('#lib/route.ts', () => ({ Route: { newUser: vi.fn(), viewUser: vi.fn(), editUser: vi.fn() } }));
vi.mock('#lib/utils/i18n.ts', () => ({ getFormatter: vi.fn() }));
describe('admin actions for active and deleted accounts', () => {
it('does not format a deletion date for an active account', () => {
@@ -1,5 +1,5 @@
<script lang="ts">
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
import { BaseEventManager } from '#lib/utils/base-event-manager.svelte.ts';
import EventSubscriber from './EventSubscriber.svelte';
const manager = new BaseEventManager<{ update: [] }>();
@@ -1,6 +1,6 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import type { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
import type { BaseEventManager } from '#lib/utils/base-event-manager.svelte.ts';
const { manager }: { manager: BaseEventManager<{ update: [] }> } = $props();
let received = $state(0);
+1 -1
View File
@@ -4,7 +4,7 @@ import { AssetVisibility, deleteAssets as deleteBulk, restoreAssets, type StackR
import { toastManager } from '@immich/ui';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import { handleError } from './handle-error';
import { handleError } from './handle-error.ts';
export type OnDelete = (assetIds: string[]) => void;
export type OnUndoDelete = (assets: TimelineAsset[]) => void;
@@ -1,7 +1,7 @@
import { AdaptiveImageLoader, type QualityList } from '#lib/utils/adaptive-image-loader.svelte.ts';
import { cancelImageUrl } from '#lib/utils/sw-messaging.ts';
vi.mock('#lib/utils/sw-messaging.js', () => ({
vi.mock('#lib/utils/sw-messaging.ts', () => ({
cancelImageUrl: vi.fn(),
}));
+1 -1
View File
@@ -1,4 +1,4 @@
import { checkWebUpdate } from './app-update';
import { checkWebUpdate } from './app-update.ts';
describe('web update checking', () => {
it.each([
+1 -1
View File
@@ -1,5 +1,5 @@
import { AssetVisibility, updateAsset, type AssetResponseDto } from '@immich/sdk';
import { canCopyImageToClipboard, getAssetFilename, getFilenameExtension, toggleArchive } from './asset-utils';
import { canCopyImageToClipboard, getAssetFilename, getFilenameExtension, toggleArchive } from './asset-utils.ts';
vi.mock('@immich/sdk', async (importOriginal) => {
const actual = await importOriginal<typeof import('@immich/sdk')>();
+1 -1
View File
@@ -27,7 +27,7 @@ import { toastManager } from '@immich/ui';
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import { handleError } from './handle-error';
import { handleError } from './handle-error.ts';
export const removeTag = async ({
assetIds,
+2 -2
View File
@@ -1,7 +1,7 @@
import { locale } from '#lib/stores/preferences.store.ts';
import { getAlbumDateRange, getShortDateRange } from './date-time';
import { getAlbumDateRange, getShortDateRange } from './date-time.ts';
vitest.mock('#lib/stores/preferences.store.js', { spy: true });
vitest.mock('#lib/stores/preferences.store.ts', { spy: true });
describe('getShortDateRange', () => {
beforeEach(() => {
+2 -2
View File
@@ -6,8 +6,8 @@ import * as utils from '#lib/utils.ts';
import { AssetMediaStatus, type AssetMediaResponseDto, type UserAdminResponseDto } from '@immich/sdk';
import { get } from 'svelte/store';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { preferencesFactory } from '#test-data/factories/preferences-factory';
import { fileUploadHandler } from './file-uploader';
import { preferencesFactory } from '#test-data/factories/preferences-factory.ts';
import { fileUploadHandler } from './file-uploader.ts';
describe('fileUploader error handling', () => {
const mockFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
+1 -1
View File
@@ -19,7 +19,7 @@ import { toastManager } from '@immich/ui';
import { tick } from 'svelte';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import { handleError } from './handle-error';
import { handleError } from './handle-error.ts';
export const uploadExecutionQueue = new ExecutorQueue({ concurrency: 2 });
+2 -1
View File
@@ -121,6 +121,7 @@ export const calculateFittedFontSize = (
return clamp(fontSize, MIN_FONT_SIZE, MAX_FONT_SIZE);
}
// eslint-disable-next-line tscompat/tscompat -- This assigns the Canvas 2D font property, not a font() method.
context.font = getReferenceFont();
let maxCharWidth = 0;
@@ -145,7 +146,7 @@ export const calculateFittedFontSize = (
return clamp((1.4 * fitWidth) / text.length, MIN_FONT_SIZE, MAX_FONT_SIZE);
}
// Unsupported in Safari iOS <16.6; falls back to default canvas font, giving less accurate but functional sizing
// eslint-disable-next-line tscompat/tscompat -- This assigns the Canvas 2D font property, not a font() method.
context.font = getReferenceFont();
const metrics = context.measureText(text);
+1 -1
View File
@@ -1,5 +1,5 @@
import type { PersonResponseDto } from '@immich/sdk';
import { searchNameLocal } from './person';
import { searchNameLocal } from './person.ts';
const makePerson = (overrides: Partial<PersonResponseDto> = {}): PersonResponseDto => ({
id: 'person-1',
+1 -1
View File
@@ -1,7 +1,7 @@
import type { PersonResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
import { derived } from 'svelte/store';
import { normalizeSearchString } from './string-utils';
import { normalizeSearchString } from './string-utils.ts';
export const searchNameLocal = (
name: string,
+1 -1
View File
@@ -1,4 +1,4 @@
import { ServiceWorkerMessenger } from './sw-messenger';
import { ServiceWorkerMessenger } from './sw-messenger.ts';
const hasServiceWorker = globalThis.isSecureContext && 'serviceWorker' in navigator;
const messenger = hasServiceWorker ? new ServiceWorkerMessenger(navigator.serviceWorker) : undefined;
+1 -1
View File
@@ -1,9 +1,9 @@
import type { TimelineAsset } from '#lib/managers/timeline-manager/types.ts';
import { locale } from '#lib/stores/preferences.store.ts';
import { getAltText } from '#lib/utils/thumbnail-util.ts';
import { AssetVisibility } from '@immich/sdk';
import { init, register, waitLocale } from 'svelte-i18n';
import { get } from 'svelte/store';
import { locale } from '$lib/stores/preferences.store';
interface Person {
name: string;
+3
View File
@@ -11,6 +11,7 @@ export async function acquireWakeLock() {
return;
}
// eslint-disable-next-line tscompat/tscompat -- The feature check above guards Wake Lock access.
if (sentinel && !sentinel.released) {
return;
}
@@ -19,6 +20,7 @@ export async function acquireWakeLock() {
}
acquiring = true;
try {
// eslint-disable-next-line tscompat/tscompat -- Browsers without Wake Lock return before this call.
sentinel = await navigator.wakeLock.request('screen');
} catch (error) {
console.warn('Failed to acquire wake lock:', error);
@@ -36,6 +38,7 @@ export async function releaseWakeLock() {
// Unset first to avoid race condition after await
sentinel = undefined;
// eslint-disable-next-line tscompat/tscompat -- A sentinel is only acquired on browsers supporting Wake Lock.
await toReleaseSentinel.release();
}
+5 -5
View File
@@ -8,19 +8,19 @@ describe('hashFile', () => {
});
it('hashFileJs produces the correct SHA-1 hex digest', async () => {
const { hashFileJs } = await import('./hash-file');
const { hashFileJs } = await import('./hash-file.ts');
const file = new File([content], 'a.txt');
await expect(hashFileJs(file)).resolves.toBe(knownSha1Hex);
});
it('hashFileWasm produces the correct SHA-1 hex digest', async () => {
const { hashFileWasm } = await import('./hash-file');
const { hashFileWasm } = await import('./hash-file.ts');
const file = new File([content], 'a.txt');
await expect(hashFileWasm(file)).resolves.toBe(knownSha1Hex);
});
it('hashFileWasm and hashFileJs produce identical output for the same input', async () => {
const { hashFileWasm, hashFileJs } = await import('./hash-file');
const { hashFileWasm, hashFileJs } = await import('./hash-file.ts');
for (const size of [0, 1, 1024, 10_000]) {
const file = new File([new Uint8Array(size)], 'x.bin');
const [wasmHash, jsHash] = await Promise.all([hashFileWasm(file), hashFileJs(file)]);
@@ -34,13 +34,13 @@ describe('hashFile', () => {
}));
vi.resetModules();
const { hashFile } = await import('./hash-file');
const { hashFile } = await import('./hash-file.ts');
const file = new File([content], 'a.txt');
await expect(hashFile(file)).resolves.toBe(knownSha1Hex);
});
it('uses the WASM path when available and matches the JS output', async () => {
const { hashFile } = await import('./hash-file');
const { hashFile } = await import('./hash-file.ts');
const file = new File([content], 'a.txt');
await expect(hashFile(file)).resolves.toBe(knownSha1Hex);
});

Some files were not shown because too many files have changed in this diff Show More