Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8119a5ee22 | ||
|
|
54881128d6 | ||
|
|
d15be0dd83 | ||
|
|
47cc38061e | ||
|
|
db411a4497 | ||
|
|
f1a09c1389 | ||
|
|
c8222bba94 | ||
|
|
af66681b99 | ||
|
|
a93bbdf3e7 | ||
|
|
1707635743 | ||
|
|
7a6123f8e6 | ||
|
|
4960eddaf3 | ||
|
|
d35a6613e6 | ||
|
|
f96816b55f | ||
|
|
eca952720a | ||
|
|
c482c1e53e | ||
|
|
75a1d7c5c8 | ||
|
|
5630e541e3 | ||
|
|
27ccbee82d | ||
|
|
3458767d66 | ||
|
|
cd28300f94 | ||
|
|
d8030f3d14 | ||
|
|
0ff6470791 | ||
|
|
717152b191 | ||
|
|
06dbcf7c84 | ||
|
|
9767ea9c02 | ||
|
|
071b8db397 | ||
|
|
1c9813e130 | ||
|
|
6569aad605 | ||
|
|
a2794f0986 | ||
|
|
6ffc365b75 | ||
|
|
dc19d3a259 | ||
|
|
f2a502448d | ||
|
|
a3ffe3b3c3 | ||
|
|
8125af4859 | ||
|
|
e5b4dd1041 | ||
|
|
621504fcda | ||
|
|
919a4fd9ef | ||
|
|
2ed9207c3f | ||
|
|
c48394aa6e | ||
|
|
86c8b9e04d | ||
|
|
1190d0cc80 | ||
|
|
9669f7cc6e |
@@ -26,10 +26,17 @@ jobs:
|
||||
env:
|
||||
BODY: ${{ github.event.pull_request.body }}
|
||||
run: |
|
||||
BODY=$(printf '%s\n' "$BODY" | tr -d '\r' | perl -0pe 's/<!--.*?-->//gs')
|
||||
section() { printf '%s\n' "$BODY" | awk -v h="$1" 'index($0, h) == 1 { f = 1; next } /^## / { f = 0 } f'; }
|
||||
|
||||
OK=true
|
||||
while IFS= read -r header; do
|
||||
printf '%s\n' "$BODY" | grep -qF "$header" || OK=false
|
||||
done < <(sed '/<!--/,/-->/d' .github/pull_request_template.md | grep "^## ")
|
||||
done < <(perl -0pe 's/<!--.*?-->//gs' .github/pull_request_template.md | grep "^## ")
|
||||
|
||||
section "## Checklist" | grep -qE '^\s*- \[[xX]\]' || OK=false
|
||||
[[ "$(section "## Please describe to which degree" | tr -d '[:space:]')" =~ ^(\.\.\.)?$ ]] && OK=false
|
||||
|
||||
echo "uses_template=$OK" | tee --append "$GITHUB_OUTPUT"
|
||||
|
||||
close_template:
|
||||
@@ -51,7 +58,7 @@ jobs:
|
||||
run: |
|
||||
gh api graphql \
|
||||
-f prId="$NODE_ID" \
|
||||
-f body="This PR has been automatically closed as the description doesn't follow [our template](https://github.com/immich-app/immich/blob/main/.github/pull_request_template.md). After you edit it to match the template, the PR will automatically be reopened." \
|
||||
-f body="This PR has been automatically closed as the description doesn't follow [our template](https://github.com/immich-app/immich/blob/main/.github/pull_request_template.md). Please make sure every section is present and filled in. After you edit it to match the template, the PR will automatically be reopened." \
|
||||
-f query='
|
||||
mutation CommentAndClosePR($prId: ID!, $body: String!) {
|
||||
addComment(input: {
|
||||
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
working_directory: ./mobile
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1.329.0
|
||||
uses: ruby/setup-ruby@e1bd6146574da5e0795075b5319b300edb30c5b0 # v1.333.0
|
||||
with:
|
||||
ruby-version: '3.4'
|
||||
bundler-cache: true # zizmor: ignore[cache-poisoning] release branches cannot read caches written by pull requests
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for breaking API changes
|
||||
uses: oasdiff/oasdiff-action/breaking@5e81b5c380accc6b523f9d32a637ca630e33620b # v0.1.17
|
||||
uses: oasdiff/oasdiff-action/breaking@9ce3a29eb0e6478dd0140932a678c7fb59d08f22 # v0.1.20
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||
revision: open-api/immich-openapi-specs.json
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -71,10 +71,10 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
# в„№пёЏ Command-line programs to run using the OS shell.
|
||||
# рџ“љ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<a href="readme_i18n/README_uk_UA.md">Українська</a>
|
||||
<a href="readme_i18n/README_ru_RU.md">Русский</a>
|
||||
<a href="readme_i18n/README_bg_BG.md">Български</a>
|
||||
<a href="readme_i18n/README_pl_PL.md">Polski</a>
|
||||
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
|
||||
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
||||
|
||||
+42
-42
@@ -1,69 +1,69 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.opentofu]]
|
||||
version = "1.13.1"
|
||||
version = "1.13.2"
|
||||
backend = "aqua:opentofu/opentofu"
|
||||
|
||||
[tools.opentofu."platforms.linux-arm64"]
|
||||
checksum = "sha256:9c1ef375aa1852db0b2888aa921b640c71f8140d4682aa4fec99378a64fa7dc3"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708055"
|
||||
checksum = "sha256:88e2d9e3f94f97b76ab2faf55ade87578f56aed19c5e45fa139d70e994366d1f"
|
||||
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-x64"]
|
||||
checksum = "sha256:378ada19d4bc70c43732004e8159be771b23b9a5afdf059e5f8a2b3fa2c70a69"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708017"
|
||||
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.macos-arm64"]
|
||||
checksum = "sha256:be78f659f04ef06a9dbd9b3934d46af95d787a3aa38396d459dea395261816a9"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708059"
|
||||
checksum = "sha256:0d1cdc85f38992055b48e7fb161304a2ddfe2e5ee0ff68ffec9d0349c8bd1b59"
|
||||
checksum_source = "mise-versions"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742637"
|
||||
|
||||
[tools.opentofu."platforms.macos-x64"]
|
||||
checksum = "sha256:a73720443ba38712d7d96dc1e857add02c15a790919c653ad07492e9952f8c27"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708142"
|
||||
checksum = "sha256:dc6e4eb1d3ed6550c2ef052596f933dbcd8c806957665ea83ae7537147cb3074"
|
||||
checksum_source = "mise-versions"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742982"
|
||||
|
||||
[tools.opentofu."platforms.windows-x64"]
|
||||
checksum = "sha256:6825ae311e9a15747ce2ede7b7e97c8bb744a4d271fa8113259eeeee3d09438c"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_windows_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708102"
|
||||
checksum = "sha256:e74fc2bda0c81ce696b6b7016a4d5c1f557638161239e4101e9ee7d8e0faba59"
|
||||
checksum_source = "mise-versions"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_windows_amd64.tar.gz"
|
||||
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,6 +1,6 @@
|
||||
[tools]
|
||||
terragrunt = "1.1.1"
|
||||
opentofu = "1.13.1"
|
||||
terragrunt = "1.2.0"
|
||||
opentofu = "1.13.2"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
run = "terragrunt hclfmt"
|
||||
|
||||
+28
-28
@@ -2,34 +2,34 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.52.9"
|
||||
constraints = "4.52.9"
|
||||
version = "4.52.10"
|
||||
constraints = "4.52.10"
|
||||
hashes = [
|
||||
"h1:LRBYMKjZyuld8kgVhzoceRtdtkht+w8LunrdpgqrL28=",
|
||||
"h1:Q2NeLtrjSTZwHHsMZXKpqOAbxF8DWgqpmH5j+1pUZGw=",
|
||||
"h1:Q4wiux11bDVWRmJScdjp8jLbxMRsj8k3xGtPtY4BPDo=",
|
||||
"h1:VVW111jEbRMTu7hXeGQMmZ/oSTB8LIYhjM5ztyymqAI=",
|
||||
"h1:Z7sTk8/B9XYFAEpTq1ib/WsB2QvpegGwef395tHCUSo=",
|
||||
"h1:eakzS6UsflV4gN/oQWdRPYvvmySKkhgKPyykCfPSVH0=",
|
||||
"h1:eesNMA1JUNBKCnuEvRLW2CT3tNrIejbXJG4A176nA9Q=",
|
||||
"h1:o8txgjqnDDODxQQ6ITwHsvPqHllCbMGnJkjCWNJK5ME=",
|
||||
"h1:rDTPIzab3tbGdtU3NmES08+tRlKG8jygNAFDFbheiuM=",
|
||||
"h1:sIoAt5vaxgFBrst1Esfm7ixQQ/T7/JNPZu0m4bw+Bc4=",
|
||||
"h1:sPCs3KnD/db5TMz2CNgiYix7sRS8+FP92T4ipSYT9vw=",
|
||||
"h1:uXPLsMQUYVJjRGUfmfDQXsKzYr1scvF6PYKj+3f0Nr4=",
|
||||
"h1:vQibC9lgBRQjRQ5KxFH61wd3/q2I40groLPjZ/jiikQ=",
|
||||
"zh:0826cad9b57368f06466ec3e1f766086f44a373288753e144f681ddc0e61ab49",
|
||||
"zh:15bf8d8abdcd202652466f3ef91110f143a7fad13c6173c7180e3dc0dbe9254b",
|
||||
"zh:1a60d1d2e55a17942902b1ccc91e8f6cb150c035843d3eefc074f1291a0e7d5f",
|
||||
"zh:23373a63fe06bbfa90c6c105c08429031719bedafa2458973cbdad32a384b5a7",
|
||||
"zh:256a97b79cedee2575002f77ac3bf18888ce0fc97170ae8a357db406bb0a88e3",
|
||||
"zh:3dd9f93c167cd59491ae77781032491d36a80843a1e44f9ee68b41b3f8e3ab8e",
|
||||
"zh:5182d2d61930dd83584d8feb56102614f80ddeec532b939b58085ad74b796a6e",
|
||||
"zh:5e3ca1888ba3b3bd8abd0f7dfcdd82fb5a8819affee899ed7fd891a33c17e0cb",
|
||||
"zh:7a3ce2119e39fb3a90a6a459a26863851309dbc92e0735336cbc98692539cb4d",
|
||||
"zh:c095dc680a646cc078c10bb0551e2ea8f08e7abdbcbc58cfd332c56b58788631",
|
||||
"zh:d2400e010e069c19916c99136e200cf873eba6db2b18d3eb9541b38eb1f565fa",
|
||||
"zh:dbc2f19f09aaefbd69334005bca2bcb0c9b690de1ed76f7ba97ce8e59a6e85df",
|
||||
"zh:e685225f432da3e23b8717848de8c0ad0e22363450b2ac92cd24a56739ab5e9b",
|
||||
"h1:57oeYqz+pYEPAw2GvHARjcER0QYU0teS6DJzS26kCAY=",
|
||||
"h1:6bnAgXSboppQbj79XmRtQLj/58d+b4EozydI8XrqwRE=",
|
||||
"h1:6eP/jGOz1ioH17d3pygKhvsRa4AEKvedKqd+wgyiHYk=",
|
||||
"h1:84gJm57BBSvtfwt7t9ZiU7EKKDQARjsztoDuqB5DfqU=",
|
||||
"h1:99tME9L/Ug/dXElX3Ac7Kx+KD7nhWEEwOTqaQubW18w=",
|
||||
"h1:TnMGVaPpIH5NZX324v+y6j1iW3ym0MqqNekxHlzXT3A=",
|
||||
"h1:Z1L5jI4GiJxAAsfNyZHU4MGTluWEjjKtlB8Xn6cA0Dg=",
|
||||
"h1:dB/WsY27MLla/cwjYggolJhVeHUeS59rlOObAWK2e24=",
|
||||
"h1:gpffNocq9fHK+uQehZQK6YmL1aHeCGrVM1p53dcFP/0=",
|
||||
"h1:jvfw1A2Uck4AjNmmWXwGH6P0sGQrnNY/KUVlqgyqYUM=",
|
||||
"h1:ojvrl9uI1EPtAwKRHim9eSc4Lc88thU/2BFGdJLEwrc=",
|
||||
"h1:rJZJK06NYCcSZhY5fwsRUSznSmAmRWVg4sO0auvbURo=",
|
||||
"h1:y2m6i2SnQ3ZxiJHlMWNLHJrEbNqQT6OpDGyhFP+Nmds=",
|
||||
"zh:06c09cc50d6c61fb5f1ca79e4b22d5f383c6a848f885ed542dadef44ab4bfd55",
|
||||
"zh:205bdfdac49d6b25ec2ca7ecffd39bf47126340e1943066bdbbd64d6d72577d2",
|
||||
"zh:341ed688772d8e2bd9a148c57d56d5dfbe3d625bc50296a505cbae3d29694562",
|
||||
"zh:3790619bae55558ab4d93f53686f7cf55f35138f800ea07b0b9c20cce75b9c0d",
|
||||
"zh:44284a5b67e34f1381bbc47b6e765cee581037e25cd3b459caec069493b63898",
|
||||
"zh:493a173f3d5245db82782e8c1aba63e34c6fcf6b952b9288926b519bfe9d9725",
|
||||
"zh:4eb7846381fee6eefdb7e0aaf80153ed71d35547752f96987218a9429645844f",
|
||||
"zh:850b1d1da770f48def40223d754ff65c31fabb1a7f334ed6ebb40eba09c41572",
|
||||
"zh:9a29a08a9fd40d7c1beefe8597617c2fc475d62ec6d5ca8fcc2cd1a056a67a2e",
|
||||
"zh:9e57472f0a56863204f5b2eef125d4fb1222eca793c6a5ac9f4df6c1776edc48",
|
||||
"zh:a9eff98931da596c2d335359841bfd668f4b6cde257e15546a558c7f5d72773b",
|
||||
"zh:c9293fb3390c79e4f5072e019c0a2c4c7408a55a55e78ae37f739a1da75886ef",
|
||||
"zh:c9e98abec7f7a1e2669dd9c065896217125d774640e4673c1303a942736b3eb4",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.52.9"
|
||||
version = "4.52.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-28
@@ -2,34 +2,34 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.52.9"
|
||||
constraints = "4.52.9"
|
||||
version = "4.52.10"
|
||||
constraints = "4.52.10"
|
||||
hashes = [
|
||||
"h1:LRBYMKjZyuld8kgVhzoceRtdtkht+w8LunrdpgqrL28=",
|
||||
"h1:Q2NeLtrjSTZwHHsMZXKpqOAbxF8DWgqpmH5j+1pUZGw=",
|
||||
"h1:Q4wiux11bDVWRmJScdjp8jLbxMRsj8k3xGtPtY4BPDo=",
|
||||
"h1:VVW111jEbRMTu7hXeGQMmZ/oSTB8LIYhjM5ztyymqAI=",
|
||||
"h1:Z7sTk8/B9XYFAEpTq1ib/WsB2QvpegGwef395tHCUSo=",
|
||||
"h1:eakzS6UsflV4gN/oQWdRPYvvmySKkhgKPyykCfPSVH0=",
|
||||
"h1:eesNMA1JUNBKCnuEvRLW2CT3tNrIejbXJG4A176nA9Q=",
|
||||
"h1:o8txgjqnDDODxQQ6ITwHsvPqHllCbMGnJkjCWNJK5ME=",
|
||||
"h1:rDTPIzab3tbGdtU3NmES08+tRlKG8jygNAFDFbheiuM=",
|
||||
"h1:sIoAt5vaxgFBrst1Esfm7ixQQ/T7/JNPZu0m4bw+Bc4=",
|
||||
"h1:sPCs3KnD/db5TMz2CNgiYix7sRS8+FP92T4ipSYT9vw=",
|
||||
"h1:uXPLsMQUYVJjRGUfmfDQXsKzYr1scvF6PYKj+3f0Nr4=",
|
||||
"h1:vQibC9lgBRQjRQ5KxFH61wd3/q2I40groLPjZ/jiikQ=",
|
||||
"zh:0826cad9b57368f06466ec3e1f766086f44a373288753e144f681ddc0e61ab49",
|
||||
"zh:15bf8d8abdcd202652466f3ef91110f143a7fad13c6173c7180e3dc0dbe9254b",
|
||||
"zh:1a60d1d2e55a17942902b1ccc91e8f6cb150c035843d3eefc074f1291a0e7d5f",
|
||||
"zh:23373a63fe06bbfa90c6c105c08429031719bedafa2458973cbdad32a384b5a7",
|
||||
"zh:256a97b79cedee2575002f77ac3bf18888ce0fc97170ae8a357db406bb0a88e3",
|
||||
"zh:3dd9f93c167cd59491ae77781032491d36a80843a1e44f9ee68b41b3f8e3ab8e",
|
||||
"zh:5182d2d61930dd83584d8feb56102614f80ddeec532b939b58085ad74b796a6e",
|
||||
"zh:5e3ca1888ba3b3bd8abd0f7dfcdd82fb5a8819affee899ed7fd891a33c17e0cb",
|
||||
"zh:7a3ce2119e39fb3a90a6a459a26863851309dbc92e0735336cbc98692539cb4d",
|
||||
"zh:c095dc680a646cc078c10bb0551e2ea8f08e7abdbcbc58cfd332c56b58788631",
|
||||
"zh:d2400e010e069c19916c99136e200cf873eba6db2b18d3eb9541b38eb1f565fa",
|
||||
"zh:dbc2f19f09aaefbd69334005bca2bcb0c9b690de1ed76f7ba97ce8e59a6e85df",
|
||||
"zh:e685225f432da3e23b8717848de8c0ad0e22363450b2ac92cd24a56739ab5e9b",
|
||||
"h1:57oeYqz+pYEPAw2GvHARjcER0QYU0teS6DJzS26kCAY=",
|
||||
"h1:6bnAgXSboppQbj79XmRtQLj/58d+b4EozydI8XrqwRE=",
|
||||
"h1:6eP/jGOz1ioH17d3pygKhvsRa4AEKvedKqd+wgyiHYk=",
|
||||
"h1:84gJm57BBSvtfwt7t9ZiU7EKKDQARjsztoDuqB5DfqU=",
|
||||
"h1:99tME9L/Ug/dXElX3Ac7Kx+KD7nhWEEwOTqaQubW18w=",
|
||||
"h1:TnMGVaPpIH5NZX324v+y6j1iW3ym0MqqNekxHlzXT3A=",
|
||||
"h1:Z1L5jI4GiJxAAsfNyZHU4MGTluWEjjKtlB8Xn6cA0Dg=",
|
||||
"h1:dB/WsY27MLla/cwjYggolJhVeHUeS59rlOObAWK2e24=",
|
||||
"h1:gpffNocq9fHK+uQehZQK6YmL1aHeCGrVM1p53dcFP/0=",
|
||||
"h1:jvfw1A2Uck4AjNmmWXwGH6P0sGQrnNY/KUVlqgyqYUM=",
|
||||
"h1:ojvrl9uI1EPtAwKRHim9eSc4Lc88thU/2BFGdJLEwrc=",
|
||||
"h1:rJZJK06NYCcSZhY5fwsRUSznSmAmRWVg4sO0auvbURo=",
|
||||
"h1:y2m6i2SnQ3ZxiJHlMWNLHJrEbNqQT6OpDGyhFP+Nmds=",
|
||||
"zh:06c09cc50d6c61fb5f1ca79e4b22d5f383c6a848f885ed542dadef44ab4bfd55",
|
||||
"zh:205bdfdac49d6b25ec2ca7ecffd39bf47126340e1943066bdbbd64d6d72577d2",
|
||||
"zh:341ed688772d8e2bd9a148c57d56d5dfbe3d625bc50296a505cbae3d29694562",
|
||||
"zh:3790619bae55558ab4d93f53686f7cf55f35138f800ea07b0b9c20cce75b9c0d",
|
||||
"zh:44284a5b67e34f1381bbc47b6e765cee581037e25cd3b459caec069493b63898",
|
||||
"zh:493a173f3d5245db82782e8c1aba63e34c6fcf6b952b9288926b519bfe9d9725",
|
||||
"zh:4eb7846381fee6eefdb7e0aaf80153ed71d35547752f96987218a9429645844f",
|
||||
"zh:850b1d1da770f48def40223d754ff65c31fabb1a7f334ed6ebb40eba09c41572",
|
||||
"zh:9a29a08a9fd40d7c1beefe8597617c2fc475d62ec6d5ca8fcc2cd1a056a67a2e",
|
||||
"zh:9e57472f0a56863204f5b2eef125d4fb1222eca793c6a5ac9f4df6c1776edc48",
|
||||
"zh:a9eff98931da596c2d335359841bfd668f4b6cde257e15546a558c7f5d72773b",
|
||||
"zh:c9293fb3390c79e4f5072e019c0a2c4c7408a55a55e78ae37f739a1da75886ef",
|
||||
"zh:c9e98abec7f7a1e2669dd9c065896217125d774640e4673c1303a942736b3eb4",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.52.9"
|
||||
version = "4.52.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:5ce7540c3c00ef4ab0c9d2c995c6a5b9c421f44b4a115d97a2c7af3b1c21cbb0
|
||||
image: prom/prometheus@sha256:fa3302279710be2f21a8301ec17a8b4f2301fd829bd113839b3b6b02b6f191a2
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.wrangler]]
|
||||
version = "4.151.0"
|
||||
version = "4.152.0"
|
||||
backend = "npm:wrangler"
|
||||
|
||||
[tools.wrangler.options]
|
||||
|
||||
+1
-1
@@ -28,4 +28,4 @@ run = "prettier --write ."
|
||||
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
||||
|
||||
[tools]
|
||||
wrangler = "4.151.0"
|
||||
wrangler = "4.152.0"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ test.describe('Shared Links', () => {
|
||||
|
||||
test('show error for invalid shared link', async ({ page }) => {
|
||||
await page.goto('/share/invalid');
|
||||
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor();
|
||||
await page.getByRole('heading', { name: 'Page not found' }).waitFor();
|
||||
});
|
||||
|
||||
test('auth on navigation from shared link to timeline', async ({ context, page }) => {
|
||||
|
||||
@@ -100,7 +100,7 @@ test.describe('OCR with stacked assets', () => {
|
||||
|
||||
const stackThumbnails = page.locator('#stack-slideshow [data-asset]');
|
||||
await expect(stackThumbnails).toHaveCount(2);
|
||||
await stackThumbnails.nth(1).click();
|
||||
await page.locator(`#stack-slideshow [data-asset="${secondAssetDto.id}"]`).click();
|
||||
|
||||
// refreshOcr() clears showOverlay when switching assets, so re-enable it
|
||||
await expect(ocrBoxes).toHaveCount(0);
|
||||
|
||||
@@ -75,8 +75,8 @@ test.describe('asset-viewer stack', () => {
|
||||
await page.goto(`/photos/${fixture.primaryAsset.id}`);
|
||||
await ensureDetailPanelVisible(page);
|
||||
|
||||
const stackAssets = page.locator('#stack-slideshow [data-asset]');
|
||||
await stackAssets.nth(1).click();
|
||||
const secondAsset = page.locator(`#stack-slideshow [data-asset="${secondAssetDto.id}"]`);
|
||||
await secondAsset.click();
|
||||
|
||||
const tags = page.getByTestId('detail-panel-tags').getByRole('link');
|
||||
await expect(tags.first()).toHaveText('test/2');
|
||||
|
||||
+3
-3
@@ -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.",
|
||||
@@ -2338,7 +2338,7 @@
|
||||
"user": "User",
|
||||
"user_has_been_deleted": "This user has been deleted.",
|
||||
"user_id": "User ID",
|
||||
"user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
|
||||
"user_liked": "{user} liked {type, select, image {this photo} video {this video} other {this}}",
|
||||
"user_pin_code_settings": "PIN Code",
|
||||
"user_pin_code_settings_description": "Manage your PIN code",
|
||||
"user_privacy": "User Privacy",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -14,7 +14,8 @@ dependencies = [
|
||||
"onnx>=1.22.0",
|
||||
"opencv-python-headless>=4.7.0.72,<5.0",
|
||||
"orjson>=3.9.5",
|
||||
"pillow>=12.2,<13",
|
||||
"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",
|
||||
@@ -64,7 +65,7 @@ compile-bytecode = true
|
||||
override-dependencies = ["opencv-python ; sys_platform == 'never'"] # only install the headless version
|
||||
|
||||
[tool.uv.sources]
|
||||
immich-model = { git = "https://github.com/immich-app/ml-models.git", rev = "8bf3fd1b44b0c8608f52fb6e50ddff52826576d8" }
|
||||
immich-model = { git = "https://github.com/immich-app/ml-models.git", rev = "a1eeff2c2118d76067efecf4cc4630998eac519a" }
|
||||
onnxruntime-gpu = { index = "onnxruntime-cuda-12" }
|
||||
openvino = { index = "openvino-nightly" }
|
||||
|
||||
|
||||
Generated
+772
-534
File diff suppressed because it is too large
Load Diff
@@ -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,123 +216,118 @@ 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.1"
|
||||
version = "1.13.2"
|
||||
backend = "aqua:opentofu/opentofu"
|
||||
specifiers = ["1.13.1"]
|
||||
specifiers = ["1.13.2"]
|
||||
|
||||
[tools.opentofu."platforms.linux-arm64"]
|
||||
checksum = "sha256:9c1ef375aa1852db0b2888aa921b640c71f8140d4682aa4fec99378a64fa7dc3"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708055"
|
||||
|
||||
[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"
|
||||
checksum = "sha256:88e2d9e3f94f97b76ab2faf55ade87578f56aed19c5e45fa139d70e994366d1f"
|
||||
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-x64"]
|
||||
checksum = "sha256:378ada19d4bc70c43732004e8159be771b23b9a5afdf059e5f8a2b3fa2c70a69"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708017"
|
||||
|
||||
[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"
|
||||
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.macos-arm64"]
|
||||
checksum = "sha256:be78f659f04ef06a9dbd9b3934d46af95d787a3aa38396d459dea395261816a9"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708059"
|
||||
checksum = "sha256:0d1cdc85f38992055b48e7fb161304a2ddfe2e5ee0ff68ffec9d0349c8bd1b59"
|
||||
checksum_source = "mise-versions"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742637"
|
||||
|
||||
[tools.opentofu."platforms.macos-x64"]
|
||||
checksum = "sha256:a73720443ba38712d7d96dc1e857add02c15a790919c653ad07492e9952f8c27"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708142"
|
||||
checksum = "sha256:dc6e4eb1d3ed6550c2ef052596f933dbcd8c806957665ea83ae7537147cb3074"
|
||||
checksum_source = "mise-versions"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742982"
|
||||
|
||||
[tools.opentofu."platforms.windows-x64"]
|
||||
checksum = "sha256:6825ae311e9a15747ce2ede7b7e97c8bb744a4d271fa8113259eeeee3d09438c"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_windows_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708102"
|
||||
checksum = "sha256:e74fc2bda0c81ce696b6b7016a4d5c1f557638161239e4101e9ee7d8e0faba59"
|
||||
checksum_source = "mise-versions"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_windows_amd64.tar.gz"
|
||||
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"
|
||||
|
||||
@@ -16,10 +16,10 @@ config_roots = [
|
||||
|
||||
[tools]
|
||||
node = "24.22.1"
|
||||
pnpm = "12.14.0"
|
||||
terragrunt = "1.1.1"
|
||||
opentofu = "1.13.1"
|
||||
"npm:@openapitools/openapi-generator-cli" = "2.41.0"
|
||||
pnpm = "12.15.0"
|
||||
terragrunt = "1.2.0"
|
||||
opentofu = "1.13.2"
|
||||
"npm:@openapitools/openapi-generator-cli" = "2.42.0"
|
||||
"npm:oazapfts" = "7.5.0"
|
||||
"github:extism/cli" = "1.6.3"
|
||||
"github:webassembly/binaryen" = "version_124"
|
||||
|
||||
@@ -60,7 +60,6 @@ class AssetService {
|
||||
}
|
||||
|
||||
final stack = await _remoteRepository.getStackChildren(asset);
|
||||
// Include the primary asset in the stack as the first item
|
||||
return [asset, ...stack];
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,23 @@ class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRep
|
||||
return _assetSelectable(id).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<Set<String>> watchDeletedAssetIds(Iterable<String> ids, AssetVisibility visibility) {
|
||||
final idList = ids.toList(growable: false);
|
||||
if (idList.isEmpty) {
|
||||
return Stream.value(const <String>{});
|
||||
}
|
||||
|
||||
final query = _db.remoteAssetEntity.selectOnly()
|
||||
..addColumns([_db.remoteAssetEntity.id])
|
||||
..where(
|
||||
_db.remoteAssetEntity.id.isIn(idList) &
|
||||
(_db.remoteAssetEntity.deletedAt.isNotNull() |
|
||||
_db.remoteAssetEntity.visibility.equalsValue(visibility).not()),
|
||||
);
|
||||
|
||||
return query.map((row) => row.read(_db.remoteAssetEntity.id)!).watch().map((rows) => rows.toSet());
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> get(String id) {
|
||||
return _assetSelectable(id).getSingleOrNull();
|
||||
}
|
||||
@@ -59,7 +76,7 @@ class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRep
|
||||
|
||||
final query = _db.remoteAssetEntity.select()
|
||||
..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not())
|
||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
||||
..orderBy([(row) => OrderingTerm.asc(row.createdAt)]);
|
||||
|
||||
return query.map((row) => row.toDto()).get();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ import 'dart:async';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/search.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
|
||||
|
||||
final searchPreFilterProvider = NotifierProvider<SearchFilterProvider, SearchFilter?>(SearchFilterProvider.new);
|
||||
@@ -32,16 +34,21 @@ class SearchState {
|
||||
}
|
||||
|
||||
final paginatedSearchProvider = StateNotifierProvider<PaginatedSearchNotifier, SearchState>(
|
||||
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)),
|
||||
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider), ref.watch(driftProvider).remoteAssetRepository),
|
||||
);
|
||||
|
||||
class PaginatedSearchNotifier extends StateNotifier<SearchState> {
|
||||
final SearchService _searchService;
|
||||
final RemoteAssetRepository _remoteAssetRepository;
|
||||
final _assetCountController = StreamController<int>.broadcast();
|
||||
|
||||
PaginatedSearchNotifier(this._searchService) : super(const SearchState());
|
||||
StreamSubscription<Set<String>>? _deletedIdsSubscription;
|
||||
List<BaseAsset> _results = const [];
|
||||
int? _nextPage = 1;
|
||||
|
||||
Stream<int> get assetCount => _assetCountController.stream;
|
||||
PaginatedSearchNotifier(this._searchService, this._remoteAssetRepository) : super(const SearchState());
|
||||
|
||||
Stream<int> get assetCount => _assetCountController.stream.distinct();
|
||||
|
||||
Future<void> search(SearchFilter filter) async {
|
||||
if (state.nextPage == null || state.isLoading) {
|
||||
@@ -52,24 +59,47 @@ class PaginatedSearchNotifier extends StateNotifier<SearchState> {
|
||||
|
||||
final result = await _searchService.search(filter, state.nextPage!);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
state = SearchState(assets: state.assets, nextPage: state.nextPage);
|
||||
return;
|
||||
}
|
||||
|
||||
final assets = [...state.assets, ...result.assets];
|
||||
state = SearchState(assets: assets, nextPage: result.nextPage);
|
||||
|
||||
_assetCountController.add(assets.length);
|
||||
_results = [..._results, ...result.assets];
|
||||
_nextPage = result.nextPage;
|
||||
_watchDeletedIds(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = const SearchState();
|
||||
_assetCountController.add(0);
|
||||
unawaited(_deletedIdsSubscription?.cancel());
|
||||
_deletedIdsSubscription = null;
|
||||
_results = const [];
|
||||
_nextPage = 1;
|
||||
_emit(const {});
|
||||
}
|
||||
|
||||
void _watchDeletedIds(AssetVisibility visibility) {
|
||||
unawaited(_deletedIdsSubscription?.cancel());
|
||||
|
||||
final ids = _results.whereType<RemoteAsset>().map((asset) => asset.id);
|
||||
_deletedIdsSubscription = _remoteAssetRepository.watchDeletedAssetIds(ids, visibility).listen(_emit);
|
||||
}
|
||||
|
||||
void _emit(Set<String> deletedIds) {
|
||||
final visible = deletedIds.isEmpty
|
||||
? _results
|
||||
: _results.where((asset) => asset is! RemoteAsset || !deletedIds.contains(asset.id)).toList(growable: false);
|
||||
|
||||
state = SearchState(assets: visible, nextPage: _nextPage);
|
||||
_assetCountController.add(visible.length);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_deletedIdsSubscription?.cancel());
|
||||
unawaited(_assetCountController.close());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 3.3.5-local.9+3030502
|
||||
version: 3.3.5-local.12+3030504
|
||||
|
||||
environment:
|
||||
sdk: '>=3.12.0 <4.0.0'
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:async/async.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
|
||||
import '../repository_context.dart';
|
||||
@@ -16,6 +18,37 @@ void main() {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('getStackChildren', () {
|
||||
test('returns stack children from oldest to newest', () async {
|
||||
final user = await ctx.newUser();
|
||||
const stackId = 'stack';
|
||||
final primary = await ctx.newRemoteAsset(
|
||||
id: 'primary',
|
||||
ownerId: user.id,
|
||||
stackId: stackId,
|
||||
createdAt: DateTime(2026, 1, 2),
|
||||
);
|
||||
final beforePrimary = await ctx.newRemoteAsset(
|
||||
id: 'before-primary',
|
||||
ownerId: user.id,
|
||||
stackId: stackId,
|
||||
createdAt: DateTime(2026, 1, 1),
|
||||
);
|
||||
final afterPrimary = await ctx.newRemoteAsset(
|
||||
id: 'after-primary',
|
||||
ownerId: user.id,
|
||||
stackId: stackId,
|
||||
createdAt: DateTime(2026, 1, 3),
|
||||
);
|
||||
await ctx.newRemoteAsset(ownerId: user.id, stackId: 'other-stack');
|
||||
|
||||
final primaryAsset = await sut.get(primary.id);
|
||||
final stack = await sut.getStackChildren(primaryAsset!);
|
||||
|
||||
expect(stack.map((asset) => asset.id), [beforePrimary.id, afterPrimary.id]);
|
||||
});
|
||||
});
|
||||
|
||||
group('getByChecksum', () {
|
||||
late String userId;
|
||||
|
||||
@@ -64,4 +97,53 @@ void main() {
|
||||
expect(result[0].id, remote.id);
|
||||
});
|
||||
});
|
||||
|
||||
group('watchDeletedAssetIds', () {
|
||||
late String userId;
|
||||
|
||||
setUp(() async {
|
||||
final user = await ctx.newUser();
|
||||
userId = user.id;
|
||||
await ctx.newAuthUser(id: userId);
|
||||
});
|
||||
|
||||
test('hides trashed and archived assets but keeps matching and unsynced ones', () async {
|
||||
final timeline = await ctx.newRemoteAsset(ownerId: userId);
|
||||
final trashed = await ctx.newRemoteAsset(ownerId: userId, deletedAt: DateTime(2020));
|
||||
final archived = await ctx.newRemoteAsset(ownerId: userId, visibility: AssetVisibility.archive);
|
||||
|
||||
final result = await sut.watchDeletedAssetIds([
|
||||
timeline.id,
|
||||
trashed.id,
|
||||
archived.id,
|
||||
'never-synced',
|
||||
], AssetVisibility.timeline).first;
|
||||
|
||||
expect(result, {trashed.id, archived.id});
|
||||
});
|
||||
|
||||
test('hides assets not matching the requested visibility for archive searches', () async {
|
||||
final timeline = await ctx.newRemoteAsset(ownerId: userId);
|
||||
final archived = await ctx.newRemoteAsset(ownerId: userId, visibility: AssetVisibility.archive);
|
||||
|
||||
final result = await sut.watchDeletedAssetIds([timeline.id, archived.id], AssetVisibility.archive).first;
|
||||
|
||||
expect(result, {timeline.id});
|
||||
});
|
||||
|
||||
test('emits when an asset is trashed and again when it is restored', () async {
|
||||
final asset = await ctx.newRemoteAsset(ownerId: userId);
|
||||
final queue = StreamQueue(sut.watchDeletedAssetIds([asset.id], AssetVisibility.timeline));
|
||||
|
||||
expect(await queue.next, isEmpty);
|
||||
|
||||
await sut.trash([asset.id]);
|
||||
expect(await queue.next, {asset.id});
|
||||
|
||||
await sut.restoreTrash([asset.id]);
|
||||
expect(await queue.next, isEmpty);
|
||||
|
||||
await queue.cancel();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -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,5 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use jxl::api::{
|
||||
AlphaMode, Endianness, Event, ExtraChannel, JxlColorEncoding, JxlColorProfile, JxlColorType,
|
||||
@@ -7,6 +7,7 @@ use jxl::api::{
|
||||
JxlParallelRunnerFun, JxlPixelFormat, JxlPrimaries, JxlTransferFunction, JxlWhitePoint,
|
||||
};
|
||||
use moxcms::{Layout, MatrixCoefficients, Transform16BitExecutor};
|
||||
use quick_cache::sync::Cache;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::color::{self, CicpColorPrimaries, CicpProfile, Target, TransferCharacteristics};
|
||||
@@ -98,7 +99,7 @@ impl<'a> Jxl<'a> {
|
||||
transform: None,
|
||||
});
|
||||
}
|
||||
let (transform, layout) = color::to_srgb(&profile.try_as_icc()?)?;
|
||||
let (transform, layout) = srgb_transform(profile)?;
|
||||
decoder
|
||||
.set_pixel_format(JxlPixelFormat {
|
||||
color_type: match layout {
|
||||
@@ -233,3 +234,23 @@ fn cicp(profile: &JxlColorProfile) -> Option<CicpProfile> {
|
||||
full_range: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Reuse the transform to sRGB for a given source profile.
|
||||
type Transforms<K> = Cache<K, (Arc<Transform16BitExecutor>, Layout)>;
|
||||
|
||||
/// [`color::to_srgb`] for `profile`, built once per distinct profile.
|
||||
fn srgb_transform(profile: &JxlColorProfile) -> Option<(Arc<Transform16BitExecutor>, Layout)> {
|
||||
const TRANSFORMS: usize = 8;
|
||||
static ENCODINGS: LazyLock<Transforms<String>> = LazyLock::new(|| Cache::new(TRANSFORMS));
|
||||
static ICCS: LazyLock<Transforms<Vec<u8>>> = LazyLock::new(|| Cache::new(TRANSFORMS));
|
||||
match profile {
|
||||
JxlColorProfile::Simple(encoding) => ENCODINGS
|
||||
.get_or_insert_with(&encoding.get_color_encoding_description(), || {
|
||||
color::to_srgb(&profile.try_as_icc().ok_or(())?).ok_or(())
|
||||
}),
|
||||
JxlColorProfile::Icc(icc) => {
|
||||
ICCS.get_or_insert_with(icc.as_slice(), || color::to_srgb(icc).ok_or(()))
|
||||
}
|
||||
}
|
||||
.ok()
|
||||
}
|
||||
|
||||
@@ -83,6 +83,36 @@ fn converts_lossy_icc_to_srgb() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_decodes_keep_profile_colors_when_transforms_are_reused() {
|
||||
let cases: [(&[u8], [u8; 8]); 3] = [
|
||||
(
|
||||
include_bytes!("fixtures/p3.jxl"),
|
||||
[255, 0, 0, 255, 255, 255, 255, 255],
|
||||
),
|
||||
(
|
||||
include_bytes!("fixtures/gray_icc.jxl"),
|
||||
[81, 81, 81, 255, 203, 203, 203, 255],
|
||||
),
|
||||
(
|
||||
include_bytes!("fixtures/srgb_alpha.jxl"),
|
||||
[128, 0, 0, 128, 0, 0, 255, 255],
|
||||
),
|
||||
];
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..4 {
|
||||
scope.spawn(|| {
|
||||
for _ in 0..8 {
|
||||
for (data, expected) in cases {
|
||||
let rgba = decode_srgb(data).unwrap();
|
||||
assert_eq!(rgba.pixels().as_flattened(), &expected);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tone_maps_hdr() {
|
||||
let rgba = decode_srgb(include_bytes!("fixtures/pq_grays.jxl")).unwrap();
|
||||
|
||||
@@ -1237,6 +1237,7 @@
|
||||
"state": "Stable"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "notification.create",
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
@@ -1306,6 +1307,7 @@
|
||||
"state": "Stable"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "notification.create",
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
@@ -1366,6 +1368,7 @@
|
||||
"state": "Stable"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "notification.create",
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
@@ -23130,6 +23133,7 @@
|
||||
"description": "Image format",
|
||||
"enum": [
|
||||
"jpeg",
|
||||
"jxl",
|
||||
"webp"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -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
-1
@@ -8,7 +8,7 @@
|
||||
"format": "prettier --cache --check i18n/",
|
||||
"format:fix": "prettier --cache --write --list-different i18n"
|
||||
},
|
||||
"packageManager": "pnpm@12.14.0",
|
||||
"packageManager": "pnpm@12.15.0",
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.22.1-trixie-slim@sha256:fdc9220097e0ae3bd947c44b10cd3af5867a67a6d3a23b30b30b70929219034e AS core
|
||||
FROM node:24.22.1-trixie-slim@sha256:43b9ba1142b8dcf33fa8f922ca1be85714adc444e0586c7cf7726daaeabc1bea AS core
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY package* pnpm* .pnpmfile.cjs ./
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "^5.0.0",
|
||||
"chokidar": "^6.0.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fastq": "^1.17.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.22.1-trixie-slim@sha256:fdc9220097e0ae3bd947c44b10cd3af5867a67a6d3a23b30b30b70929219034e
|
||||
FROM node:24.22.1-trixie-slim@sha256:43b9ba1142b8dcf33fa8f922ca1be85714adc444e0586c7cf7726daaeabc1bea
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"oidc-provider": "^9.0.0",
|
||||
"tsx": "^4.20.6"
|
||||
},
|
||||
"packageManager": "pnpm@12.14.0"
|
||||
"packageManager": "pnpm@12.15.0"
|
||||
}
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"packageManager": "pnpm@12.14.0",
|
||||
"packageManager": "pnpm@12.15.0",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1",
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@types/node": "^24.19.2",
|
||||
"esbuild": "^0.28.0",
|
||||
"tsc-alias": "1.9.5",
|
||||
"tsc-alias": "1.9.9",
|
||||
"typescript": "^7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -33,5 +33,5 @@
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^5.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@12.14.0"
|
||||
"packageManager": "pnpm@12.15.0"
|
||||
}
|
||||
|
||||
@@ -8214,6 +8214,7 @@ export enum Colorspace {
|
||||
}
|
||||
export enum ImageFormat {
|
||||
Jpeg = "jpeg",
|
||||
Jxl = "jxl",
|
||||
Webp = "webp"
|
||||
}
|
||||
export enum LogLevel {
|
||||
|
||||
Generated
+444
-365
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -32,7 +32,7 @@ overrides:
|
||||
canvas: 3.2.3
|
||||
sharp: ^0.35.3
|
||||
# 1.9.3-1.9.5 are confirmed broken
|
||||
tsc-alias: 1.9.2
|
||||
tsc-alias: 1.9.9
|
||||
packageExtensions:
|
||||
'@koddsson/eslint-plugin-tscompat':
|
||||
dependencies:
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<a href="README_zh_TW.md">正體中文</a>
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
+82
-45
@@ -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>
|
||||
<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/>
|
||||
</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>
|
||||
@@ -32,67 +33,103 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<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
|
||||
|
||||

|
||||
|
||||
## 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>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<a href="README_zh_TW.md">正體中文</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<a href="README_zh_TW.md">正體中文</a>
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<p align="center">
|
||||
<br/>
|
||||
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="Licencja: AGPLv3"></a>
|
||||
<a href="https://discord.immich.app">
|
||||
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<br/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="/design/immich-logo-stacked-light.svg" width="300" title="Login With Custom URL">
|
||||
</p>
|
||||
<h3 align="center">Hostowane lokalnie, wydajne rozwiązanie do zarządzania zdjęciami i filmami</h3>
|
||||
<br/>
|
||||
<a href="https://immich.app">
|
||||
<img src="/design/immich-screenshots.png" title="Główny zrzut ekranu">
|
||||
</a>
|
||||
<br/>
|
||||
|
||||
<p align="center">
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_ko_KR.md">한국어</a>
|
||||
<a href="README_de_DE.md">Deutsch</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">简体中文</a>
|
||||
<a href="README_zh_TW.md">正體中文</a>
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
|
||||
> [!WARNING]
|
||||
> ⚠️ Zawsze stosuj strategię kopii zapasowych [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) dla ochrony swoich multimediów!
|
||||
>
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
> Dokumentację oraz instrukcję instalacji znajdziesz na https://immich.app/.
|
||||
|
||||
## Linki
|
||||
|
||||
- [Dokumentacja](https://docs.immich.app/)
|
||||
- [O projekcie](https://docs.immich.app/overview/introduction)
|
||||
- [Instalacja](https://docs.immich.app/install/requirements)
|
||||
- [Plan rozwoju](https://immich.app/roadmap)
|
||||
- [Demo](#demo)
|
||||
- [Funkcje](#funkcje)
|
||||
- [Tłumaczenia](https://docs.immich.app/developer/translations)
|
||||
- [Współtworzenie projektu](https://docs.immich.app/overview/support-the-project)
|
||||
|
||||
## Demo
|
||||
|
||||
Wersję demo znajdziesz [tutaj](https://demo.immich.app). W aplikacji mobilnej w polu z adresem serwera (`Punkt końcowy serwera`) użyj `https://demo.immich.app`.
|
||||
|
||||
### Dane logowania
|
||||
|
||||
| E-mail | Hasło |
|
||||
| --------------- | ----- |
|
||||
| demo@immich.app | demo |
|
||||
|
||||
## Funkcje
|
||||
|
||||
| Funkcje | Aplikacja mobilna | Web |
|
||||
| :--------------------------------------------------------- | ----------------- | --- |
|
||||
| Przesyłanie i przeglądanie zdjęć i filmów | Tak | Tak |
|
||||
| Automatyczna kopia zapasowa po uruchomieniu aplikacji | Tak | N/D |
|
||||
| Zapobieganie duplikowaniu plików | Tak | Tak |
|
||||
| Wybór albumów do kopii zapasowej | Tak | N/D |
|
||||
| Pobieranie zdjęć i filmów na urządzenie | Tak | Tak |
|
||||
| Obsługa wielu użytkowników | Tak | Tak |
|
||||
| Albumy i Albumy udostępnione | Tak | Tak |
|
||||
| Pasek przewijania z możliwością przeciągania | Tak | Tak |
|
||||
| Obsługa formatów RAW | Tak | Tak |
|
||||
| Podgląd metadanych (EXIF, mapa) | Tak | Tak |
|
||||
| Wyszukiwanie według metadanych, obiektów, twarzy i CLIP | Tak | Tak |
|
||||
| Funkcje administracyjne (zarządzanie użytkownikami) | Nie | Tak |
|
||||
| Kopia zapasowa w tle | Tak | N/D |
|
||||
| Wirtualne przewijanie | Tak | Tak |
|
||||
| Obsługa OAuth | Tak | Tak |
|
||||
| Klucze API | N/D | Tak |
|
||||
| Kopia zapasowa i odtwarzanie Live Photo / Motion Photo | Tak | Tak |
|
||||
| Wyświetlanie obrazów 360° | Nie | Tak |
|
||||
| Własna struktura przechowywania plików | Tak | Tak |
|
||||
| Udostępnianie publiczne | Tak | Tak |
|
||||
| Archiwum i Ulubione | Tak | Tak |
|
||||
| Mapa globalna | Tak | Tak |
|
||||
| Udostępnianie partnerowi | Tak | Tak |
|
||||
| Rozpoznawanie i grupowanie twarzy | Tak | Tak |
|
||||
| Wspomnienia (x lat temu) | Tak | Tak |
|
||||
| Obsługa trybu offline | Tak | Nie |
|
||||
| Galeria tylko do odczytu | Tak | Tak |
|
||||
| Grupowanie zdjęć w stosy | Tak | Tak |
|
||||
| Tagi | Nie | Tak |
|
||||
| Widok folderów | Tak | Tak |
|
||||
|
||||
## Tłumaczenia
|
||||
|
||||
Więcej o tłumaczeniach przeczytasz [tutaj](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="Status tłumaczeń" />
|
||||
</a>
|
||||
|
||||
## Aktywność w repozytorium
|
||||
|
||||

|
||||
|
||||
## Współtwórcy
|
||||
|
||||
<a href="https://github.com/immich-app/immich/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||
</a>
|
||||
@@ -34,6 +34,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</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>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<a href="README_zh_TW.md">正體中文</a>
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<br>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<a href="README_zh_TW.md">正體中文</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_bg_BG.md">Български</a>
|
||||
<a href="README_pl_PL.md">Polski</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ FROM plugin-sdk AS plugins
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.10.15@sha256:0593528c637b0dae8a0acca66b5d49266cf262a811f06530d7273d3cc002f3a0 /usr/local/bin/mise /usr/local/bin/mise
|
||||
COPY --from=ghcr.io/jdx/mise:2026.10.20@sha256:a3023a390ce9d79343741039d35a36aa41e4fe8198464bf8cc9ffd23ae081a24 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
COPY ./mise.toml ./mise.toml
|
||||
COPY ./mise.lock ./mise.lock
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202610211607@sha256:f99c605b44b291834cb680b80fd606f2b7c4bee5d8f9591bfdac51fdbd68c21d AS dev
|
||||
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.10.15@sha256:0593528c637b0dae8a0acca66b5d49266cf262a811f06530d7273d3cc002f3a0 /usr/local/bin/mise /usr/local/bin/mise
|
||||
COPY --from=ghcr.io/jdx/mise:2026.10.20@sha256:a3023a390ce9d79343741039d35a36aa41e4fe8198464bf8cc9ffd23ae081a24 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \
|
||||
echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \
|
||||
|
||||
+3
-2
@@ -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": [
|
||||
|
||||
+5
-5
@@ -51,25 +51,25 @@
|
||||
"@nestjs/swagger": "^12.0.0",
|
||||
"@nestjs/websockets": "^12.0.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "^2.0.0",
|
||||
"@opentelemetry/context-async-hooks": "^3.0.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.223.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.223.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.71.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.70.0",
|
||||
"@opentelemetry/instrumentation-pg": "^0.76.0",
|
||||
"@opentelemetry/resources": "^2.0.1",
|
||||
"@opentelemetry/sdk-metrics": "^2.0.1",
|
||||
"@opentelemetry/resources": "^3.0.0",
|
||||
"@opentelemetry/sdk-metrics": "^3.0.0",
|
||||
"@opentelemetry/sdk-node": "^0.223.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"@simplewebauthn/server": "14.0.3",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"aaguids": "^0.0.71",
|
||||
"aaguids": "^0.0.72",
|
||||
"archiver": "^8.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"bullmq": "^6.0.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"chokidar": "^6.0.0",
|
||||
"compression": "^1.8.0",
|
||||
"cookie": "^2.0.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
import request from 'supertest';
|
||||
import { NotificationAdminController } from 'src/controllers/notification-admin.controller.js';
|
||||
import { Permission } from 'src/enum.js';
|
||||
import { NotificationAdminService } from 'src/services/notification-admin.service.js';
|
||||
import { factory } from 'test/small.factory.js';
|
||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils.js';
|
||||
@@ -27,5 +28,39 @@ describe(NotificationAdminController.name, () => {
|
||||
.send({ title: 'Test', userId: factory.uuid(), readAt: null });
|
||||
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ readAt: null }));
|
||||
});
|
||||
|
||||
it('should require notification create permission', async () => {
|
||||
await request(ctx.getHttpServer()).post(`/admin/notifications`).send({ title: 'Test', userId: factory.uuid() });
|
||||
|
||||
expect(ctx.authenticate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({ permission: Permission.NotificationCreate, adminRoute: true }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /admin/notifications/test-email', () => {
|
||||
it('should require notification create permission', async () => {
|
||||
await request(ctx.getHttpServer()).post(`/admin/notifications/test-email`).send({});
|
||||
|
||||
expect(ctx.authenticate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({ permission: Permission.NotificationCreate, adminRoute: true }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /admin/notifications/templates/:name', () => {
|
||||
it('should require notification create permission', async () => {
|
||||
await request(ctx.getHttpServer()).post(`/admin/notifications/templates/welcome`).send({ template: '' });
|
||||
|
||||
expect(ctx.authenticate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({ permission: Permission.NotificationCreate, adminRoute: true }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
TemplateResponseDto,
|
||||
TestEmailResponseDto,
|
||||
} from 'src/dtos/notification.dto.js';
|
||||
import { ApiTag } from 'src/enum.js';
|
||||
import { ApiTag, Permission } from 'src/enum.js';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard.js';
|
||||
import { EmailTemplate } from 'src/repositories/email.repository.js';
|
||||
import { NotificationAdminService } from 'src/services/notification-admin.service.js';
|
||||
@@ -21,7 +21,7 @@ export class NotificationAdminController {
|
||||
constructor(private service: NotificationAdminService) {}
|
||||
|
||||
@Post()
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.NotificationCreate, admin: true })
|
||||
@Endpoint({
|
||||
summary: 'Create a notification',
|
||||
description: 'Create a new notification for a specific user.',
|
||||
@@ -32,7 +32,7 @@ export class NotificationAdminController {
|
||||
}
|
||||
|
||||
@Post('test-email')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.NotificationCreate, admin: true })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Endpoint({
|
||||
summary: 'Send test email',
|
||||
@@ -44,7 +44,7 @@ export class NotificationAdminController {
|
||||
}
|
||||
|
||||
@Post('templates/:name')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.NotificationCreate, admin: true })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Endpoint({
|
||||
summary: 'Render email template',
|
||||
|
||||
@@ -610,6 +610,7 @@ export const ColorspaceSchema = z.enum(Colorspace).describe('Colorspace').meta({
|
||||
|
||||
export enum ImageFormat {
|
||||
Jpeg = 'jpeg',
|
||||
Jxl = 'jxl',
|
||||
Webp = 'webp',
|
||||
}
|
||||
|
||||
|
||||
@@ -818,6 +818,7 @@ export class AssetRepository {
|
||||
})
|
||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions, auth: AuthDto) {
|
||||
const order = options.order ?? 'desc';
|
||||
const bucket = timeBucket.replace(/^[+-]/, '');
|
||||
const query = this.db
|
||||
.with('cte', (qb) =>
|
||||
qb
|
||||
@@ -869,7 +870,14 @@ export class AssetRepository {
|
||||
|
||||
return withBoundingBox(withBoundingCircle, bbox);
|
||||
})
|
||||
.where(truncatedDate(options.orderBy), '=', timeBucket.replace(/^[+-]/, ''))
|
||||
.$if(options.orderBy === AssetOrderBy.CreatedAt, (qb) =>
|
||||
qb
|
||||
.where('asset.createdAt', '>=', sql<Date>`${bucket}::timestamp AT TIME ZONE 'UTC'`)
|
||||
.where('asset.createdAt', '<', sql<Date>`(${bucket}::timestamp + interval '1 month') AT TIME ZONE 'UTC'`),
|
||||
)
|
||||
.$if(options.orderBy !== AssetOrderBy.CreatedAt, (qb) =>
|
||||
qb.where(truncatedDate(options.orderBy), '=', bucket),
|
||||
)
|
||||
.$if(!!options.albumId, (qb) =>
|
||||
qb.where((eb) =>
|
||||
eb.exists(
|
||||
|
||||
@@ -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'`] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -308,7 +308,7 @@ describe(PasskeyService.name, () => {
|
||||
expect(mocks.passkey.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should verify against the android app origins when the origin header is missing', async () => {
|
||||
it('should verify against the mobile app origins when the origin header is missing', async () => {
|
||||
const auth = AuthFactory.from().session().build();
|
||||
const passkey = PasskeyFactory.create({ userId: auth.user.id });
|
||||
|
||||
@@ -323,6 +323,7 @@ describe(PasskeyService.name, () => {
|
||||
expect(mocks.webAuthn.verifyRegistrationResponse).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
expectedOrigin: [
|
||||
'https://my.immich.app',
|
||||
'android:apk-key-hash:hsXEVd-vSYWSOo81rbMdDJ4LlX1_lMLSr2okOKqWACA',
|
||||
'android:apk-key-hash:WiLBg0dUBfVJxOufsmwuk6PvnFdmFQp684yNP-UVzNY',
|
||||
],
|
||||
|
||||
@@ -32,6 +32,9 @@ const CHALLENGE_TTL = Duration.fromObject({ minutes: 5 });
|
||||
|
||||
const MOBILE_RP_ID = 'my.immich.app';
|
||||
|
||||
// iOS apps send https:// + the rp id as their origin
|
||||
const IOS_ORIGIN = `https://${MOBILE_RP_ID}`;
|
||||
|
||||
// keep in sync with https://my.immich.app/.well-known/assetlinks.json
|
||||
const ANDROID_CERTIFICATE_FINGERPRINTS = [
|
||||
'86:C5:C4:55:DF:AF:49:85:92:3A:8F:35:AD:B3:1D:0C:9E:0B:95:7D:7F:94:C2:D2:AF:6A:24:38:AA:96:00:20',
|
||||
@@ -273,7 +276,7 @@ export class PasskeyService extends BaseService {
|
||||
const origin = headers.origin;
|
||||
// no origin implies a mobile app
|
||||
if (!origin) {
|
||||
return { origin: ANDROID_ORIGINS, rpId: MOBILE_RP_ID };
|
||||
return { origin: [IOS_ORIGIN, ...ANDROID_ORIGINS], rpId: MOBILE_RP_ID };
|
||||
}
|
||||
|
||||
let hostname: string;
|
||||
|
||||
@@ -211,6 +211,36 @@ describe(SmartInfoService.name, () => {
|
||||
expect(mocks.search.upsert).toHaveBeenCalledWith(asset.id, '[0.01, 0.02, 0.03]');
|
||||
});
|
||||
|
||||
it('should use the edited preview when both preview variants exist', async () => {
|
||||
const asset = AssetFactory.from()
|
||||
.file({ type: AssetFileType.Preview, isEdited: false, path: 'original-preview' })
|
||||
.file({ type: AssetFileType.Preview, isEdited: true, path: 'edited-preview' })
|
||||
.build();
|
||||
mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue(asset);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Success);
|
||||
expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith(
|
||||
'edited-preview',
|
||||
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should select the edited preview regardless of file order', async () => {
|
||||
const asset = AssetFactory.from()
|
||||
.file({ type: AssetFileType.Preview, isEdited: true, path: 'edited-preview' })
|
||||
.file({ type: AssetFileType.Preview, isEdited: false, path: 'original-preview' })
|
||||
.build();
|
||||
mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue(asset);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Success);
|
||||
expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith(
|
||||
'edited-preview',
|
||||
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip invisible assets', async () => {
|
||||
const asset = AssetFactory.from({ visibility: AssetVisibility.Hidden })
|
||||
.file({ type: AssetFileType.Preview })
|
||||
|
||||
@@ -92,7 +92,8 @@ export class SmartInfoService extends BaseService {
|
||||
}
|
||||
|
||||
const asset = await this.assetJobRepository.getForClipEncoding(id);
|
||||
if (!asset || asset.files.length !== 1) {
|
||||
const previewFile = asset?.files.find((file) => file.isEdited) ?? asset?.files[0];
|
||||
if (!asset || !previewFile) {
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@ export class SmartInfoService extends BaseService {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
const embedding = await this.machineLearningRepository.encodeImage(asset.files[0].path, machineLearning.clip);
|
||||
const embedding = await this.machineLearningRepository.encodeImage(previewFile.path, machineLearning.clip);
|
||||
|
||||
if (this.databaseRepository.isBusy(DatabaseLock.CLIPDimSize)) {
|
||||
this.logger.verbose(`Waiting for CLIP dimension size to be updated`);
|
||||
|
||||
+4
-11
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Kysely } from 'kysely';
|
||||
import { AssetVisibility, SharedLinkType } from 'src/enum.js';
|
||||
import { AssetOrderBy, AssetVisibility, SharedLinkType } from 'src/enum.js';
|
||||
import { AccessRepository } from 'src/repositories/access.repository.js';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository.js';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository.js';
|
||||
@@ -162,6 +162,34 @@ describe(TimelineService.name, () => {
|
||||
const rawResponse = await sut.getTimeBucket(factory.auth(), { timeBucket: '012345-01-01' });
|
||||
const response = JSON.parse(rawResponse);
|
||||
expect(response).toEqual(expect.objectContaining({ id: [] }));
|
||||
|
||||
const createdAtResponse = await sut.getTimeBucket(factory.auth(), {
|
||||
timeBucket: '012345-01-01',
|
||||
orderBy: AssetOrderBy.CreatedAt,
|
||||
});
|
||||
expect(JSON.parse(createdAtResponse)).toEqual(expect.objectContaining({ id: [] }));
|
||||
});
|
||||
|
||||
it('should bucket by upload month when ordering by createdAt', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user: { id: user.id } });
|
||||
const dates = [
|
||||
'1970-01-31T23:59:59.999Z',
|
||||
'1970-02-01T00:00:00.000Z',
|
||||
'1970-02-28T23:59:59.999Z',
|
||||
'1970-03-01T00:00:00.000Z',
|
||||
];
|
||||
const ids: string[] = [];
|
||||
for (const createdAt of dates) {
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, createdAt, localDateTime: new Date('2000-01-01') });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
ids.push(asset.id);
|
||||
}
|
||||
|
||||
const rawResponse = await sut.getTimeBucket(auth, { timeBucket: '1970-02-01', orderBy: AssetOrderBy.CreatedAt });
|
||||
const response = JSON.parse(rawResponse);
|
||||
expect(response.id).toEqual([ids[2], ids[1]]);
|
||||
});
|
||||
|
||||
it('should return time bucket in trash', async () => {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"importOrder": ["<THIRD_PARTY_MODULES>", "<BUILTIN_MODULES>", "^\\$(.*)$", "^@test-data(.*)$", "^[./]"],
|
||||
"importOrder": ["<THIRD_PARTY_MODULES>", "<BUILTIN_MODULES>", "^\\$(.*)$", "^#test-data(.*)$", "^[./]"],
|
||||
"importOrderSeparation": false,
|
||||
"jsonRecursiveSort": true,
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
|
||||
|
||||
@@ -53,7 +53,7 @@ export default defineConfig(
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
// ignores: ['**/service-worker/**'],
|
||||
ignores: ['**/service-worker/**'],
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
@@ -74,6 +74,7 @@ export default defineConfig(
|
||||
'tailwind.config.js',
|
||||
'coverage',
|
||||
'vite.config.ts',
|
||||
'**/service-worker/**',
|
||||
],
|
||||
},
|
||||
typescriptEslint.configs.recommended,
|
||||
@@ -164,6 +165,7 @@ export default defineConfig(
|
||||
'@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }],
|
||||
'svelte/button-has-type': 'error',
|
||||
'object-shorthand': ['error', 'always'],
|
||||
'svelte/no-at-const-tags': 'error',
|
||||
'svelte/no-navigation-without-resolve': 'off',
|
||||
'unicorn/prefer-early-return': 'off',
|
||||
'unicorn/prefer-ternary': 'off',
|
||||
|
||||
+19
-10
@@ -3,6 +3,14 @@
|
||||
"version": "3.3.5",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#lib": "./src/lib/index.js",
|
||||
"#lib/*": "./src/lib/*",
|
||||
"#tests": "./src/../tests",
|
||||
"#tests/*": "./src/../tests/*",
|
||||
"#i18n/*": "../i18n/*",
|
||||
"#test-data/*": "./src/test-data/*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||
"build": "vite build",
|
||||
@@ -27,7 +35,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@immich/ui": "^0.90.0",
|
||||
"@immich/ui": "^0.91.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
@@ -36,8 +44,8 @@
|
||||
"@photo-sphere-viewer/resolution-plugin": "^5.14.0",
|
||||
"@photo-sphere-viewer/settings-plugin": "^5.14.0",
|
||||
"@photo-sphere-viewer/video-plugin": "^5.14.0",
|
||||
"@tanstack/svelte-query": "^6.3.2",
|
||||
"@simplewebauthn/browser": "14.0.0",
|
||||
"@tanstack/svelte-query": "^6.3.2",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@zoom-image/core": "^0.43.0",
|
||||
"@zoom-image/svelte": "^0.3.0",
|
||||
@@ -78,9 +86,9 @@
|
||||
"@faker-js/faker": "^10.0.0",
|
||||
"@koddsson/eslint-plugin-tscompat": "^0.2.0",
|
||||
"@socket.io/component-emitter": "^3.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/enhanced-img": "^0.11.0",
|
||||
"@sveltejs/kit": "^2.56.1",
|
||||
"@sveltejs/adapter-static": "^4.0.0",
|
||||
"@sveltejs/enhanced-img": "^1.0.0",
|
||||
"@sveltejs/kit": "^3.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "7.3.1",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@testing-library/jest-dom": "^7.0.0",
|
||||
@@ -101,22 +109,23 @@
|
||||
"eslint-plugin-better-tailwindcss": "^4.5.0",
|
||||
"eslint-plugin-compat": "^7.0.0",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-svelte": "^3.12.4",
|
||||
"eslint-plugin-svelte": "^3.23.1",
|
||||
"eslint-plugin-unicorn": "^75.0.0",
|
||||
"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",
|
||||
"rollup-plugin-visualizer": "^7.0.0",
|
||||
"svelte": "5.57.1",
|
||||
"svelte-check": "^4.4.6",
|
||||
"svelte": "5.57.2",
|
||||
"svelte-check": "^4.7.5",
|
||||
"svelte-eslint-parser": "^1.8.1",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "8.71.2",
|
||||
"vite": "^8.0.0",
|
||||
"typescript-eslint": "8.72.0",
|
||||
"vite": "^8.0.12",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -93,7 +93,7 @@
|
||||
@layer utilities {
|
||||
@font-face {
|
||||
font-family: 'GoogleSans';
|
||||
src: url('$lib/assets/fonts/GoogleSans/GoogleSans.ttf') format('truetype-variations');
|
||||
src: url('#lib/assets/fonts/GoogleSans/GoogleSans.ttf') format('truetype-variations');
|
||||
font-weight: 410 900;
|
||||
font-style: normal;
|
||||
ascent-override: 106.25%;
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'GoogleSansCode';
|
||||
src: url('$lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf') format('truetype-variations');
|
||||
src: url('#lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf') format('truetype-variations');
|
||||
font-weight: 1 900;
|
||||
font-style: monospace;
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type en from '#i18n/en.json';
|
||||
import 'svelte-i18n';
|
||||
import type en from '$i18n/en.json';
|
||||
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
@@ -34,7 +34,7 @@ type NestedKeys<T, K = keyof T> = K extends keyof T & string
|
||||
: never;
|
||||
|
||||
declare module 'svelte-i18n' {
|
||||
import type { InterpolationValues } from '$lib/elements/format-message';
|
||||
import type { InterpolationValues } from '#lib/elements/format-message.ts';
|
||||
import type { Readable } from 'svelte/store';
|
||||
|
||||
type Translations = NestedKeys<typeof en>;
|
||||
|
||||
+2
-6
@@ -19,12 +19,8 @@
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48.png" />
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96.png" />
|
||||
<link rel="icon" type="image/png" sizes="144x144" href="/favicon-144.png" />
|
||||
<link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" sizes="any" href="/icon.svg" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180.png" />
|
||||
<link rel="preload" as="font" type="font/ttf" href="%app.font%" crossorigin="anonymous" />
|
||||
<link rel="preload" as="font" type="font/ttf" href="%app.monofont%" crossorigin="anonymous" />
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineEnvVars } from '@sveltejs/kit/env';
|
||||
|
||||
export const variables = defineEnvVars({
|
||||
PUBLIC_IMMICH_BUY_HOST: { public: true, static: true },
|
||||
PUBLIC_IMMICH_PAY_HOST: { public: true, static: true },
|
||||
});
|
||||
+10
-6
@@ -1,5 +1,5 @@
|
||||
import { isHttpError, type ApiHttpError } from '@immich/sdk';
|
||||
import type { HandleClientError } from '@sveltejs/kit';
|
||||
import type { HandleClientError } from '@sveltejs/kit/hooks';
|
||||
|
||||
const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
|
||||
|
||||
@@ -19,19 +19,23 @@ const parseHTTPError = (httpError: ApiHttpError) => {
|
||||
};
|
||||
};
|
||||
|
||||
const parseError = (error: unknown, status: number, message: string) => {
|
||||
const parseError = (error: App.Error | { status: number; message: string }) => {
|
||||
if (isHttpError(error)) {
|
||||
return parseHTTPError(error);
|
||||
}
|
||||
|
||||
return {
|
||||
message: (error as Error)?.message || message || DEFAULT_MESSAGE,
|
||||
code: status,
|
||||
message: error.message || DEFAULT_MESSAGE,
|
||||
code: error.status,
|
||||
};
|
||||
};
|
||||
|
||||
export const handleError: HandleClientError = ({ error, status, message }) => {
|
||||
const result = parseError(error, status, message);
|
||||
export const handleError: HandleClientError = ({ kind, error }) => {
|
||||
if (kind === 'unknown') {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
const result = parseError(error);
|
||||
console.error(`[hooks.client.ts]:handleError ${result.message}`, error);
|
||||
return result;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user