mirror of
https://github.com/immich-app/immich.git
synced 2026-07-09 05:39:33 -07:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41a2fa8a44 | |||
| 7a9b9e01be | |||
| 0cbe07c4d4 | |||
| 45ea248e8b | |||
| 8cbf0db3b3 | |||
| a225c962de | |||
| daabab8c1c | |||
| a4f28dafb2 | |||
| 704b287bba | |||
| c7b4a798cb | |||
| f8e78792b8 | |||
| 0ebb723b39 | |||
| 99a8354145 | |||
| 7ee2f3644f | |||
| d2a46ade78 | |||
| 68b04e8e8f | |||
| 58b1c5e8fb | |||
| 6f45521610 | |||
| 69d568f289 | |||
| a6f0253544 | |||
| 32352fa375 | |||
| 6aa5651032 | |||
| 4e25db2989 | |||
| 4f5e0d3505 | |||
| b3abe0bb5d | |||
| c04acc8951 | |||
| af32a8ca7c | |||
| ed83fb3f5a | |||
| 628de417e9 | |||
| be02c00afd | |||
| 43abb9a92f | |||
| 103585ceb9 |
@@ -103,14 +103,14 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
||||||
|
|
||||||
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|
||||||
- name: Restore Gradle Cache
|
- name: Restore Gradle Cache
|
||||||
id: cache-gradle-restore
|
id: cache-gradle-restore
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -181,7 +181,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Save Gradle Cache
|
- name: Save Gradle Cache
|
||||||
id: cache-gradle-save
|
id: cache-gradle-save
|
||||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -237,7 +237,7 @@ jobs:
|
|||||||
run: flutter build ios --config-only --no-codesign
|
run: flutter build ios --config-only --no-codesign
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@89f90524b88a01fe6e0b732220432cc6142926af # v1.313.0
|
uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.3'
|
ruby-version: '3.3'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for breaking API changes
|
- name: Check for breaking API changes
|
||||||
uses: oasdiff/oasdiff-action/breaking@e24529087d93f837b28b50bb66ba9016380a7fcc # v0.1.2
|
uses: oasdiff/oasdiff-action/breaking@ae5ef6402ebe218ac6e872c59d0fa4557694e75c # v0.1.4
|
||||||
with:
|
with:
|
||||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||||
revision: open-api/immich-openapi-specs.json
|
revision: open-api/immich-openapi-specs.json
|
||||||
|
|||||||
@@ -75,13 +75,13 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate docker image tags
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
with:
|
with:
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
latest=false
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}
|
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
file: packages/cli/Dockerfile
|
file: packages/cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ 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
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -83,6 +83,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
|
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
suffix: ['']
|
suffix: ['']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: ${{ needs.bump_version.outputs.rc }}
|
prerelease: ${{ needs.bump_version.outputs.rc }}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
||||||
<a href="readme_i18n/README_vi_VN.md">Tiếng Việt</a>
|
<a href="readme_i18n/README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="readme_i18n/README_th_TH.md">ภาษาไทย</a>
|
<a href="readme_i18n/README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="readme_i18n/README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,30 +7,37 @@ backend = "aqua:opentofu/opentofu"
|
|||||||
[tools.opentofu."platforms.linux-arm64"]
|
[tools.opentofu."platforms.linux-arm64"]
|
||||||
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536382"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-arm64-musl"]
|
[tools.opentofu."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536382"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-x64"]
|
[tools.opentofu."platforms.linux-x64"]
|
||||||
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536401"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-x64-musl"]
|
[tools.opentofu."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536401"
|
||||||
|
|
||||||
[tools.opentofu."platforms.macos-arm64"]
|
[tools.opentofu."platforms.macos-arm64"]
|
||||||
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
|
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536399"
|
||||||
|
|
||||||
[tools.opentofu."platforms.macos-x64"]
|
[tools.opentofu."platforms.macos-x64"]
|
||||||
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
|
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536384"
|
||||||
|
|
||||||
[tools.opentofu."platforms.windows-x64"]
|
[tools.opentofu."platforms.windows-x64"]
|
||||||
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
|
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406"
|
||||||
|
|
||||||
[[tools.terragrunt]]
|
[[tools.terragrunt]]
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -39,27 +46,34 @@ backend = "aqua:gruntwork-io/terragrunt"
|
|||||||
[tools.terragrunt."platforms.linux-arm64"]
|
[tools.terragrunt."platforms.linux-arm64"]
|
||||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
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"]
|
[tools.terragrunt."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
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-x64"]
|
[tools.terragrunt."platforms.linux-x64"]
|
||||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
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"]
|
[tools.terragrunt."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
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.macos-arm64"]
|
[tools.terragrunt."platforms.macos-arm64"]
|
||||||
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
|
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"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.macos-x64"]
|
[tools.terragrunt."platforms.macos-x64"]
|
||||||
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
|
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"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.windows-x64"]
|
[tools.terragrunt."platforms.windows-x64"]
|
||||||
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
|
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"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ You can trigger a database backup manually:
|
|||||||
|
|
||||||
1. Go to **Administration > Job Queues**
|
1. Go to **Administration > Job Queues**
|
||||||
2. Click **Create job** in the top right
|
2. Click **Create job** in the top right
|
||||||
3. Select **Create Database Backup** and click **Confirm**
|
3. Select **Create Database Dump** and click **Confirm**
|
||||||
|
|
||||||
The backup will appear in `UPLOAD_LOCATION/backups` and counts toward your retention limit.
|
The backup will appear in `UPLOAD_LOCATION/backups` and counts toward your retention limit.
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ This environment includes the services below. Additional details are available i
|
|||||||
- Redis
|
- Redis
|
||||||
- PostgreSQL development database with exposed port `5432` so you can use any database client to access it
|
- PostgreSQL development database with exposed port `5432` so you can use any database client to access it
|
||||||
|
|
||||||
All the services are packaged to run as with single Docker Compose command.
|
All the services are packaged to run with a single Docker Compose command.
|
||||||
|
|
||||||
:::tip mise
|
:::tip mise
|
||||||
[mise](https://mise.jdx.dev) is used throughout the project to manage tool versions and run tasks. [Install mise](https://mise.jdx.dev/installing-mise.html), then from the repo root run `mise trust` and `mise install` to get all required tools. Tasks for each service can be run from the repo root using `mise //namespace:task` (e.g. `mise //server:lint`). To list all available tasks, run `mise tasks ls --all`.
|
[mise](https://mise.jdx.dev) is used throughout the project to manage tool versions and run tasks. [Install mise](https://mise.jdx.dev/installing-mise.html), then from the repo root run `mise trust` and `mise install` to get all required tools. Tasks for each service can be run from the repo root using `mise //namespace:task` (e.g. `mise //server:lint`). To list all available tasks, run `mise tasks ls --all`.
|
||||||
@@ -41,7 +41,7 @@ All the services are packaged to run as with single Docker Compose command.
|
|||||||
1. Clone the project repo.
|
1. Clone the project repo.
|
||||||
2. Run `cp docker/example.env docker/.env`.
|
2. Run `cp docker/example.env docker/.env`.
|
||||||
3. Edit `docker/.env` to provide values for the required variable `UPLOAD_LOCATION`.
|
3. Edit `docker/.env` to provide values for the required variable `UPLOAD_LOCATION`.
|
||||||
4. Install dependencies - `pnpm i`
|
4. Install dependencies - `mise x -- pnpm i`
|
||||||
5. From the root directory, run:
|
5. From the root directory, run:
|
||||||
|
|
||||||
```bash title="Start development server"
|
```bash title="Start development server"
|
||||||
@@ -52,7 +52,7 @@ mise dev
|
|||||||
|
|
||||||
All the services will be started with hot-reloading enabled for a quick feedback loop.
|
All the services will be started with hot-reloading enabled for a quick feedback loop.
|
||||||
|
|
||||||
You can access the web from `http://your-machine-ip:3000` or `http://localhost:3000` and access the server from the mobile app at `http://your-machine-ip:3000/api`
|
You can access the web from `http://your-machine-ip:3000` or `http://localhost:3000` and access the server from the mobile app at `http://your-machine-ip:3000`
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,15 @@ docker image prune
|
|||||||
|
|
||||||
## Versioning Policy
|
## Versioning Policy
|
||||||
|
|
||||||
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`. We intend for breaking changes to be limited to major version releases.
|
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`.
|
||||||
You can configure your Docker image to point to the current major version by using a metatag, such as `:v3`.
|
We intend for breaking changes, including those to the API or deployment, to be limited to major version releases.
|
||||||
|
You can configure your Docker image to point to the current major version by using a metatag, such as `:v3`. These metatags do not follow release candidates.
|
||||||
|
|
||||||
Currently, we have no plans to backport patches to earlier versions. We encourage all users to run the most recent release of Immich.
|
The mobile app is typically compatible with the current and prior major version. However, the server is only compatible with the matching major version.
|
||||||
Switching back to an earlier version, even within the same minor release tag, is not supported.
|
Thus, we recommend upgrading all mobile clients before upgrading the server to ensure compatibility.
|
||||||
|
|
||||||
|
We do not backport patches to earlier versions. We encourage all users to run the most recent stable release of Immich.
|
||||||
|
Downgrading to an earlier version, even within the same minor version, is not supported.
|
||||||
|
|
||||||
[semver]: https://semver.org/
|
[semver]: https://semver.org/
|
||||||
|
|
||||||
|
|||||||
@@ -431,6 +431,10 @@
|
|||||||
"transcoding_realtime_description": "Allows transcoding to be performed in real-time as the video is being streamed. Enables quality switching, but may cause higher playback latency and stuttering depending on server capabilities.",
|
"transcoding_realtime_description": "Allows transcoding to be performed in real-time as the video is being streamed. Enables quality switching, but may cause higher playback latency and stuttering depending on server capabilities.",
|
||||||
"transcoding_realtime_enabled": "Enable real-time transcoding",
|
"transcoding_realtime_enabled": "Enable real-time transcoding",
|
||||||
"transcoding_realtime_enabled_description": "If disabled, the server will refuse to start new real-time transcoding sessions.",
|
"transcoding_realtime_enabled_description": "If disabled, the server will refuse to start new real-time transcoding sessions.",
|
||||||
|
"transcoding_realtime_resolutions": "Resolutions",
|
||||||
|
"transcoding_realtime_resolutions_description": "The resolutions offered for real-time transcoding. Higher resolutions may cause playback issues if the server cannot transcode them quickly enough.",
|
||||||
|
"transcoding_realtime_video_codecs": "Video codecs",
|
||||||
|
"transcoding_realtime_video_codecs_description": "The video codecs offered for real-time transcoding. Clients will choose the best option they support during playback. AV1 is more efficient than HEVC, which is more efficient than H.264. When using hardware acceleration, only select the codecs the accelerator can encode. When using software transcoding, note that H.264 is faster than AV1, which is faster than HEVC.",
|
||||||
"transcoding_reference_frames": "Reference frames",
|
"transcoding_reference_frames": "Reference frames",
|
||||||
"transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.",
|
"transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.",
|
||||||
"transcoding_required_description": "Only videos not in an accepted format",
|
"transcoding_required_description": "Only videos not in an accepted format",
|
||||||
@@ -593,6 +597,7 @@
|
|||||||
"asset_viewer_settings_title": "Asset Viewer",
|
"asset_viewer_settings_title": "Asset Viewer",
|
||||||
"assets": "Assets",
|
"assets": "Assets",
|
||||||
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
||||||
|
"assets_added_to_album_partial_count": "Added {successCount} out of {totalCount} {totalCount, plural, one {asset} other {assets}} to the album",
|
||||||
"assets_added_to_albums_count": "Added {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}",
|
"assets_added_to_albums_count": "Added {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}",
|
||||||
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
|
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
|
||||||
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums",
|
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums",
|
||||||
@@ -2150,6 +2155,7 @@
|
|||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"unknown_country": "Unknown Country",
|
"unknown_country": "Unknown Country",
|
||||||
"unknown_date": "Unknown date",
|
"unknown_date": "Unknown date",
|
||||||
|
"unknown_schema": "Unknown schema",
|
||||||
"unknown_year": "Unknown Year",
|
"unknown_year": "Unknown Year",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
"unlink_motion_video": "Unlink motion video",
|
"unlink_motion_video": "Unlink motion video",
|
||||||
|
|||||||
+21
-14
@@ -5,38 +5,38 @@ version = "3.11.15"
|
|||||||
backend = "core:python"
|
backend = "core:python"
|
||||||
|
|
||||||
[tools.python."platforms.linux-arm64"]
|
[tools.python."platforms.linux-arm64"]
|
||||||
checksum = "sha256:cbce0660e88cd9c56be7aaf2a2df92bea51f359388a521838b6b01817d728df0"
|
checksum = "sha256:9ac18c9a761e91e6c6452bc0ef0082922a00a3fdec734555635d57c3169309b7"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.python."platforms.linux-arm64-musl"]
|
[tools.python."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:a55ea44225ee3741d4157c383f3d5c3e8eee5f9665e2ea069233486b4275d928"
|
checksum = "sha256:bcd414f2e9d8cc5d4ac7fab2c8f912e30379c150fa6b94da19d64daf85cfc5d4"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.python."platforms.linux-x64"]
|
[tools.python."platforms.linux-x64"]
|
||||||
checksum = "sha256:67a5b22f796e96f4d7fa628f95866d5fd1d524d0588f74e4601facd82b66792b"
|
checksum = "sha256:0604cd029b142dc223e131f17f5941c0c8d2d5074997c8178b515b19eea2a6c2"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.python."platforms.linux-x64-musl"]
|
[tools.python."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:5a8544aa4303da3ca4b7505c98dd8453b671157039d25cd551e55abea0f83a60"
|
checksum = "sha256:45bffdb320cf4a2c2dbf183b634aeb7e28f9cb0646f715b2f4117c11084d839a"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.python."platforms.macos-arm64"]
|
[tools.python."platforms.macos-arm64"]
|
||||||
checksum = "sha256:8c56f1f59142e0f9f8861ad897bdfd97fd84403afa7b3d8b0f33b208ec471355"
|
checksum = "sha256:2318799eaf104f8a29bc09a93b0851b05dbbcb4ce9a5f045ddea169c0c7ff3a5"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.python."platforms.macos-x64"]
|
[tools.python."platforms.macos-x64"]
|
||||||
checksum = "sha256:8cd3878c656ba1698314cbcb65f78df4c37b7c8eabff958558115c6db11adb3d"
|
checksum = "sha256:4925e5aaa9bc77c85302d350b36c1d9def2002996a6bcfa55c88ba6eb318de29"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.python."platforms.windows-x64"]
|
[tools.python."platforms.windows-x64"]
|
||||||
checksum = "sha256:f081a733b4e7ba0e5e5e12d533b3c795dbef3ecbebf92f0b4202e5329bf7c8ab"
|
checksum = "sha256:6589ca6d63f520bec4096d62b3ab91da3d0a80b16b594c99a6b677e335814683"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[[tools.uv]]
|
[[tools.uv]]
|
||||||
@@ -46,27 +46,34 @@ backend = "aqua:astral-sh/uv"
|
|||||||
[tools.uv."platforms.linux-arm64"]
|
[tools.uv."platforms.linux-arm64"]
|
||||||
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
|
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
|
||||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235419"
|
||||||
|
|
||||||
[tools.uv."platforms.linux-arm64-musl"]
|
[tools.uv."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
|
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
|
||||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235419"
|
||||||
|
|
||||||
[tools.uv."platforms.linux-x64"]
|
[tools.uv."platforms.linux-x64"]
|
||||||
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
|
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
|
||||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235475"
|
||||||
|
|
||||||
[tools.uv."platforms.linux-x64-musl"]
|
[tools.uv."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
|
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
|
||||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235475"
|
||||||
|
|
||||||
[tools.uv."platforms.macos-arm64"]
|
[tools.uv."platforms.macos-arm64"]
|
||||||
checksum = "sha256:103367962c5cb00bf7370d84cbaa3fec5a9807be9cc833ea9d8eea400c119fa2"
|
checksum = "sha256:103367962c5cb00bf7370d84cbaa3fec5a9807be9cc833ea9d8eea400c119fa2"
|
||||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-apple-darwin.tar.gz"
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-apple-darwin.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235412"
|
||||||
|
|
||||||
[tools.uv."platforms.macos-x64"]
|
[tools.uv."platforms.macos-x64"]
|
||||||
checksum = "sha256:2bbef70982e97dfc36454de173f35ec1a5e83ae11e3885df6a50db3fd76171cb"
|
checksum = "sha256:2bbef70982e97dfc36454de173f35ec1a5e83ae11e3885df6a50db3fd76171cb"
|
||||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-apple-darwin.tar.gz"
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-apple-darwin.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235464"
|
||||||
|
|
||||||
[tools.uv."platforms.windows-x64"]
|
[tools.uv."platforms.windows-x64"]
|
||||||
checksum = "sha256:459d95892a5cc5c21779532f4f41b9238594b79e312a5142da2148ecfa10e705"
|
checksum = "sha256:459d95892a5cc5c21779532f4f41b9238594b79e312a5142da2148ecfa10e705"
|
||||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-pc-windows-msvc.zip"
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-pc-windows-msvc.zip"
|
||||||
|
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235467"
|
||||||
|
|||||||
@@ -254,30 +254,37 @@ backend = "aqua:opentofu/opentofu"
|
|||||||
[tools.opentofu."platforms.linux-arm64"]
|
[tools.opentofu."platforms.linux-arm64"]
|
||||||
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536382"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-arm64-musl"]
|
[tools.opentofu."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536382"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-x64"]
|
[tools.opentofu."platforms.linux-x64"]
|
||||||
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536401"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-x64-musl"]
|
[tools.opentofu."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536401"
|
||||||
|
|
||||||
[tools.opentofu."platforms.macos-arm64"]
|
[tools.opentofu."platforms.macos-arm64"]
|
||||||
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
|
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536399"
|
||||||
|
|
||||||
[tools.opentofu."platforms.macos-x64"]
|
[tools.opentofu."platforms.macos-x64"]
|
||||||
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
|
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536384"
|
||||||
|
|
||||||
[tools.opentofu."platforms.windows-x64"]
|
[tools.opentofu."platforms.windows-x64"]
|
||||||
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
|
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406"
|
||||||
|
|
||||||
[[tools.pnpm]]
|
[[tools.pnpm]]
|
||||||
version = "11.6.0"
|
version = "11.6.0"
|
||||||
@@ -286,31 +293,37 @@ backend = "aqua:pnpm/pnpm"
|
|||||||
[tools.pnpm."platforms.linux-arm64"]
|
[tools.pnpm."platforms.linux-arm64"]
|
||||||
checksum = "sha256:2fec653ff6dadab340d1c3d2214688a7451cc471f39710839440b293ca7c53b0"
|
checksum = "sha256:2fec653ff6dadab340d1c3d2214688a7451cc471f39710839440b293ca7c53b0"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64.tar.gz"
|
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120174"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
[tools.pnpm."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:56a78c08cf22adf29e7dacb6f7f100139731693863d20fb94a7883463a62169e"
|
checksum = "sha256:56a78c08cf22adf29e7dacb6f7f100139731693863d20fb94a7883463a62169e"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64-musl.tar.gz"
|
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64-musl.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120177"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-x64"]
|
[tools.pnpm."platforms.linux-x64"]
|
||||||
checksum = "sha256:74d64c1646385fb21691f32f0ab6aca1a9f5c829ba54d3cda3a24838a228e68c"
|
checksum = "sha256:74d64c1646385fb21691f32f0ab6aca1a9f5c829ba54d3cda3a24838a228e68c"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64.tar.gz"
|
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120176"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-x64-musl"]
|
[tools.pnpm."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:7a0c463a09d912fba6b7d9eca0a946bc228ea50f3015a05c09a29e7e85a932d7"
|
checksum = "sha256:7a0c463a09d912fba6b7d9eca0a946bc228ea50f3015a05c09a29e7e85a932d7"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64-musl.tar.gz"
|
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64-musl.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120175"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.macos-arm64"]
|
[tools.pnpm."platforms.macos-arm64"]
|
||||||
checksum = "sha256:87c901635a14481fb30566a3749041134ffd4317bc6fe866c345b69fdf9b6b85"
|
checksum = "sha256:87c901635a14481fb30566a3749041134ffd4317bc6fe866c345b69fdf9b6b85"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-darwin-arm64.tar.gz"
|
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-darwin-arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120180"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.windows-x64"]
|
[tools.pnpm."platforms.windows-x64"]
|
||||||
checksum = "sha256:91c753435542b04859c689304fae0dd64eba6b40937cfa426a48485b712e4e9c"
|
checksum = "sha256:91c753435542b04859c689304fae0dd64eba6b40937cfa426a48485b712e4e9c"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-win32-x64.zip"
|
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-win32-x64.zip"
|
||||||
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120173"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[[tools.terragrunt]]
|
[[tools.terragrunt]]
|
||||||
@@ -320,27 +333,34 @@ backend = "aqua:gruntwork-io/terragrunt"
|
|||||||
[tools.terragrunt."platforms.linux-arm64"]
|
[tools.terragrunt."platforms.linux-arm64"]
|
||||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
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"]
|
[tools.terragrunt."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
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-x64"]
|
[tools.terragrunt."platforms.linux-x64"]
|
||||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
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"]
|
[tools.terragrunt."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
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.macos-arm64"]
|
[tools.terragrunt."platforms.macos-arm64"]
|
||||||
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
|
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"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.macos-x64"]
|
[tools.terragrunt."platforms.macos-x64"]
|
||||||
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
|
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"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.windows-x64"]
|
[tools.terragrunt."platforms.windows-x64"]
|
||||||
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
|
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"
|
||||||
|
|||||||
@@ -107,3 +107,67 @@ Java_app_alextran_immich_NativeImage_rotate(
|
|||||||
}
|
}
|
||||||
return (jlong) dst;
|
return (jlong) dst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert an RGBA_1010102 buffer to densely-packed RGBA_8888, matching Skia's
|
||||||
|
// Bitmap.copy(ARGB_8888) byte-for-byte so it's a drop-in for the intermediate 8888 bitmap.
|
||||||
|
// Each source pixel is a native (little-endian on every Android ABI) u32 with R in bits 0-9,
|
||||||
|
// G in 10-19, B in 20-29, A in 30-31 (standard RGB10_A2). Each 10-bit channel maps to 8-bit via
|
||||||
|
// round(v*255/1023). The 2-bit alpha maps to a*85. Both are kept as plain arithmetic as the whole
|
||||||
|
// loop auto-vectorizes to NEON and measures faster than a LUT on-device. Output is R,G,B,A bytes
|
||||||
|
// per pixel, i.e. Android ARGB_8888 memory == Dart PixelFormat.rgba8888.
|
||||||
|
static void convert_1010102(const uint8_t *src, int srcStride, uint32_t *dst, int w, int h) {
|
||||||
|
for (int y = 0; y < h; y++) {
|
||||||
|
const uint32_t *srcRow = (const uint32_t *) (src + (size_t) y * srcStride);
|
||||||
|
uint32_t *dstRow = dst + (size_t) y * w;
|
||||||
|
for (int x = 0; x < w; x++) {
|
||||||
|
uint32_t px = srcRow[x];
|
||||||
|
uint32_t r = ((px & 0x3FF) * 16336u + 32768u) >> 16;
|
||||||
|
uint32_t g = (((px >> 10) & 0x3FF) * 16336u + 32768u) >> 16;
|
||||||
|
uint32_t b = (((px >> 20) & 0x3FF) * 16336u + 32768u) >> 16;
|
||||||
|
uint32_t a = ((px >> 30) & 0x3) * 85u;
|
||||||
|
dstRow[x] = r | (g << 8) | (b << 16) | (a << 24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts an RGBA_1010102 bitmap (what a 10-bit HEIC/AVIF decodes to on API 33+) into a freshly
|
||||||
|
// malloc'd RGBA_8888 buffer. Fills outInfo with {width, height, rowBytes} and returns the buffer
|
||||||
|
// address, or 0 (so the caller falls back to a Skia copy) if the bitmap isn't 1010102 or can't be
|
||||||
|
// locked. Same ownership contract as rotate: free the returned buffer via NativeBuffer.free.
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_app_alextran_immich_NativeImage_convert1010102(
|
||||||
|
JNIEnv *env, jclass clazz, jobject bitmap, jintArray outInfo) {
|
||||||
|
AndroidBitmapInfo info;
|
||||||
|
if (AndroidBitmap_getInfo(env, bitmap, &info) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_1010102) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int w = (int) info.width;
|
||||||
|
int h = (int) info.height;
|
||||||
|
|
||||||
|
uint32_t *dst = (uint32_t *) malloc((size_t) w * h * 4);
|
||||||
|
if (dst == NULL) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *srcPixels = NULL;
|
||||||
|
if (AndroidBitmap_lockPixels(env, bitmap, &srcPixels) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
||||||
|
free(dst);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
convert_1010102((const uint8_t *) srcPixels, (int) info.stride, dst, w, h);
|
||||||
|
|
||||||
|
AndroidBitmap_unlockPixels(env, bitmap);
|
||||||
|
|
||||||
|
jint dims[3] = {w, h, w * 4};
|
||||||
|
(*env)->SetIntArrayRegion(env, outInfo, 0, 3, dims);
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
free(dst);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (jlong) dst;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,4 +16,14 @@ object NativeImage {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
external fun rotate(bitmap: Bitmap, orientation: Int, outInfo: IntArray): Long
|
external fun rotate(bitmap: Bitmap, orientation: Int, outInfo: IntArray): Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an RGBA_1010102 [bitmap] (what a 10-bit HEIC/AVIF decodes to on API 33+) to RGBA_8888,
|
||||||
|
* writing the result into a freshly malloc'd native buffer in one pass, with no intermediate
|
||||||
|
* ARGB_8888 bitmap. Returns the buffer address (free it with [NativeBuffer.free]) and fills
|
||||||
|
* [outInfo] with {width, height, rowBytes}. Returns 0 when the bitmap isn't RGBA_1010102 so the
|
||||||
|
* caller can fall back to a Skia copy.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
external fun convert1010102(bitmap: Bitmap, outInfo: IntArray): Long
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,22 +46,47 @@ inline fun ImageDecoder.Source.decodeBitmap(target: Size = Size(0, 0)): Bitmap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
||||||
val size = width * height * 4
|
// Dart reads the buffer as rgba8888, but 10-bit sources decode to RGBA_1010102, which garbles
|
||||||
|
// colors when copied verbatim. Convert those straight into the output buffer in native code -
|
||||||
|
// one pass, no intermediate ARGB_8888 bitmap.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && config == Bitmap.Config.RGBA_1010102) {
|
||||||
|
val info = IntArray(3)
|
||||||
|
val pointer = NativeImage.convert1010102(this, info)
|
||||||
|
if (pointer != 0L) {
|
||||||
|
recycle()
|
||||||
|
return mapOf(
|
||||||
|
"pointer" to pointer,
|
||||||
|
"width" to info[0].toLong(),
|
||||||
|
"height" to info[1].toLong(),
|
||||||
|
"rowBytes" to info[2].toLong()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// native convert declined (OOM/lock) -> fall through to the Skia copy path below.
|
||||||
|
}
|
||||||
|
// Other non-8888 configs (e.g. HDR F16) still need Skia's convert; 8-bit is copied as-is.
|
||||||
|
val bitmap = if (config != Bitmap.Config.ARGB_8888) {
|
||||||
|
val converted = copy(Bitmap.Config.ARGB_8888, false)
|
||||||
|
recycle()
|
||||||
|
converted ?: throw IOException("could not convert bitmap to ARGB_8888")
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
val size = bitmap.width * bitmap.height * 4
|
||||||
val pointer = NativeBuffer.allocate(size)
|
val pointer = NativeBuffer.allocate(size)
|
||||||
try {
|
try {
|
||||||
val buffer = NativeBuffer.wrap(pointer, size)
|
val buffer = NativeBuffer.wrap(pointer, size)
|
||||||
copyPixelsToBuffer(buffer)
|
bitmap.copyPixelsToBuffer(buffer)
|
||||||
return mapOf(
|
return mapOf(
|
||||||
"pointer" to pointer,
|
"pointer" to pointer,
|
||||||
"width" to width.toLong(),
|
"width" to bitmap.width.toLong(),
|
||||||
"height" to height.toLong(),
|
"height" to bitmap.height.toLong(),
|
||||||
"rowBytes" to (width * 4).toLong()
|
"rowBytes" to (bitmap.width * 4).toLong()
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
NativeBuffer.free(pointer)
|
NativeBuffer.free(pointer)
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
recycle()
|
bitmap.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package app.alextran.immich.images
|
package app.alextran.immich.images
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.os.OperationCanceledException
|
import android.os.OperationCanceledException
|
||||||
import app.alextran.immich.INITIAL_BUFFER_SIZE
|
import app.alextran.immich.INITIAL_BUFFER_SIZE
|
||||||
@@ -22,6 +24,7 @@ import java.io.File
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
private const val MAX_PREALLOC_BYTES = 128 * 1024 * 1024
|
private const val MAX_PREALLOC_BYTES = 128 * 1024 * 1024
|
||||||
|
|
||||||
@@ -36,12 +39,16 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val CANCELLED = Result.success<Map<String, Long>?>(null)
|
val CANCELLED = Result.success<Map<String, Long>?>(null)
|
||||||
|
|
||||||
|
// Shared, process-lifetime pool: RemoteImagesImpl is re-created per FlutterEngine, so a
|
||||||
|
// per-instance pool would leak threads across engine restarts.
|
||||||
|
private val decodeExecutor = Executors.newFixedThreadPool(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestImage(
|
override fun requestImage(
|
||||||
url: String,
|
url: String,
|
||||||
requestId: Long,
|
requestId: Long,
|
||||||
@Suppress("UNUSED_PARAMETER") preferEncoded: Boolean, // always returns encoded; setting has no effect on Android
|
preferEncoded: Boolean,
|
||||||
callback: (Result<Map<String, Long>?>) -> Unit
|
callback: (Result<Map<String, Long>?>) -> Unit
|
||||||
) {
|
) {
|
||||||
val signal = CancellationSignal()
|
val signal = CancellationSignal()
|
||||||
@@ -51,12 +58,51 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
url,
|
url,
|
||||||
signal,
|
signal,
|
||||||
onSuccess = { buffer ->
|
onSuccess = { buffer ->
|
||||||
requestMap.remove(requestId)
|
|
||||||
if (signal.isCanceled) {
|
if (signal.isCanceled) {
|
||||||
NativeBuffer.free(buffer.pointer)
|
requestMap.remove(requestId)
|
||||||
|
buffer.free()
|
||||||
return@fetch callback(CANCELLED)
|
return@fetch callback(CANCELLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode natively when the caller wants pixels: Flutter's fallback decoder copies
|
||||||
|
// 10-bit bitmaps (RGBA_1010102) as if they were rgba8888, garbling colors. Decode on a
|
||||||
|
// dedicated pool - the fetch callback threads are shared with video streaming. On any
|
||||||
|
// decode failure (including OOM on huge originals), hand Flutter the encoded bytes as
|
||||||
|
// before.
|
||||||
|
if (!preferEncoded && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
decodeExecutor.execute {
|
||||||
|
val res = if (signal.isCanceled) null else try {
|
||||||
|
val source = ImageDecoder.createSource(NativeBuffer.wrap(buffer.pointer, buffer.offset))
|
||||||
|
source.decodeBitmap().toNativeBuffer()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
requestMap.remove(requestId)
|
||||||
|
when {
|
||||||
|
// Deliver even if the request was cancelled meanwhile: re-checking here would orphan
|
||||||
|
// res's malloc, and Dart frees the buffer itself when it sees the cancel.
|
||||||
|
res != null -> {
|
||||||
|
buffer.free()
|
||||||
|
callback(Result.success(res))
|
||||||
|
}
|
||||||
|
signal.isCanceled -> {
|
||||||
|
buffer.free()
|
||||||
|
callback(CANCELLED)
|
||||||
|
}
|
||||||
|
else -> callback(
|
||||||
|
Result.success(
|
||||||
|
mapOf(
|
||||||
|
"pointer" to buffer.pointer,
|
||||||
|
"length" to buffer.offset.toLong()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMap.remove(requestId)
|
||||||
callback(
|
callback(
|
||||||
Result.success(
|
Result.success(
|
||||||
mapOf(
|
mapOf(
|
||||||
|
|||||||
+3626
File diff suppressed because it is too large
Load Diff
@@ -70,7 +70,8 @@
|
|||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
enableGPUValidationMode = "1"
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES"
|
||||||
|
queueDebuggingEnabled = "NO">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
|
|||||||
@@ -1,19 +1,65 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
enum AspectRatioPreset {
|
class CropAspectRatio {
|
||||||
free(ratio: null, label: 'Free', icon: Icons.crop_free_rounded),
|
final int? numerator;
|
||||||
square(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded),
|
final int? denominator;
|
||||||
ratio16x9(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded),
|
|
||||||
ratio3x2(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded),
|
|
||||||
ratio7x5(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded),
|
|
||||||
ratio9x16(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true),
|
|
||||||
ratio2x3(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true),
|
|
||||||
ratio5x7(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true);
|
|
||||||
|
|
||||||
final double? ratio;
|
final String? customLabel;
|
||||||
final String label;
|
final IconData? icon;
|
||||||
final IconData icon;
|
|
||||||
final bool iconRotated;
|
|
||||||
|
|
||||||
const AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false});
|
const CropAspectRatio({this.numerator, this.denominator, this.customLabel, this.icon});
|
||||||
|
|
||||||
|
static const free = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free);
|
||||||
|
static const original = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original);
|
||||||
|
|
||||||
|
String get label {
|
||||||
|
return customLabel ?? (numerator != null && denominator != null ? '$numerator:$denominator' : 'Free');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasFlippedVariant => numerator != denominator;
|
||||||
|
double? get ratio => (numerator != null && denominator != null) ? numerator! / denominator! : null;
|
||||||
|
|
||||||
|
CropAspectRatio get flipped {
|
||||||
|
return CropAspectRatio(numerator: denominator, denominator: numerator, customLabel: customLabel, icon: icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return other is CropAspectRatio &&
|
||||||
|
other.numerator == numerator &&
|
||||||
|
other.denominator == denominator &&
|
||||||
|
other.customLabel == customLabel &&
|
||||||
|
other.icon == icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return numerator.hashCode ^ denominator.hashCode ^ customLabel.hashCode ^ icon.hashCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const aspectRatioFree = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free);
|
||||||
|
const aspectRatioOriginal = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original);
|
||||||
|
|
||||||
|
final aspectRatioPresets = [
|
||||||
|
CropAspectRatio.free,
|
||||||
|
CropAspectRatio.original,
|
||||||
|
|
||||||
|
const CropAspectRatio(numerator: 1, denominator: 1),
|
||||||
|
|
||||||
|
// lanscape
|
||||||
|
const CropAspectRatio(numerator: 16, denominator: 9),
|
||||||
|
const CropAspectRatio(numerator: 3, denominator: 2),
|
||||||
|
const CropAspectRatio(numerator: 7, denominator: 5),
|
||||||
|
const CropAspectRatio(numerator: 4, denominator: 3),
|
||||||
|
|
||||||
|
// portrait
|
||||||
|
const CropAspectRatio(numerator: 16, denominator: 9).flipped,
|
||||||
|
const CropAspectRatio(numerator: 3, denominator: 2).flipped,
|
||||||
|
const CropAspectRatio(numerator: 7, denominator: 5).flipped,
|
||||||
|
const CropAspectRatio(numerator: 4, denominator: 3).flipped,
|
||||||
|
];
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ sealed class BaseAsset {
|
|||||||
final int? height;
|
final int? height;
|
||||||
final int? durationMs;
|
final int? durationMs;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
final String? livePhotoVideoId;
|
|
||||||
final bool isEdited;
|
final bool isEdited;
|
||||||
|
|
||||||
const BaseAsset({
|
const BaseAsset({
|
||||||
@@ -40,32 +39,15 @@ sealed class BaseAsset {
|
|||||||
this.height,
|
this.height,
|
||||||
this.durationMs,
|
this.durationMs,
|
||||||
this.isFavorite = false,
|
this.isFavorite = false,
|
||||||
this.livePhotoVideoId,
|
|
||||||
required this.isEdited,
|
required this.isEdited,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isImage => type == AssetType.image;
|
bool get isImage => type == AssetType.image;
|
||||||
bool get isVideo => type == AssetType.video;
|
bool get isVideo => type == AssetType.video;
|
||||||
|
|
||||||
bool get isMotionPhoto => livePhotoVideoId != null;
|
bool get isMotionPhoto => playbackStyle == AssetPlaybackStyle.livePhoto;
|
||||||
bool get isAnimatedImage => playbackStyle == AssetPlaybackStyle.imageAnimated;
|
bool get isAnimatedImage => playbackStyle == AssetPlaybackStyle.imageAnimated;
|
||||||
|
|
||||||
AssetPlaybackStyle get playbackStyle {
|
|
||||||
if (isVideo) {
|
|
||||||
return AssetPlaybackStyle.video;
|
|
||||||
}
|
|
||||||
if (isMotionPhoto) {
|
|
||||||
return AssetPlaybackStyle.livePhoto;
|
|
||||||
}
|
|
||||||
if (isImage && durationMs != null && durationMs! > 0) {
|
|
||||||
return AssetPlaybackStyle.imageAnimated;
|
|
||||||
}
|
|
||||||
if (isImage) {
|
|
||||||
return AssetPlaybackStyle.image;
|
|
||||||
}
|
|
||||||
return AssetPlaybackStyle.unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
Duration get duration {
|
Duration get duration {
|
||||||
final durationMs = this.durationMs;
|
final durationMs = this.durationMs;
|
||||||
if (durationMs != null) {
|
if (durationMs != null) {
|
||||||
@@ -86,6 +68,7 @@ sealed class BaseAsset {
|
|||||||
String? get localId;
|
String? get localId;
|
||||||
String? get remoteId;
|
String? get remoteId;
|
||||||
String get heroTag;
|
String get heroTag;
|
||||||
|
AssetPlaybackStyle get playbackStyle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class LocalAsset extends BaseAsset {
|
|||||||
super.height,
|
super.height,
|
||||||
super.durationMs,
|
super.durationMs,
|
||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
super.livePhotoVideoId,
|
|
||||||
this.orientation = 0,
|
this.orientation = 0,
|
||||||
required this.playbackStyle,
|
required this.playbackStyle,
|
||||||
this.adjustmentTime,
|
this.adjustmentTime,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
final AssetVisibility visibility;
|
final AssetVisibility visibility;
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
final String? stackId;
|
final String? stackId;
|
||||||
|
final String? livePhotoVideoId;
|
||||||
final DateTime? uploadedAt;
|
final DateTime? uploadedAt;
|
||||||
final DateTime? deletedAt;
|
final DateTime? deletedAt;
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
this.thumbHash,
|
this.thumbHash,
|
||||||
this.visibility = AssetVisibility.timeline,
|
this.visibility = AssetVisibility.timeline,
|
||||||
super.livePhotoVideoId,
|
this.livePhotoVideoId,
|
||||||
this.stackId,
|
this.stackId,
|
||||||
required super.isEdited,
|
required super.isEdited,
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
@@ -38,6 +39,23 @@ class RemoteAsset extends BaseAsset {
|
|||||||
@override
|
@override
|
||||||
String? get localId => localAssetId;
|
String? get localId => localAssetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AssetPlaybackStyle get playbackStyle {
|
||||||
|
if (isVideo) {
|
||||||
|
return AssetPlaybackStyle.video;
|
||||||
|
}
|
||||||
|
if (livePhotoVideoId != null) {
|
||||||
|
return AssetPlaybackStyle.livePhoto;
|
||||||
|
}
|
||||||
|
if (isImage && durationMs != null && durationMs! > 0) {
|
||||||
|
return AssetPlaybackStyle.imageAnimated;
|
||||||
|
}
|
||||||
|
if (isImage) {
|
||||||
|
return AssetPlaybackStyle.image;
|
||||||
|
}
|
||||||
|
return AssetPlaybackStyle.unknown;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get remoteId => id;
|
String? get remoteId => id;
|
||||||
|
|
||||||
@@ -90,6 +108,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
thumbHash == other.thumbHash &&
|
thumbHash == other.thumbHash &&
|
||||||
visibility == other.visibility &&
|
visibility == other.visibility &&
|
||||||
stackId == other.stackId &&
|
stackId == other.stackId &&
|
||||||
|
livePhotoVideoId == other.livePhotoVideoId &&
|
||||||
uploadedAt == other.uploadedAt &&
|
uploadedAt == other.uploadedAt &&
|
||||||
deletedAt == other.deletedAt;
|
deletedAt == other.deletedAt;
|
||||||
}
|
}
|
||||||
@@ -103,6 +122,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
thumbHash.hashCode ^
|
thumbHash.hashCode ^
|
||||||
visibility.hashCode ^
|
visibility.hashCode ^
|
||||||
stackId.hashCode ^
|
stackId.hashCode ^
|
||||||
|
livePhotoVideoId.hashCode ^
|
||||||
uploadedAt.hashCode ^
|
uploadedAt.hashCode ^
|
||||||
deletedAt.hashCode;
|
deletedAt.hashCode;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class AssetService {
|
|||||||
return _localRepository.getByChecksum(checksum);
|
return _localRepository.getByChecksum(checksum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<LocalAsset?> getLocalAsset(String id) {
|
||||||
|
return _localRepository.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
|
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
|
||||||
return _remoteRepository.getByChecksum(checksum);
|
return _remoteRepository.getByChecksum(checksum);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,8 +85,12 @@ class TimelineFactory {
|
|||||||
TimelineService fromAssetsWithBuckets(List<BaseAsset> assets, TimelineOrigin type) =>
|
TimelineService fromAssetsWithBuckets(List<BaseAsset> assets, TimelineOrigin type) =>
|
||||||
TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type));
|
TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type));
|
||||||
|
|
||||||
TimelineService map(List<String> userIds, TimelineMapOptions options) =>
|
/// Creates a TimelineService for serving geographical map queries, such assets within bounded locations
|
||||||
TimelineService(_timelineRepository.map(userIds, options, groupBy));
|
TimelineService geographicMap(
|
||||||
|
List<String> userIds,
|
||||||
|
TimelineMapOptions Function() currentOptions,
|
||||||
|
Stream<TimelineMapOptions> optionsStream,
|
||||||
|
) => TimelineService(_timelineRepository.geographicMap(userIds, currentOptions, optionsStream, groupBy));
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineService {
|
class TimelineService {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ WHERE (library_id IS NOT NULL);
|
|||||||
CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created
|
CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created
|
||||||
ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)
|
ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)
|
||||||
''')
|
''')
|
||||||
|
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)')
|
||||||
class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||||
const RemoteAssetEntity();
|
const RemoteAssetEntity();
|
||||||
|
|
||||||
|
|||||||
@@ -1779,3 +1779,7 @@ i0.Index get idxRemoteAssetOwnerVisibilityDeletedCreated => i0.Index(
|
|||||||
'idx_remote_asset_owner_visibility_deleted_created',
|
'idx_remote_asset_owner_visibility_deleted_created',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
||||||
);
|
);
|
||||||
|
i0.Index get idxRemoteAssetUploaded => i0.Index(
|
||||||
|
'idx_remote_asset_uploaded',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)',
|
||||||
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: false);
|
final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: false);
|
||||||
// Android always returns encoded data, so we need to check for both shapes of the response.
|
// Android falls back to encoded data if native decoding fails, so check for both shapes of the response.
|
||||||
final frame = switch (info) {
|
final frame = switch (info) {
|
||||||
{'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length),
|
{'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length),
|
||||||
{'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} =>
|
{'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} =>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class Drift extends $Drift {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 30;
|
int get schemaVersion => 31;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -128,199 +128,209 @@ class Drift extends $Drift {
|
|||||||
// Run migration steps without foreign keys and re-enable them later
|
// Run migration steps without foreign keys and re-enable them later
|
||||||
await customStatement('PRAGMA foreign_keys = OFF');
|
await customStatement('PRAGMA foreign_keys = OFF');
|
||||||
|
|
||||||
await m.runMigrationSteps(
|
try {
|
||||||
from: from,
|
await transaction(
|
||||||
to: to,
|
() => m.runMigrationSteps(
|
||||||
steps: migrationSteps(
|
from: from,
|
||||||
from1To2: (m, v2) async {
|
to: to,
|
||||||
for (final entity in v2.entities) {
|
steps: migrationSteps(
|
||||||
await m.drop(entity);
|
from1To2: (m, v2) async {
|
||||||
await m.create(entity);
|
for (final entity in v2.entities) {
|
||||||
}
|
await m.drop(entity);
|
||||||
},
|
await m.create(entity);
|
||||||
from2To3: (m, v3) async {
|
}
|
||||||
// Removed foreign key constraint on stack.primaryAssetId
|
},
|
||||||
await m.alterTable(TableMigration(v3.stackEntity));
|
from2To3: (m, v3) async {
|
||||||
},
|
// Removed foreign key constraint on stack.primaryAssetId
|
||||||
from3To4: (m, v4) async {
|
await m.alterTable(TableMigration(v3.stackEntity));
|
||||||
// Thumbnail path column got removed from person_entity
|
},
|
||||||
await m.alterTable(TableMigration(v4.personEntity));
|
from3To4: (m, v4) async {
|
||||||
// asset_face_entity is added
|
// Thumbnail path column got removed from person_entity
|
||||||
await m.create(v4.assetFaceEntity);
|
await m.alterTable(TableMigration(v4.personEntity));
|
||||||
},
|
// asset_face_entity is added
|
||||||
from4To5: (m, v5) async {
|
await m.create(v4.assetFaceEntity);
|
||||||
await m.alterTable(
|
},
|
||||||
TableMigration(
|
from4To5: (m, v5) async {
|
||||||
v5.userEntity,
|
await m.alterTable(
|
||||||
newColumns: [v5.userEntity.hasProfileImage, v5.userEntity.profileChangedAt],
|
TableMigration(
|
||||||
columnTransformer: {v5.userEntity.profileChangedAt: currentDateAndTime},
|
v5.userEntity,
|
||||||
),
|
newColumns: [v5.userEntity.hasProfileImage, v5.userEntity.profileChangedAt],
|
||||||
);
|
columnTransformer: {v5.userEntity.profileChangedAt: currentDateAndTime},
|
||||||
},
|
),
|
||||||
from5To6: (m, v6) async {
|
);
|
||||||
// Drops the (checksum, ownerId) and adds it back as (ownerId, checksum)
|
},
|
||||||
await customStatement('DROP INDEX IF EXISTS UQ_remote_asset_owner_checksum');
|
from5To6: (m, v6) async {
|
||||||
await m.drop(v6.idxRemoteAssetOwnerChecksum);
|
// Drops the (checksum, ownerId) and adds it back as (ownerId, checksum)
|
||||||
await m.create(v6.idxRemoteAssetOwnerChecksum);
|
await customStatement('DROP INDEX IF EXISTS UQ_remote_asset_owner_checksum');
|
||||||
// Adds libraryId to remote_asset_entity
|
await m.drop(v6.idxRemoteAssetOwnerChecksum);
|
||||||
await m.addColumn(v6.remoteAssetEntity, v6.remoteAssetEntity.libraryId);
|
await m.create(v6.idxRemoteAssetOwnerChecksum);
|
||||||
await m.drop(v6.uQRemoteAssetsOwnerChecksum);
|
// Adds libraryId to remote_asset_entity
|
||||||
await m.create(v6.uQRemoteAssetsOwnerChecksum);
|
await m.addColumn(v6.remoteAssetEntity, v6.remoteAssetEntity.libraryId);
|
||||||
await m.drop(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
await m.drop(v6.uQRemoteAssetsOwnerChecksum);
|
||||||
await m.create(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
await m.create(v6.uQRemoteAssetsOwnerChecksum);
|
||||||
},
|
await m.drop(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
||||||
from6To7: (m, v7) async {
|
await m.create(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
||||||
await m.createIndex(v7.idxLatLng);
|
},
|
||||||
},
|
from6To7: (m, v7) async {
|
||||||
from7To8: (m, v8) async {
|
await m.createIndex(v7.idxLatLng);
|
||||||
await m.create(v8.storeEntity);
|
},
|
||||||
},
|
from7To8: (m, v8) async {
|
||||||
from8To9: (m, v9) async {
|
await m.create(v8.storeEntity);
|
||||||
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
|
},
|
||||||
},
|
from8To9: (m, v9) async {
|
||||||
from9To10: (m, v10) async {
|
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
|
||||||
await m.createTable(v10.authUserEntity);
|
},
|
||||||
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
|
from9To10: (m, v10) async {
|
||||||
await m.alterTable(TableMigration(v10.userEntity));
|
await m.createTable(v10.authUserEntity);
|
||||||
},
|
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
|
||||||
from10To11: (m, v11) async {
|
await m.alterTable(TableMigration(v10.userEntity));
|
||||||
await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_);
|
},
|
||||||
},
|
from10To11: (m, v11) async {
|
||||||
from11To12: (m, v12) async {
|
await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_);
|
||||||
final localToUTCMapping = {
|
},
|
||||||
v12.localAssetEntity: [v12.localAssetEntity.createdAt, v12.localAssetEntity.updatedAt],
|
from11To12: (m, v12) async {
|
||||||
v12.localAlbumEntity: [v12.localAlbumEntity.updatedAt],
|
final localToUTCMapping = {
|
||||||
};
|
v12.localAssetEntity: [v12.localAssetEntity.createdAt, v12.localAssetEntity.updatedAt],
|
||||||
|
v12.localAlbumEntity: [v12.localAlbumEntity.updatedAt],
|
||||||
|
};
|
||||||
|
|
||||||
for (final entry in localToUTCMapping.entries) {
|
for (final entry in localToUTCMapping.entries) {
|
||||||
final table = entry.key;
|
final table = entry.key;
|
||||||
await m.alterTable(
|
await m.alterTable(
|
||||||
TableMigration(
|
TableMigration(
|
||||||
table,
|
table,
|
||||||
columnTransformer: {
|
columnTransformer: {
|
||||||
for (final column in entry.value)
|
for (final column in entry.value)
|
||||||
column: column.modify(const DateTimeModifier.utc()).strftime('%Y-%m-%dT%H:%M:%fZ'),
|
column: column.modify(const DateTimeModifier.utc()).strftime('%Y-%m-%dT%H:%M:%fZ'),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
from12To13: (m, v13) async {
|
from12To13: (m, v13) async {
|
||||||
await m.create(v13.trashedLocalAssetEntity);
|
await m.create(v13.trashedLocalAssetEntity);
|
||||||
await m.createIndex(v13.idxTrashedLocalAssetChecksum);
|
await m.createIndex(v13.idxTrashedLocalAssetChecksum);
|
||||||
await m.createIndex(v13.idxTrashedLocalAssetAlbum);
|
await m.createIndex(v13.idxTrashedLocalAssetAlbum);
|
||||||
},
|
},
|
||||||
from13To14: (m, v14) async {
|
from13To14: (m, v14) async {
|
||||||
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.adjustmentTime);
|
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.adjustmentTime);
|
||||||
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude);
|
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude);
|
||||||
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude);
|
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude);
|
||||||
},
|
},
|
||||||
from14To15: (m, v15) async {
|
from14To15: (m, v15) async {
|
||||||
await m.alterTable(
|
await m.alterTable(
|
||||||
TableMigration(
|
TableMigration(
|
||||||
v15.trashedLocalAssetEntity,
|
v15.trashedLocalAssetEntity,
|
||||||
columnTransformer: {v15.trashedLocalAssetEntity.source: Constant(TrashOrigin.localSync.index)},
|
columnTransformer: {v15.trashedLocalAssetEntity.source: Constant(TrashOrigin.localSync.index)},
|
||||||
newColumns: [v15.trashedLocalAssetEntity.source],
|
newColumns: [v15.trashedLocalAssetEntity.source],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
from15To16: (m, v16) async {
|
from15To16: (m, v16) async {
|
||||||
// Add i_cloud_id to local and remote asset tables
|
// Add i_cloud_id to local and remote asset tables
|
||||||
await m.addColumn(v16.localAssetEntity, v16.localAssetEntity.iCloudId);
|
await m.addColumn(v16.localAssetEntity, v16.localAssetEntity.iCloudId);
|
||||||
await m.createIndex(v16.idxLocalAssetCloudId);
|
await m.createIndex(v16.idxLocalAssetCloudId);
|
||||||
await m.createTable(v16.remoteAssetCloudIdEntity);
|
await m.createTable(v16.remoteAssetCloudIdEntity);
|
||||||
},
|
},
|
||||||
from16To17: (m, v17) async {
|
from16To17: (m, v17) async {
|
||||||
await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited);
|
await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited);
|
||||||
},
|
},
|
||||||
from17To18: (m, v18) async {
|
from17To18: (m, v18) async {
|
||||||
await m.createIndex(v18.idxRemoteAssetCloudId);
|
await m.createIndex(v18.idxRemoteAssetCloudId);
|
||||||
},
|
},
|
||||||
from18To19: (m, v19) async {
|
from18To19: (m, v19) async {
|
||||||
await m.createIndex(v19.idxAssetFacePersonId);
|
await m.createIndex(v19.idxAssetFacePersonId);
|
||||||
await m.createIndex(v19.idxAssetFaceAssetId);
|
await m.createIndex(v19.idxAssetFaceAssetId);
|
||||||
await m.createIndex(v19.idxLocalAlbumAssetAlbumAsset);
|
await m.createIndex(v19.idxLocalAlbumAssetAlbumAsset);
|
||||||
await m.createIndex(v19.idxPartnerSharedWithId);
|
await m.createIndex(v19.idxPartnerSharedWithId);
|
||||||
await m.createIndex(v19.idxPersonOwnerId);
|
await m.createIndex(v19.idxPersonOwnerId);
|
||||||
await m.createIndex(v19.idxRemoteAlbumOwnerId);
|
await m.createIndex(v19.idxRemoteAlbumOwnerId);
|
||||||
await m.createIndex(v19.idxRemoteAlbumAssetAlbumAsset);
|
await m.createIndex(v19.idxRemoteAlbumAssetAlbumAsset);
|
||||||
await m.createIndex(v19.idxRemoteAssetStackId);
|
await m.createIndex(v19.idxRemoteAssetStackId);
|
||||||
await m.createIndex(v19.idxRemoteAssetLocalDateTimeDay);
|
await m.createIndex(v19.idxRemoteAssetLocalDateTimeDay);
|
||||||
await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth);
|
await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth);
|
||||||
await m.createIndex(v19.idxStackPrimaryAssetId);
|
await m.createIndex(v19.idxStackPrimaryAssetId);
|
||||||
},
|
},
|
||||||
from19To20: (m, v20) async {
|
from19To20: (m, v20) async {
|
||||||
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
|
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
|
||||||
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
|
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
|
||||||
},
|
},
|
||||||
from20To21: (m, v21) async {
|
from20To21: (m, v21) async {
|
||||||
await m.addColumn(v21.localAssetEntity, v21.localAssetEntity.playbackStyle);
|
await m.addColumn(v21.localAssetEntity, v21.localAssetEntity.playbackStyle);
|
||||||
await m.addColumn(v21.trashedLocalAssetEntity, v21.trashedLocalAssetEntity.playbackStyle);
|
await m.addColumn(v21.trashedLocalAssetEntity, v21.trashedLocalAssetEntity.playbackStyle);
|
||||||
},
|
},
|
||||||
from21To22: (m, v22) async {
|
from21To22: (m, v22) async {
|
||||||
await m.createTable(v22.assetEditEntity);
|
await m.createTable(v22.assetEditEntity);
|
||||||
await m.createIndex(v22.idxAssetEditAssetId);
|
await m.createIndex(v22.idxAssetEditAssetId);
|
||||||
},
|
},
|
||||||
from22To23: (m, v23) async {
|
from22To23: (m, v23) async {
|
||||||
await m.renameColumn(v23.localAssetEntity, 'duration_in_seconds', v23.localAssetEntity.durationMs);
|
await m.renameColumn(v23.localAssetEntity, 'duration_in_seconds', v23.localAssetEntity.durationMs);
|
||||||
await m.renameColumn(v23.remoteAssetEntity, 'duration_in_seconds', v23.remoteAssetEntity.durationMs);
|
await m.renameColumn(v23.remoteAssetEntity, 'duration_in_seconds', v23.remoteAssetEntity.durationMs);
|
||||||
await m.renameColumn(
|
await m.renameColumn(
|
||||||
v23.trashedLocalAssetEntity,
|
v23.trashedLocalAssetEntity,
|
||||||
'duration_in_seconds',
|
'duration_in_seconds',
|
||||||
v23.trashedLocalAssetEntity.durationMs,
|
v23.trashedLocalAssetEntity.durationMs,
|
||||||
);
|
);
|
||||||
|
|
||||||
await localAssetEntity.update().write(
|
await localAssetEntity.update().write(
|
||||||
LocalAssetEntityCompanion.custom(durationMs: v23.localAssetEntity.durationMs * const Constant(1000)),
|
LocalAssetEntityCompanion.custom(durationMs: v23.localAssetEntity.durationMs * const Constant(1000)),
|
||||||
);
|
);
|
||||||
await remoteAssetEntity.update().write(
|
await remoteAssetEntity.update().write(
|
||||||
RemoteAssetEntityCompanion.custom(durationMs: v23.remoteAssetEntity.durationMs * const Constant(1000)),
|
RemoteAssetEntityCompanion.custom(
|
||||||
);
|
durationMs: v23.remoteAssetEntity.durationMs * const Constant(1000),
|
||||||
await trashedLocalAssetEntity.update().write(
|
),
|
||||||
TrashedLocalAssetEntityCompanion.custom(
|
);
|
||||||
durationMs: v23.trashedLocalAssetEntity.durationMs * const Constant(1000),
|
await trashedLocalAssetEntity.update().write(
|
||||||
),
|
TrashedLocalAssetEntityCompanion.custom(
|
||||||
);
|
durationMs: v23.trashedLocalAssetEntity.durationMs * const Constant(1000),
|
||||||
},
|
),
|
||||||
from23To24: (m, v24) async {
|
);
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
|
},
|
||||||
await m.alterTable(TableMigration(v24.remoteAlbumEntity));
|
from23To24: (m, v24) async {
|
||||||
},
|
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
|
||||||
from24To25: (m, v25) async {
|
await m.alterTable(TableMigration(v24.remoteAlbumEntity));
|
||||||
await m.createTable(v25.metadata);
|
},
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum');
|
from24To25: (m, v25) async {
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day');
|
await m.createTable(v25.metadata);
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month');
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum');
|
||||||
await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated);
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day');
|
||||||
await m.createIndex(v25.idxRemoteExifCity);
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month');
|
||||||
await m.createIndex(v25.idxAssetFaceVisiblePerson);
|
await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated);
|
||||||
},
|
await m.createIndex(v25.idxRemoteExifCity);
|
||||||
from25To26: (m, v26) async {
|
await m.createIndex(v25.idxAssetFaceVisiblePerson);
|
||||||
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
|
},
|
||||||
},
|
from25To26: (m, v26) async {
|
||||||
from26To27: (m, v27) async {
|
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
|
||||||
await customStatement('ALTER TABLE metadata RENAME TO settings');
|
},
|
||||||
},
|
from26To27: (m, v27) async {
|
||||||
from27To28: (m, v28) async {
|
await customStatement('ALTER TABLE metadata RENAME TO settings');
|
||||||
await m.createIndex(v28.idxLocalAssetCreatedAt);
|
},
|
||||||
},
|
from27To28: (m, v28) async {
|
||||||
from28To29: (m, v29) async {
|
await m.createIndex(v28.idxLocalAssetCreatedAt);
|
||||||
await m.createTable(v29.assetOcrEntity);
|
},
|
||||||
await m.createIndex(v29.idxAssetOcrAssetId);
|
from28To29: (m, v29) async {
|
||||||
},
|
await m.createTable(v29.assetOcrEntity);
|
||||||
from29To30: (m, v30) async {
|
await m.createIndex(v29.idxAssetOcrAssetId);
|
||||||
await m.alterTable(TableMigration(v30.settings));
|
},
|
||||||
},
|
from29To30: (m, v30) async {
|
||||||
),
|
await m.alterTable(TableMigration(v30.settings));
|
||||||
);
|
},
|
||||||
|
from30To31: (m, v31) async {
|
||||||
|
await m.createIndex(v31.idxRemoteAssetUploaded);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
// Fail if the migration broke foreign keys
|
// Fail if the migration broke foreign keys
|
||||||
final wrongFKs = await customSelect('PRAGMA foreign_key_check').get();
|
final wrongFKs = await customSelect('PRAGMA foreign_key_check').get();
|
||||||
assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}');
|
assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await customStatement('PRAGMA foreign_keys = ON;');
|
||||||
}
|
}
|
||||||
|
|
||||||
await customStatement('PRAGMA foreign_keys = ON;');
|
|
||||||
await optimize();
|
await optimize();
|
||||||
},
|
},
|
||||||
beforeOpen: (details) async {
|
beforeOpen: (details) async {
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
i2.idxRemoteAssetChecksum,
|
i2.idxRemoteAssetChecksum,
|
||||||
i2.idxRemoteAssetStackId,
|
i2.idxRemoteAssetStackId,
|
||||||
i2.idxRemoteAssetOwnerVisibilityDeletedCreated,
|
i2.idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||||
|
i2.idxRemoteAssetUploaded,
|
||||||
authUserEntity,
|
authUserEntity,
|
||||||
userMetadataEntity,
|
userMetadataEntity,
|
||||||
partnerEntity,
|
partnerEntity,
|
||||||
|
|||||||
@@ -15920,6 +15920,592 @@ i1.GeneratedColumn<String> _column_224(String aliasedName) =>
|
|||||||
type: i1.DriftSqlType.string,
|
type: i1.DriftSqlType.string,
|
||||||
$customConstraints: 'NULL',
|
$customConstraints: 'NULL',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema31 extends i0.VersionedSchema {
|
||||||
|
Schema31({required super.database}) : super(version: 31);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
idxLocalAlbumAssetAlbumAsset,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
idxLocalAssetCloudId,
|
||||||
|
idxLocalAssetCreatedAt,
|
||||||
|
idxStackPrimaryAssetId,
|
||||||
|
uQRemoteAssetsOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
idxRemoteAssetStackId,
|
||||||
|
idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||||
|
idxRemoteAssetUploaded,
|
||||||
|
authUserEntity,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
remoteAssetCloudIdEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
personEntity,
|
||||||
|
assetFaceEntity,
|
||||||
|
storeEntity,
|
||||||
|
trashedLocalAssetEntity,
|
||||||
|
assetEditEntity,
|
||||||
|
settings,
|
||||||
|
assetOcrEntity,
|
||||||
|
idxPartnerSharedWithId,
|
||||||
|
idxLatLng,
|
||||||
|
idxRemoteExifCity,
|
||||||
|
idxRemoteAlbumAssetAlbumAsset,
|
||||||
|
idxRemoteAssetCloudId,
|
||||||
|
idxPersonOwnerId,
|
||||||
|
idxAssetFacePersonId,
|
||||||
|
idxAssetFaceAssetId,
|
||||||
|
idxAssetFaceVisiblePerson,
|
||||||
|
idxTrashedLocalAssetChecksum,
|
||||||
|
idxTrashedLocalAssetAlbum,
|
||||||
|
idxAssetEditAssetId,
|
||||||
|
idxAssetOcrAssetId,
|
||||||
|
];
|
||||||
|
late final Shape33 userEntity = Shape33(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_109,
|
||||||
|
_column_110,
|
||||||
|
_column_111,
|
||||||
|
_column_112,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape50 remoteAssetEntity = Shape50(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_108,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_107,
|
||||||
|
_column_119,
|
||||||
|
_column_120,
|
||||||
|
_column_121,
|
||||||
|
_column_122,
|
||||||
|
_column_123,
|
||||||
|
_column_124,
|
||||||
|
_column_212,
|
||||||
|
_column_125,
|
||||||
|
_column_126,
|
||||||
|
_column_127,
|
||||||
|
_column_128,
|
||||||
|
_column_129,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape35 stackEntity = Shape35(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_121,
|
||||||
|
_column_130,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape36 localAssetEntity = Shape36(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_108,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_107,
|
||||||
|
_column_131,
|
||||||
|
_column_120,
|
||||||
|
_column_132,
|
||||||
|
_column_133,
|
||||||
|
_column_134,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
_column_137,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape48 remoteAlbumEntity = Shape48(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_138,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_139,
|
||||||
|
_column_140,
|
||||||
|
_column_141,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape38 localAlbumEntity = Shape38(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_115,
|
||||||
|
_column_142,
|
||||||
|
_column_143,
|
||||||
|
_column_144,
|
||||||
|
_column_145,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape39 localAlbumAssetEntity = Shape39(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_146, _column_147, _column_145],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||||
|
'idx_local_album_asset_album_asset',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||||
|
'idx_local_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetCreatedAt = i1.Index(
|
||||||
|
'idx_local_asset_created_at',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
|
||||||
|
);
|
||||||
|
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||||
|
'idx_stack_primary_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_library_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||||
|
'idx_remote_asset_stack_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
|
||||||
|
'idx_remote_asset_owner_visibility_deleted_created',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetUploaded = i1.Index(
|
||||||
|
'idx_remote_asset_uploaded',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)',
|
||||||
|
);
|
||||||
|
late final Shape40 authUserEntity = Shape40(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'auth_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_109,
|
||||||
|
_column_148,
|
||||||
|
_column_110,
|
||||||
|
_column_111,
|
||||||
|
_column_149,
|
||||||
|
_column_150,
|
||||||
|
_column_151,
|
||||||
|
_column_152,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape4 userMetadataEntity = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_metadata_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||||
|
columns: [_column_153, _column_154, _column_155],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape41 partnerEntity = Shape41(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||||
|
columns: [_column_156, _column_157, _column_158],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape42 remoteExifEntity = Shape42(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_159,
|
||||||
|
_column_160,
|
||||||
|
_column_161,
|
||||||
|
_column_162,
|
||||||
|
_column_163,
|
||||||
|
_column_164,
|
||||||
|
_column_117,
|
||||||
|
_column_116,
|
||||||
|
_column_165,
|
||||||
|
_column_166,
|
||||||
|
_column_167,
|
||||||
|
_column_168,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
_column_169,
|
||||||
|
_column_170,
|
||||||
|
_column_171,
|
||||||
|
_column_172,
|
||||||
|
_column_173,
|
||||||
|
_column_174,
|
||||||
|
_column_175,
|
||||||
|
_column_176,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_159, _column_177],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||||
|
columns: [_column_177, _column_153, _column_178],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape43 remoteAssetCloudIdEntity = Shape43(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_cloud_id_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_159,
|
||||||
|
_column_179,
|
||||||
|
_column_180,
|
||||||
|
_column_134,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape44 memoryEntity = Shape44(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_124,
|
||||||
|
_column_121,
|
||||||
|
_column_113,
|
||||||
|
_column_181,
|
||||||
|
_column_182,
|
||||||
|
_column_183,
|
||||||
|
_column_184,
|
||||||
|
_column_185,
|
||||||
|
_column_186,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape12 memoryAssetEntity = Shape12(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||||
|
columns: [_column_159, _column_187],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape45 personEntity = Shape45(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'person_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_121,
|
||||||
|
_column_108,
|
||||||
|
_column_188,
|
||||||
|
_column_189,
|
||||||
|
_column_190,
|
||||||
|
_column_191,
|
||||||
|
_column_192,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape46 assetFaceEntity = Shape46(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_face_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_159,
|
||||||
|
_column_193,
|
||||||
|
_column_194,
|
||||||
|
_column_195,
|
||||||
|
_column_196,
|
||||||
|
_column_197,
|
||||||
|
_column_198,
|
||||||
|
_column_199,
|
||||||
|
_column_200,
|
||||||
|
_column_201,
|
||||||
|
_column_124,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape18 storeEntity = Shape18(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'store_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_202, _column_203, _column_204],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape47 trashedLocalAssetEntity = Shape47(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'trashed_local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_108,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_107,
|
||||||
|
_column_205,
|
||||||
|
_column_131,
|
||||||
|
_column_120,
|
||||||
|
_column_132,
|
||||||
|
_column_206,
|
||||||
|
_column_137,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape32 assetEditEntity = Shape32(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_edit_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_159,
|
||||||
|
_column_207,
|
||||||
|
_column_208,
|
||||||
|
_column_209,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape49 settings = Shape49(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'settings',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY("key")'],
|
||||||
|
columns: [_column_210, _column_224, _column_115],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape51 assetOcrEntity = Shape51(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_ocr_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_159,
|
||||||
|
_column_213,
|
||||||
|
_column_214,
|
||||||
|
_column_215,
|
||||||
|
_column_216,
|
||||||
|
_column_217,
|
||||||
|
_column_218,
|
||||||
|
_column_219,
|
||||||
|
_column_220,
|
||||||
|
_column_221,
|
||||||
|
_column_222,
|
||||||
|
_column_223,
|
||||||
|
_column_201,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||||
|
'idx_partner_shared_with_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLatLng = i1.Index(
|
||||||
|
'idx_lat_lng',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteExifCity = i1.Index(
|
||||||
|
'idx_remote_exif_city',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||||
|
'idx_remote_album_asset_album_asset',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||||
|
'idx_remote_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxPersonOwnerId = i1.Index(
|
||||||
|
'idx_person_owner_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||||
|
'idx_asset_face_person_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||||
|
'idx_asset_face_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
|
||||||
|
'idx_asset_face_visible_person',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_album',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetEditAssetId = i1.Index(
|
||||||
|
'idx_asset_edit_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetOcrAssetId = i1.Index(
|
||||||
|
'idx_asset_ocr_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
@@ -15950,6 +16536,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
||||||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -16098,6 +16685,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from29To30(migrator, schema);
|
await from29To30(migrator, schema);
|
||||||
return 30;
|
return 30;
|
||||||
|
case 30:
|
||||||
|
final schema = Schema31(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from30To31(migrator, schema);
|
||||||
|
return 31;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -16134,6 +16726,7 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
||||||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
@@ -16165,5 +16758,6 @@ i1.OnUpgrade stepByStep({
|
|||||||
from27To28: from27To28,
|
from27To28: from27To28,
|
||||||
from28To29: from28To29,
|
from28To29: from28To29,
|
||||||
from29To30: from29To30,
|
from29To30: from29To30,
|
||||||
|
from30To31: from30To31,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class StorageRepository {
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(agg23): Unify these methods
|
||||||
Future<File?> getMotionFileForAsset(LocalAsset asset) async {
|
Future<File?> getMotionFileForAsset(LocalAsset asset) async {
|
||||||
File? file;
|
File? file;
|
||||||
final log = Logger('StorageRepository');
|
final log = Logger('StorageRepository');
|
||||||
|
|||||||
@@ -509,9 +509,21 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
return query.map((row) => row.toDto()).get();
|
return query.map((row) => row.toDto()).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineQuery map(List<String> userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => (
|
/// Creates a geographic map query that can dynamically filter on changing [TimelineMapOptions]
|
||||||
bucketSource: () => _watchMapBucket(userIds, options, groupBy: groupBy),
|
/// (most notably the active map bounds)
|
||||||
assetSource: (offset, count) => _getMapBucketAssets(userIds, options, offset: offset, count: count),
|
TimelineQuery geographicMap(
|
||||||
|
List<String> userIds,
|
||||||
|
TimelineMapOptions Function() currentOptions,
|
||||||
|
Stream<TimelineMapOptions> optionsStream,
|
||||||
|
GroupAssetsBy groupBy,
|
||||||
|
) => (
|
||||||
|
bucketSource: () => Stream.value(currentOptions())
|
||||||
|
.followedBy(optionsStream)
|
||||||
|
.switchMap(
|
||||||
|
// Any error would kill the stream for all options; make sure the stream stays alive
|
||||||
|
(options) => _watchMapBucket(userIds, options, groupBy: groupBy).handleError((_) {}),
|
||||||
|
),
|
||||||
|
assetSource: (offset, count) => _getMapBucketAssets(userIds, currentOptions(), offset: offset, count: count),
|
||||||
origin: TimelineOrigin.map,
|
origin: TimelineOrigin.map,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -614,7 +626,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
return (
|
return (
|
||||||
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy, sortBy: sortBy),
|
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy, sortBy: sortBy),
|
||||||
assetSource: (offset, count) =>
|
assetSource: (offset, count) =>
|
||||||
_getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal),
|
_getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal, sortBy: sortBy),
|
||||||
origin: origin,
|
origin: origin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -651,6 +663,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
required int offset,
|
required int offset,
|
||||||
required int count,
|
required int count,
|
||||||
bool joinLocal = false,
|
bool joinLocal = false,
|
||||||
|
SortAssetsBy sortBy = SortAssetsBy.taken,
|
||||||
}) {
|
}) {
|
||||||
if (joinLocal) {
|
if (joinLocal) {
|
||||||
final query =
|
final query =
|
||||||
@@ -663,7 +676,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
])
|
])
|
||||||
..addColumns([_db.localAssetEntity.id])
|
..addColumns([_db.localAssetEntity.id])
|
||||||
..where(filter(_db.remoteAssetEntity))
|
..where(filter(_db.remoteAssetEntity))
|
||||||
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
..orderBy([
|
||||||
|
OrderingTerm.desc(
|
||||||
|
sortBy == SortAssetsBy.uploaded ? _db.remoteAssetEntity.uploadedAt : _db.remoteAssetEntity.createdAt,
|
||||||
|
),
|
||||||
|
])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
return query
|
return query
|
||||||
@@ -672,7 +689,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
} else {
|
} else {
|
||||||
final query = _db.remoteAssetEntity.select()
|
final query = _db.remoteAssetEntity.select()
|
||||||
..where(filter)
|
..where(filter)
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
..orderBy([(row) => OrderingTerm.desc(sortBy == SortAssetsBy.uploaded ? row.uploadedAt : row.createdAt)])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
return query.map((row) => row.toDto()).get();
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
|
|||||||
_PropertyItem(label: 'Height', value: asset.height?.toString()),
|
_PropertyItem(label: 'Height', value: asset.height?.toString()),
|
||||||
_PropertyItem(label: 'Duration', value: asset.durationMs != null ? '${asset.durationMs} ms' : null),
|
_PropertyItem(label: 'Duration', value: asset.durationMs != null ? '${asset.durationMs} ms' : null),
|
||||||
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
|
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
|
||||||
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
|
|
||||||
_PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()),
|
_PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -151,6 +150,7 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
|
|||||||
_PropertyItem(label: 'Thumb Hash', value: asset.thumbHash),
|
_PropertyItem(label: 'Thumb Hash', value: asset.thumbHash),
|
||||||
_PropertyItem(label: 'Visibility', value: asset.visibility.toString()),
|
_PropertyItem(label: 'Visibility', value: asset.visibility.toString()),
|
||||||
_PropertyItem(label: 'Stack ID', value: asset.stackId),
|
_PropertyItem(label: 'Stack ID', value: asset.stackId),
|
||||||
|
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
|
||||||
];
|
];
|
||||||
|
|
||||||
properties.insertAll(4, additionalProps);
|
properties.insertAll(4, additionalProps);
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AspectRatioButton extends StatelessWidget {
|
class _AspectRatioButton extends StatelessWidget {
|
||||||
final AspectRatioPreset ratio;
|
final CropAspectRatio ratio;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
@@ -162,15 +162,16 @@ class _AspectRatioButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final color = isSelected ? context.primaryColor : context.themeData.iconTheme.color;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: 36,
|
iconSize: 36,
|
||||||
icon: Transform.rotate(
|
icon: ratio.ratio != null
|
||||||
angle: ratio.iconRotated ? pi / 2 : 0,
|
? _AspectRatioRect(ratio: ratio.ratio!, color: color)
|
||||||
child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color),
|
: Icon(ratio.icon, color: color),
|
||||||
),
|
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
Text(ratio.label, style: context.textTheme.displayMedium),
|
Text(ratio.label, style: context.textTheme.displayMedium),
|
||||||
@@ -179,6 +180,32 @@ class _AspectRatioButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AspectRatioRect extends StatelessWidget {
|
||||||
|
final double ratio;
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
const _AspectRatioRect({required this.ratio, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: Center(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: ratio,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: color ?? Colors.transparent, width: 3),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _AspectRatioSelector extends ConsumerWidget {
|
class _AspectRatioSelector extends ConsumerWidget {
|
||||||
const _AspectRatioSelector();
|
const _AspectRatioSelector();
|
||||||
|
|
||||||
@@ -187,22 +214,16 @@ class _AspectRatioSelector extends ConsumerWidget {
|
|||||||
final editorState = ref.watch(editorStateProvider);
|
final editorState = ref.watch(editorStateProvider);
|
||||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||||
|
|
||||||
// the whole crop view is rotated, so we need to swap the aspect ratio when the rotation is 90 or 270 degrees
|
|
||||||
double? selectedAspectRatio = editorState.aspectRatio;
|
|
||||||
if (editorState.rotationAngle % 180 != 0 && selectedAspectRatio != null) {
|
|
||||||
selectedAspectRatio = 1 / selectedAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: AspectRatioPreset.values.map((entry) {
|
children: aspectRatioPresets.map((entry) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: _AspectRatioButton(
|
child: _AspectRatioButton(
|
||||||
ratio: entry,
|
ratio: entry,
|
||||||
isSelected: selectedAspectRatio == entry.ratio,
|
isSelected: editorState.aspectRatio == entry,
|
||||||
onPressed: () => editorNotifier.setAspectRatio(entry.ratio),
|
onPressed: () => editorNotifier.setAspectRatio(entry),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -357,8 +378,22 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
|||||||
final editorState = ref.watch(editorStateProvider);
|
final editorState = ref.watch(editorStateProvider);
|
||||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||||
|
|
||||||
ref.listen(editorStateProvider, (_, current) {
|
ref.listen(editorStateProvider, (previous, current) {
|
||||||
cropController.aspectRatio = current.aspectRatio;
|
// Only re-apply the aspect ratio when it changes, otherwise the crop rect will shrink on every rotation
|
||||||
|
if (previous?.aspectRatio != current.aspectRatio) {
|
||||||
|
double? ratio;
|
||||||
|
|
||||||
|
ratio = switch (current.aspectRatio) {
|
||||||
|
CropAspectRatio.original => current.originalWidth / current.originalHeight,
|
||||||
|
_ => current.aspectRatio.ratio,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (current.rotationAngle % 180 != 0) {
|
||||||
|
ratio = ratio != null ? 1 / ratio : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cropController.aspectRatio = ratio;
|
||||||
|
}
|
||||||
|
|
||||||
if (cropController.crop != current.crop) {
|
if (cropController.crop != current.crop) {
|
||||||
cropController.crop = current.crop;
|
cropController.crop = current.crop;
|
||||||
@@ -386,7 +421,9 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
|||||||
1.0,
|
1.0,
|
||||||
1.0,
|
1.0,
|
||||||
),
|
),
|
||||||
child: Container(
|
child: AnimatedContainer(
|
||||||
|
duration: editorState.animationDuration,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
||||||
height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/aspect_ratios.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/utils/editor.utils.dart';
|
import 'package:immich_mobile/utils/editor.utils.dart';
|
||||||
@@ -60,13 +61,8 @@ class EditorProvider extends Notifier<EditorState> {
|
|||||||
state = state.copyWith(crop: crop, hasUnsavedEdits: true);
|
state = state.copyWith(crop: crop, hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAspectRatio(double? aspectRatio) {
|
void setAspectRatio(CropAspectRatio preset) {
|
||||||
if (aspectRatio != null && state.rotationAngle % 180 != 0) {
|
state = state.copyWith(aspectRatio: preset, hasUnsavedEdits: true);
|
||||||
// When rotated 90 or 270 degrees, swap width and height for aspect ratio calculations
|
|
||||||
aspectRatio = 1 / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(aspectRatio: aspectRatio);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetEdits() {
|
void resetEdits() {
|
||||||
@@ -76,19 +72,19 @@ class EditorProvider extends Notifier<EditorState> {
|
|||||||
flipHorizontal: false,
|
flipHorizontal: false,
|
||||||
flipVertical: false,
|
flipVertical: false,
|
||||||
crop: const Rect.fromLTRB(0, 0, 1, 1),
|
crop: const Rect.fromLTRB(0, 0, 1, 1),
|
||||||
aspectRatio: null,
|
aspectRatio: CropAspectRatio.free,
|
||||||
hasUnsavedEdits: true,
|
hasUnsavedEdits: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void rotateCCW() {
|
void rotateCCW() {
|
||||||
_animateRotation(state.rotationAngle - 90);
|
_animateRotation(state.rotationAngle - 90);
|
||||||
state = state.copyWith(hasUnsavedEdits: true);
|
state = state.copyWith(aspectRatio: state.aspectRatio.flipped, hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void rotateCW() {
|
void rotateCW() {
|
||||||
_animateRotation(state.rotationAngle + 90);
|
_animateRotation(state.rotationAngle + 90);
|
||||||
state = state.copyWith(hasUnsavedEdits: true);
|
state = state.copyWith(aspectRatio: state.aspectRatio.flipped, hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void flipHorizontally() {
|
void flipHorizontally() {
|
||||||
@@ -117,7 +113,7 @@ class EditorState {
|
|||||||
final bool flipHorizontal;
|
final bool flipHorizontal;
|
||||||
final bool flipVertical;
|
final bool flipVertical;
|
||||||
final Rect crop;
|
final Rect crop;
|
||||||
final double? aspectRatio;
|
final CropAspectRatio aspectRatio;
|
||||||
|
|
||||||
final int originalWidth;
|
final int originalWidth;
|
||||||
final int originalHeight;
|
final int originalHeight;
|
||||||
@@ -132,7 +128,7 @@ class EditorState {
|
|||||||
bool? flipHorizontal,
|
bool? flipHorizontal,
|
||||||
bool? flipVertical,
|
bool? flipVertical,
|
||||||
Rect? crop,
|
Rect? crop,
|
||||||
this.aspectRatio,
|
CropAspectRatio? aspectRatio,
|
||||||
int? originalWidth,
|
int? originalWidth,
|
||||||
int? originalHeight,
|
int? originalHeight,
|
||||||
Duration? animationDuration,
|
Duration? animationDuration,
|
||||||
@@ -145,6 +141,7 @@ class EditorState {
|
|||||||
originalWidth = originalWidth ?? 0,
|
originalWidth = originalWidth ?? 0,
|
||||||
originalHeight = originalHeight ?? 0,
|
originalHeight = originalHeight ?? 0,
|
||||||
crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1),
|
crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1),
|
||||||
|
aspectRatio = aspectRatio ?? CropAspectRatio.free,
|
||||||
hasUnsavedEdits = hasUnsavedEdits ?? false;
|
hasUnsavedEdits = hasUnsavedEdits ?? false;
|
||||||
|
|
||||||
EditorState copyWith({
|
EditorState copyWith({
|
||||||
@@ -152,7 +149,7 @@ class EditorState {
|
|||||||
int? rotationAngle,
|
int? rotationAngle,
|
||||||
bool? flipHorizontal,
|
bool? flipHorizontal,
|
||||||
bool? flipVertical,
|
bool? flipVertical,
|
||||||
double? aspectRatio = double.infinity,
|
CropAspectRatio? aspectRatio,
|
||||||
int? originalWidth,
|
int? originalWidth,
|
||||||
int? originalHeight,
|
int? originalHeight,
|
||||||
Duration? animationDuration,
|
Duration? animationDuration,
|
||||||
@@ -164,7 +161,7 @@ class EditorState {
|
|||||||
rotationAngle: rotationAngle ?? this.rotationAngle,
|
rotationAngle: rotationAngle ?? this.rotationAngle,
|
||||||
flipHorizontal: flipHorizontal ?? this.flipHorizontal,
|
flipHorizontal: flipHorizontal ?? this.flipHorizontal,
|
||||||
flipVertical: flipVertical ?? this.flipVertical,
|
flipVertical: flipVertical ?? this.flipVertical,
|
||||||
aspectRatio: aspectRatio == double.infinity ? this.aspectRatio : aspectRatio,
|
aspectRatio: aspectRatio ?? this.aspectRatio,
|
||||||
animationDuration: animationDuration ?? this.animationDuration,
|
animationDuration: animationDuration ?? this.animationDuration,
|
||||||
originalWidth: originalWidth ?? this.originalWidth,
|
originalWidth: originalWidth ?? this.originalWidth,
|
||||||
originalHeight: originalHeight ?? this.originalHeight,
|
originalHeight: originalHeight ?? this.originalHeight,
|
||||||
|
|||||||
@@ -159,8 +159,6 @@ class ShareActionButton extends ConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -173,7 +171,6 @@ class ShareActionButton extends ConsumerWidget {
|
|||||||
buildContext.pop();
|
buildContext.pop();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show download progress with a "Preparing" message
|
|
||||||
return preparingDialog;
|
return preparingDialog;
|
||||||
},
|
},
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
|
|||||||
@@ -122,9 +122,14 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) {
|
// Attempt to retrieve LocalAsset, falling back to remote if it cannot be found
|
||||||
final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!;
|
final localAsset = await _localPlaybackAsset(videoAsset);
|
||||||
final file = await StorageRepository().getFileForAsset(id);
|
|
||||||
|
if (localAsset != null) {
|
||||||
|
final file = localAsset.isMotionPhoto
|
||||||
|
? await StorageRepository().getMotionFileForAsset(localAsset)
|
||||||
|
: await StorageRepository().getFileForAsset(localAsset.id);
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -141,14 +146,13 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final remoteId = (videoAsset as RemoteAsset).id;
|
final remoteAsset = videoAsset as RemoteAsset;
|
||||||
|
|
||||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||||
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
|
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
|
||||||
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
||||||
final String videoUrl = videoAsset.livePhotoVideoId != null
|
final String assetId = remoteAsset.livePhotoVideoId ?? remoteAsset.id;
|
||||||
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl'
|
final String videoUrl = '$serverEndpoint/assets/$assetId/$postfixUrl';
|
||||||
: '$serverEndpoint/assets/$remoteId/$postfixUrl';
|
|
||||||
|
|
||||||
return VideoSource.init(path: videoUrl, type: VideoSourceType.network, headers: ApiService.getRequestHeaders());
|
return VideoSource.init(path: videoUrl, type: VideoSourceType.network, headers: ApiService.getRequestHeaders());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -157,6 +161,43 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<LocalAsset?> _localPlaybackAsset(BaseAsset baseAsset) async {
|
||||||
|
if (!baseAsset.hasLocal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalAsset? localAsset;
|
||||||
|
|
||||||
|
if (baseAsset is LocalAsset) {
|
||||||
|
localAsset = baseAsset;
|
||||||
|
} else {
|
||||||
|
final localId = (baseAsset as RemoteAsset).localId;
|
||||||
|
localAsset = localId != null ? await ref.read(assetServiceProvider).getLocalAsset(localId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localAsset == null) {
|
||||||
|
_log.severe(
|
||||||
|
'Invariant violation: asset ${baseAsset.name} (${baseAsset.localId}) is marked `hasLocal` but local asset could not be retrieved',
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clients (local) may not correctly recognize a given asset as a motion photo. This allows for a scenario where both remote and local
|
||||||
|
// have the same asset (hash), but only the remote properly recognizes it as a motion asset
|
||||||
|
// If this scenario occurs, fall back to using the remote asset
|
||||||
|
if (baseAsset.isMotionPhoto && !localAsset.isMotionPhoto) {
|
||||||
|
// Platform mismatch for motion photo, use remote instead
|
||||||
|
_log.warning(
|
||||||
|
'Mismatched local and remote motion states on ${baseAsset.name} (${baseAsset.localId}), local = ${localAsset.isMotionPhoto}, remote = ${baseAsset.isMotionPhoto}',
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return localAsset;
|
||||||
|
}
|
||||||
|
|
||||||
void _onPlaybackReady() async {
|
void _onPlaybackReady() async {
|
||||||
if (!mounted || !widget.isCurrent) {
|
if (!mounted || !widget.isCurrent) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class _ScopedMapTimeline extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// TODO: this causes the timeline to switch to flicker to "loading" state and back. This is both janky and inefficient.
|
|
||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
timelineServiceProvider.overrideWith((ref) {
|
timelineServiceProvider.overrideWith((ref) {
|
||||||
@@ -44,13 +43,16 @@ class _ScopedMapTimeline extends StatelessWidget {
|
|||||||
throw Exception('User must be logged in to access archive');
|
throw Exception('User must be logged in to access archive');
|
||||||
}
|
}
|
||||||
|
|
||||||
final users = ref.watch(mapStateProvider).withPartners
|
final withPartners = ref.watch(mapStateProvider.select((s) => s.withPartners));
|
||||||
? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id]
|
final users = withPartners ? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id] : [user.id];
|
||||||
: [user.id];
|
|
||||||
|
|
||||||
final timelineService = ref
|
final timelineService = ref
|
||||||
.watch(timelineFactoryProvider)
|
.watch(timelineFactoryProvider)
|
||||||
.map(users, ref.watch(mapStateProvider).toOptions());
|
.geographicMap(
|
||||||
|
users,
|
||||||
|
() => ref.read(mapStateProvider).toOptions(),
|
||||||
|
ref.read(mapStateProvider.notifier).optionsStream,
|
||||||
|
);
|
||||||
ref.onDispose(timelineService.dispose);
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -297,8 +297,7 @@ class _AssetTypeIcons extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final remoteAsset = asset is RemoteAsset ? asset as RemoteAsset : null;
|
final isLivePhoto = asset.isMotionPhoto;
|
||||||
final isLivePhoto = remoteAsset?.livePhotoVideoId != null;
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||||
@@ -63,11 +65,16 @@ class MapState {
|
|||||||
class MapStateNotifier extends Notifier<MapState> {
|
class MapStateNotifier extends Notifier<MapState> {
|
||||||
MapStateNotifier();
|
MapStateNotifier();
|
||||||
|
|
||||||
|
final StreamController<TimelineMapOptions> _optionsController = StreamController.broadcast();
|
||||||
|
|
||||||
|
Stream<TimelineMapOptions> get optionsStream => _optionsController.stream;
|
||||||
|
|
||||||
bool setBounds(LatLngBounds bounds) {
|
bool setBounds(LatLngBounds bounds) {
|
||||||
if (state.bounds == bounds) {
|
if (state.bounds == bounds) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
state = state.copyWith(bounds: bounds);
|
state = state.copyWith(bounds: bounds);
|
||||||
|
_optionsController.add(state.toOptions());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +89,14 @@ class MapStateNotifier extends Notifier<MapState> {
|
|||||||
void switchFavoriteOnly(bool isFavoriteOnly) {
|
void switchFavoriteOnly(bool isFavoriteOnly) {
|
||||||
ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly);
|
ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly);
|
||||||
state = state.copyWith(onlyFavorites: isFavoriteOnly);
|
state = state.copyWith(onlyFavorites: isFavoriteOnly);
|
||||||
|
_optionsController.add(state.toOptions());
|
||||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
void switchIncludeArchived(bool isIncludeArchived) {
|
void switchIncludeArchived(bool isIncludeArchived) {
|
||||||
ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived);
|
ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived);
|
||||||
state = state.copyWith(includeArchived: isIncludeArchived);
|
state = state.copyWith(includeArchived: isIncludeArchived);
|
||||||
|
_optionsController.add(state.toOptions());
|
||||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,11 +109,13 @@ class MapStateNotifier extends Notifier<MapState> {
|
|||||||
void setRelativeTime(int relativeDays) {
|
void setRelativeTime(int relativeDays) {
|
||||||
ref.read(settingsProvider).write(.mapRelativeDate, relativeDays);
|
ref.read(settingsProvider).write(.mapRelativeDate, relativeDays);
|
||||||
state = state.copyWith(relativeDays: relativeDays);
|
state = state.copyWith(relativeDays: relativeDays);
|
||||||
|
_optionsController.add(state.toOptions());
|
||||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MapState build() {
|
MapState build() {
|
||||||
|
ref.onDispose(_optionsController.close);
|
||||||
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
|
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
|
||||||
return MapState(
|
return MapState(
|
||||||
themeMode: mapConfig.themeMode,
|
themeMode: mapConfig.themeMode,
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
ConsumerState createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBindingObserver {
|
||||||
late final ScrollController _scrollController;
|
late final ScrollController _scrollController;
|
||||||
StreamSubscription? _eventSubscription;
|
StreamSubscription? _eventSubscription;
|
||||||
|
|
||||||
@@ -153,6 +153,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_scrollController = ScrollController(onAttach: _restoreAssetPosition);
|
_scrollController = ScrollController(onAttach: _restoreAssetPosition);
|
||||||
_eventSubscription = EventStream.shared.listen(_onEvent);
|
_eventSubscription = EventStream.shared.listen(_onEvent);
|
||||||
|
|
||||||
@@ -178,17 +179,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture iOS status bar tap
|
||||||
|
@override
|
||||||
|
void handleStatusBarTap() => _scrollToTop();
|
||||||
|
|
||||||
void _onEvent(Event event) {
|
void _onEvent(Event event) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case ScrollToTopEvent():
|
case ScrollToTopEvent():
|
||||||
{
|
_scrollToTop();
|
||||||
final timelineState = ref.read(timelineStateProvider.notifier);
|
|
||||||
timelineState.setScrubbing(true);
|
|
||||||
_scrollController
|
|
||||||
.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut)
|
|
||||||
.whenComplete(() => timelineState.setScrubbing(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
case ScrollToDateEvent scrollToDateEvent:
|
case ScrollToDateEvent scrollToDateEvent:
|
||||||
_scrollToDate(scrollToDateEvent.date);
|
_scrollToDate(scrollToDateEvent.date);
|
||||||
case TimelineReloadEvent():
|
case TimelineReloadEvent():
|
||||||
@@ -246,11 +244,24 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
_eventSubscription?.cancel();
|
_eventSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scrollToTop() {
|
||||||
|
if (!_scrollController.hasClients) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineState = ref.read(timelineStateProvider.notifier);
|
||||||
|
timelineState.setScrubbing(true);
|
||||||
|
_scrollController
|
||||||
|
.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut)
|
||||||
|
.whenComplete(() => timelineState.setScrubbing(false));
|
||||||
|
}
|
||||||
|
|
||||||
void _scrollToDate(DateTime date) {
|
void _scrollToDate(DateTime date) {
|
||||||
final timelineState = ref.read(timelineStateProvider.notifier);
|
final timelineState = ref.read(timelineStateProvider.notifier);
|
||||||
final asyncSegments = ref.read(timelineSegmentProvider);
|
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||||
@@ -381,6 +392,9 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
child: PrimaryScrollController(
|
child: PrimaryScrollController(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
// This removes the built in Scaffold `handleStatusBarTap` implementation, preventing duplicate
|
||||||
|
// events when we provide our own
|
||||||
|
primary: false,
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
floatingActionButton: const DownloadStatusFloatingButton(),
|
floatingActionButton: const DownloadStatusFloatingButton(),
|
||||||
body: asyncSegments.widgetWhen(
|
body: asyncSegments.widgetWhen(
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:immich_mobile/repositories/upload.repository.dart';
|
|||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:openapi/api.dart' as api;
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
final backgroundUploadServiceProvider = Provider((ref) {
|
final backgroundUploadServiceProvider = Provider((ref) {
|
||||||
@@ -335,7 +336,8 @@ class BackgroundUploadService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final fields = {'livePhotoVideoId': livePhotoVideoId};
|
// Visibility hidden on upload to prevent the server from running regular jobs on the live photo asset
|
||||||
|
final fields = {'livePhotoVideoId': livePhotoVideoId, 'visibility': api.AssetVisibility.hidden.value};
|
||||||
|
|
||||||
final requiresWiFi = _shouldRequireWiFi(asset);
|
final requiresWiFi = _shouldRequireWiFi(asset);
|
||||||
final originalFileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
final originalFileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
|
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' hide AssetVisibility;
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
||||||
@@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
|||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||||
|
|
||||||
@@ -338,7 +339,8 @@ class ForegroundUploadService {
|
|||||||
final livePhotoResult = await _uploadRepository.uploadFile(
|
final livePhotoResult = await _uploadRepository.uploadFile(
|
||||||
file: livePhotoFile,
|
file: livePhotoFile,
|
||||||
originalFileName: livePhotoTitle,
|
originalFileName: livePhotoTitle,
|
||||||
fields: fields,
|
// Visibility hidden on upload to prevent the server from running regular jobs on the live photo asset
|
||||||
|
fields: {...fields, 'visibility': AssetVisibility.hidden.value},
|
||||||
cancelToken: cancelToken,
|
cancelToken: cancelToken,
|
||||||
onProgress: onProgress != null
|
onProgress: onProgress != null
|
||||||
? (bytes, totalBytes) => onProgress(asset.localId!, livePhotoTitle, bytes, totalBytes)
|
? (bytes, totalBytes) => onProgress(asset.localId!, livePhotoTitle, bytes, totalBytes)
|
||||||
|
|||||||
Generated
+1
@@ -452,6 +452,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
|
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
|
||||||
- [FoldersResponse](doc//FoldersResponse.md)
|
- [FoldersResponse](doc//FoldersResponse.md)
|
||||||
- [FoldersUpdate](doc//FoldersUpdate.md)
|
- [FoldersUpdate](doc//FoldersUpdate.md)
|
||||||
|
- [HlsVideoResolution](doc//HlsVideoResolution.md)
|
||||||
- [ImageFormat](doc//ImageFormat.md)
|
- [ImageFormat](doc//ImageFormat.md)
|
||||||
- [IntegrityReport](doc//IntegrityReport.md)
|
- [IntegrityReport](doc//IntegrityReport.md)
|
||||||
- [IntegrityReportResponseDto](doc//IntegrityReportResponseDto.md)
|
- [IntegrityReportResponseDto](doc//IntegrityReportResponseDto.md)
|
||||||
|
|||||||
Generated
+1
@@ -173,6 +173,7 @@ part 'model/face_dto.dart';
|
|||||||
part 'model/facial_recognition_config.dart';
|
part 'model/facial_recognition_config.dart';
|
||||||
part 'model/folders_response.dart';
|
part 'model/folders_response.dart';
|
||||||
part 'model/folders_update.dart';
|
part 'model/folders_update.dart';
|
||||||
|
part 'model/hls_video_resolution.dart';
|
||||||
part 'model/image_format.dart';
|
part 'model/image_format.dart';
|
||||||
part 'model/integrity_report.dart';
|
part 'model/integrity_report.dart';
|
||||||
part 'model/integrity_report_response_dto.dart';
|
part 'model/integrity_report_response_dto.dart';
|
||||||
|
|||||||
Generated
+2
@@ -391,6 +391,8 @@ class ApiClient {
|
|||||||
return FoldersResponse.fromJson(value);
|
return FoldersResponse.fromJson(value);
|
||||||
case 'FoldersUpdate':
|
case 'FoldersUpdate':
|
||||||
return FoldersUpdate.fromJson(value);
|
return FoldersUpdate.fromJson(value);
|
||||||
|
case 'HlsVideoResolution':
|
||||||
|
return HlsVideoResolutionTypeTransformer().decode(value);
|
||||||
case 'ImageFormat':
|
case 'ImageFormat':
|
||||||
return ImageFormatTypeTransformer().decode(value);
|
return ImageFormatTypeTransformer().decode(value);
|
||||||
case 'IntegrityReport':
|
case 'IntegrityReport':
|
||||||
|
|||||||
Generated
+3
@@ -106,6 +106,9 @@ String parameterToString(dynamic value) {
|
|||||||
if (value is Colorspace) {
|
if (value is Colorspace) {
|
||||||
return ColorspaceTypeTransformer().encode(value).toString();
|
return ColorspaceTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
if (value is HlsVideoResolution) {
|
||||||
|
return HlsVideoResolutionTypeTransformer().encode(value).toString();
|
||||||
|
}
|
||||||
if (value is ImageFormat) {
|
if (value is ImageFormat) {
|
||||||
return ImageFormatTypeTransformer().encode(value).toString();
|
return ImageFormatTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
/// HLS video resolution
|
||||||
|
class HlsVideoResolution {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const HlsVideoResolution._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value.toString();
|
||||||
|
|
||||||
|
int toJson() => value;
|
||||||
|
|
||||||
|
static const number480 = HlsVideoResolution._(480);
|
||||||
|
static const number720 = HlsVideoResolution._(720);
|
||||||
|
static const number1080 = HlsVideoResolution._(1080);
|
||||||
|
static const number1440 = HlsVideoResolution._(1440);
|
||||||
|
static const number2160 = HlsVideoResolution._(2160);
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][HlsVideoResolution].
|
||||||
|
static const values = <HlsVideoResolution>[
|
||||||
|
number480,
|
||||||
|
number720,
|
||||||
|
number1080,
|
||||||
|
number1440,
|
||||||
|
number2160,
|
||||||
|
];
|
||||||
|
|
||||||
|
static HlsVideoResolution? fromJson(dynamic value) => HlsVideoResolutionTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<HlsVideoResolution> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <HlsVideoResolution>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = HlsVideoResolution.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [HlsVideoResolution] to int,
|
||||||
|
/// and [decode] dynamic data back to [HlsVideoResolution].
|
||||||
|
class HlsVideoResolutionTypeTransformer {
|
||||||
|
factory HlsVideoResolutionTypeTransformer() => _instance ??= const HlsVideoResolutionTypeTransformer._();
|
||||||
|
|
||||||
|
const HlsVideoResolutionTypeTransformer._();
|
||||||
|
|
||||||
|
int encode(HlsVideoResolution data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a HlsVideoResolution.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
HlsVideoResolution? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case 480: return HlsVideoResolution.number480;
|
||||||
|
case 720: return HlsVideoResolution.number720;
|
||||||
|
case 1080: return HlsVideoResolution.number1080;
|
||||||
|
case 1440: return HlsVideoResolution.number1440;
|
||||||
|
case 2160: return HlsVideoResolution.number2160;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [HlsVideoResolutionTypeTransformer] instance.
|
||||||
|
static HlsVideoResolutionTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -14,26 +14,40 @@ class SystemConfigFFmpegRealtimeDto {
|
|||||||
/// Returns a new [SystemConfigFFmpegRealtimeDto] instance.
|
/// Returns a new [SystemConfigFFmpegRealtimeDto] instance.
|
||||||
SystemConfigFFmpegRealtimeDto({
|
SystemConfigFFmpegRealtimeDto({
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
|
this.resolutions = const [],
|
||||||
|
this.videoCodecs = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Enable real-time HLS transcoding (alpha)
|
/// Enable real-time HLS transcoding (alpha)
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
||||||
|
/// Resolutions to use for real-time HLS transcoding
|
||||||
|
List<HlsVideoResolution> resolutions;
|
||||||
|
|
||||||
|
/// Video codecs to use for real-time HLS transcoding
|
||||||
|
List<VideoCodec> videoCodecs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegRealtimeDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegRealtimeDto &&
|
||||||
other.enabled == enabled;
|
other.enabled == enabled &&
|
||||||
|
_deepEquality.equals(other.resolutions, resolutions) &&
|
||||||
|
_deepEquality.equals(other.videoCodecs, videoCodecs);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(enabled.hashCode);
|
(enabled.hashCode) +
|
||||||
|
(resolutions.hashCode) +
|
||||||
|
(videoCodecs.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigFFmpegRealtimeDto[enabled=$enabled]';
|
String toString() => 'SystemConfigFFmpegRealtimeDto[enabled=$enabled, resolutions=$resolutions, videoCodecs=$videoCodecs]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'enabled'] = this.enabled;
|
json[r'enabled'] = this.enabled;
|
||||||
|
json[r'resolutions'] = this.resolutions;
|
||||||
|
json[r'videoCodecs'] = this.videoCodecs;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +61,8 @@ class SystemConfigFFmpegRealtimeDto {
|
|||||||
|
|
||||||
return SystemConfigFFmpegRealtimeDto(
|
return SystemConfigFFmpegRealtimeDto(
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
|
resolutions: HlsVideoResolution.listFromJson(json[r'resolutions']),
|
||||||
|
videoCodecs: VideoCodec.listFromJson(json[r'videoCodecs']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -95,6 +111,8 @@ class SystemConfigFFmpegRealtimeDto {
|
|||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'enabled',
|
'enabled',
|
||||||
|
'resolutions',
|
||||||
|
'videoCodecs',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ class SystemConfigIntegrityChecksumJob {
|
|||||||
/// Percentage limit of the integrity checksum job
|
/// Percentage limit of the integrity checksum job
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
/// Maximum value: 9007199254740991
|
/// Maximum value: 1
|
||||||
int percentageLimit;
|
double percentageLimit;
|
||||||
|
|
||||||
/// How long the integrity checksum job may run for
|
/// How long the integrity checksum job may run for
|
||||||
///
|
///
|
||||||
@@ -75,7 +75,7 @@ class SystemConfigIntegrityChecksumJob {
|
|||||||
return SystemConfigIntegrityChecksumJob(
|
return SystemConfigIntegrityChecksumJob(
|
||||||
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
percentageLimit: mapValueOfType<int>(json, r'percentageLimit')!,
|
percentageLimit: mapValueOfType<double>(json, r'percentageLimit')!,
|
||||||
timeLimit: mapValueOfType<int>(json, r'timeLimit')!,
|
timeLimit: mapValueOfType<int>(json, r'timeLimit')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -34,6 +34,7 @@ import 'schema_v27.dart' as v27;
|
|||||||
import 'schema_v28.dart' as v28;
|
import 'schema_v28.dart' as v28;
|
||||||
import 'schema_v29.dart' as v29;
|
import 'schema_v29.dart' as v29;
|
||||||
import 'schema_v30.dart' as v30;
|
import 'schema_v30.dart' as v30;
|
||||||
|
import 'schema_v31.dart' as v31;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -99,6 +100,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v29.DatabaseAtV29(db);
|
return v29.DatabaseAtV29(db);
|
||||||
case 30:
|
case 30:
|
||||||
return v30.DatabaseAtV30(db);
|
return v30.DatabaseAtV30(db);
|
||||||
|
case 31:
|
||||||
|
return v31.DatabaseAtV31(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
@@ -135,5 +138,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
28,
|
28,
|
||||||
29,
|
29,
|
||||||
30,
|
30,
|
||||||
|
31,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+10032
File diff suppressed because it is too large
Load Diff
@@ -70,4 +70,36 @@ void main() {
|
|||||||
expect((assets.first as RemoteAsset).id, asset.id);
|
expect((assets.first as RemoteAsset).id, asset.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('live photos', () {
|
||||||
|
test('remote-only live photo contains livePhotoVideoId and is marked as a motion photo', () async {
|
||||||
|
final user = await ctx.newUser();
|
||||||
|
final asset = await ctx.newRemoteAsset(ownerId: user.id, livePhotoVideoId: 'motion-photo-1');
|
||||||
|
|
||||||
|
final assets = await sut.main([user.id], .day).assetSource(0, 10);
|
||||||
|
|
||||||
|
expect(assets, hasLength(1));
|
||||||
|
final remote = assets.single as RemoteAsset;
|
||||||
|
expect(remote.id, asset.id);
|
||||||
|
expect(remote.livePhotoVideoId, 'motion-photo-1');
|
||||||
|
expect(remote.isMotionPhoto, isTrue);
|
||||||
|
expect(remote.localId, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('merged live photo resolves localId and is marked as a motion photo', () async {
|
||||||
|
final user = await ctx.newUser();
|
||||||
|
const checksum = 'shared-live-photo-checksum';
|
||||||
|
final asset = await ctx.newRemoteAsset(ownerId: user.id, checksum: checksum, livePhotoVideoId: 'motion-photo-2');
|
||||||
|
final local = await ctx.newLocalAsset(checksum: checksum);
|
||||||
|
|
||||||
|
final assets = await sut.main([user.id], .day).assetSource(0, 10);
|
||||||
|
|
||||||
|
expect(assets, hasLength(1));
|
||||||
|
final remote = assets.single as RemoteAsset;
|
||||||
|
expect(remote.id, asset.id);
|
||||||
|
expect(remote.livePhotoVideoId, 'motion-photo-2');
|
||||||
|
expect(remote.isMotionPhoto, isTrue);
|
||||||
|
expect(remote.localId, local.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ void main() {
|
|||||||
expect(task, isNotNull);
|
expect(task, isNotNull);
|
||||||
expect(task!.fields['filename'], equals('OriginalLivePhoto.HEIC'));
|
expect(task!.fields['filename'], equals('OriginalLivePhoto.HEIC'));
|
||||||
expect(task.fields['livePhotoVideoId'], equals('video-id-123'));
|
expect(task.fields['livePhotoVideoId'], equals('video-id-123'));
|
||||||
|
expect(task.fields['visibility'], equals('hidden'));
|
||||||
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -333,6 +334,7 @@ void main() {
|
|||||||
expect(task, isNotNull);
|
expect(task, isNotNull);
|
||||||
expect(task!.fields.containsKey('metadata'), isTrue);
|
expect(task!.fields.containsKey('metadata'), isTrue);
|
||||||
expect(task.fields['livePhotoVideoId'], equals('video-123'));
|
expect(task.fields['livePhotoVideoId'], equals('video-123'));
|
||||||
|
expect(task.fields['visibility'], equals('hidden'));
|
||||||
|
|
||||||
final metadata = jsonDecode(task.fields['metadata']!) as List;
|
final metadata = jsonDecode(task.fields['metadata']!) as List;
|
||||||
expect(metadata, hasLength(1));
|
expect(metadata, hasLength(1));
|
||||||
|
|||||||
@@ -19120,6 +19120,17 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"HlsVideoResolution": {
|
||||||
|
"description": "HLS video resolution",
|
||||||
|
"enum": [
|
||||||
|
480,
|
||||||
|
720,
|
||||||
|
1080,
|
||||||
|
1440,
|
||||||
|
2160
|
||||||
|
],
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"ImageFormat": {
|
"ImageFormat": {
|
||||||
"description": "Image format",
|
"description": "Image format",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -25754,10 +25765,26 @@
|
|||||||
"enabled": {
|
"enabled": {
|
||||||
"description": "Enable real-time HLS transcoding (alpha)",
|
"description": "Enable real-time HLS transcoding (alpha)",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"description": "Resolutions to use for real-time HLS transcoding",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/HlsVideoResolution"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"videoCodecs": {
|
||||||
|
"description": "Video codecs to use for real-time HLS transcoding",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/VideoCodec"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"enabled"
|
"enabled",
|
||||||
|
"resolutions",
|
||||||
|
"videoCodecs"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@@ -25890,9 +25917,10 @@
|
|||||||
},
|
},
|
||||||
"percentageLimit": {
|
"percentageLimit": {
|
||||||
"description": "Percentage limit of the integrity checksum job",
|
"description": "Percentage limit of the integrity checksum job",
|
||||||
"maximum": 9007199254740991,
|
"format": "double",
|
||||||
|
"maximum": 1,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"type": "integer"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"timeLimit": {
|
"timeLimit": {
|
||||||
"description": "How long the integrity checksum job may run for",
|
"description": "How long the integrity checksum job may run for",
|
||||||
|
|||||||
@@ -151,14 +151,14 @@ const methods = wrapper<Manifest>({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
webhook: ({ config, data, functions, type, trigger }) => {
|
webhook: ({ config, data, functions, type, trigger }) => {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {};
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.headerName && config.headerValue) {
|
if (config.headerName && config.headerValue) {
|
||||||
headers[config.headerName] = config.headerValue;
|
headers[config.headerName] = config.headerValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
functions.httpRequest(config.url, {
|
functions.httpRequest(config.url, {
|
||||||
method: config.method ?? 'POST',
|
method: config.method ?? 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -2302,6 +2302,10 @@ export type SystemConfigBackupsDto = {
|
|||||||
export type SystemConfigFFmpegRealtimeDto = {
|
export type SystemConfigFFmpegRealtimeDto = {
|
||||||
/** Enable real-time HLS transcoding (alpha) */
|
/** Enable real-time HLS transcoding (alpha) */
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
/** Resolutions to use for real-time HLS transcoding */
|
||||||
|
resolutions: HlsVideoResolution[];
|
||||||
|
/** Video codecs to use for real-time HLS transcoding */
|
||||||
|
videoCodecs: VideoCodec[];
|
||||||
};
|
};
|
||||||
export type SystemConfigFFmpegDto = {
|
export type SystemConfigFFmpegDto = {
|
||||||
accel: TranscodeHWAccel;
|
accel: TranscodeHWAccel;
|
||||||
@@ -7629,6 +7633,13 @@ export enum CQMode {
|
|||||||
Cqp = "cqp",
|
Cqp = "cqp",
|
||||||
Icq = "icq"
|
Icq = "icq"
|
||||||
}
|
}
|
||||||
|
export enum HlsVideoResolution {
|
||||||
|
$480 = 480,
|
||||||
|
$720 = 720,
|
||||||
|
$1080 = 1080,
|
||||||
|
$1440 = 1440,
|
||||||
|
$2160 = 2160
|
||||||
|
}
|
||||||
export enum ToneMapping {
|
export enum ToneMapping {
|
||||||
Hable = "hable",
|
Hable = "hable",
|
||||||
Mobius = "mobius",
|
Mobius = "mobius",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<p align="center">
|
||||||
|
<br/>
|
||||||
|
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: 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="Immich">
|
||||||
|
</p>
|
||||||
|
<h3 align="center">ഫോട്ടോകളും വീഡിയോകളും കൈകാര്യം ചെയ്യുന്നതിനുള്ള ഉയർന്ന കാര്യക്ഷമതയുള്ള സെൽഫ്-ഹോസ്റ്റഡ് ആപ്ലിക്കേഷൻ</h3>
|
||||||
|
<br/>
|
||||||
|
<a href="https://immich.app">
|
||||||
|
<img src="../design/immich-screenshots.png" title="പ്രധാന സ്ക്രീൻഷോട്ട്">
|
||||||
|
</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_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>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> ⚠️ നിങ്ങളുടെ വിലയേറിയ ഫോട്ടോകൾക്കും വീഡിയോകൾക്കും എല്ലായ്പ്പോഴും [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) ബാക്കപ്പ് പ്ലാൻ പിന്തുടരുക!
|
||||||
|
>
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> ഇൻസ്റ്റാളേഷൻ ഗൈഡുകൾ ഉൾപ്പെടെയുള്ള പ്രധാന ഡോക്യുമെന്റേഷൻ https://immich.app/ എന്ന വെബ്സൈറ്റിൽ നിങ്ങൾക്ക് കണ്ടെത്താം.
|
||||||
|
|
||||||
|
## ലിങ്കുകൾ
|
||||||
|
|
||||||
|
- [ഡോക്യുമെന്റേഷൻ](https://docs.immich.app/)
|
||||||
|
- [വിവരങ്ങൾ](https://docs.immich.app/overview/introduction)
|
||||||
|
- [ഇൻസ്റ്റാളേഷൻ](https://docs.immich.app/install/requirements)
|
||||||
|
- [റോഡ്മാപ്പ്](https://immich.app/roadmap)
|
||||||
|
- [ഡെമോ](#ഡെമോ)
|
||||||
|
- [സവിശേഷതകൾ](#സവിശേഷതകൾ)
|
||||||
|
- [വിവർത്തനങ്ങൾ](https://docs.immich.app/developer/translations)
|
||||||
|
- [സംഭാവന നൽകൽ](https://docs.immich.app/overview/support-the-project)
|
||||||
|
|
||||||
|
## ഡെമോ
|
||||||
|
|
||||||
|
ഡെമോ വെബ്സൈറ്റ് [ഇവിടെ](https://demo.immich.app) ആക്സസ് ചെയ്യാം. മൊബൈൽ ആപ്പിന്റെ ഡെമോ കാണാൻ ആപ്പ് ഇൻസ്റ്റാൾ ചെയ്ത ശേഷം, `Server Endpoint URL` എന്ന സെറ്റിങ്ങിൽ നിങ്ങൾക്ക് `https://demo.immich.app` ഉപയോഗിക്കാം.
|
||||||
|
|
||||||
|
### ലോഗിൻ വിവരങ്ങൾ
|
||||||
|
|
||||||
|
| ഇമെയിൽ | പാസ്വേഡ് |
|
||||||
|
| --------------- | -------- |
|
||||||
|
| demo@immich.app | demo |
|
||||||
|
|
||||||
|
## സവിശേഷതകൾ
|
||||||
|
|
||||||
|
| സവിശേഷതകൾ | മൊബൈൽ | വെബ് |
|
||||||
|
| :-------------------------------------------------- | ------ | --- |
|
||||||
|
| ഫോട്ടോകളും വീഡിയോകളും അപ്ലോഡ് ചെയ്യാനും കാണാനും സാധിക്കും | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| ആപ്പ് തുറക്കുമ്പോൾ ഓട്ടോമാറ്റിക് ബാക്കപ്പ് | ഉണ്ട് | N/A |
|
||||||
|
| ഫയലുകളുടെ ഇരട്ടിപ്പ് (duplication) തടയുന്നു | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| ബാക്കപ്പിനായി നിർദ്ദിഷ്ട ആൽബങ്ങൾ തിരഞ്ഞെടുക്കാം | ഉണ്ട് | N/A |
|
||||||
|
| ഫോട്ടോകളും വീഡിയോകളും ലോക്കൽ ഡിവൈസിലേക്ക് ഡൗൺലോഡ് ചെയ്യാം | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| ഒന്നിലധികം ഉപയോക്താക്കൾക്കുള്ള പിന്തുണ (Multi-user) | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| ആൽബങ്ങളും പങ്കിട്ട ആൽബങ്ങളും | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| സ്ക്രബ്ബ് ചെയ്യാവുന്ന/വലിച്ചിഴക്കാവുന്ന സ്ക്രോൾബാർ | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| റോ (RAW) ഫോർമാറ്റുകൾക്കുള്ള പിന്തുണ | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| മെറ്റാഡാറ്റ കാഴ്ച (EXIF, മാപ്പ്) | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| മെറ്റാഡാറ്റ, ഒബ്ജക്റ്റുകൾ, മുഖങ്ങൾ, CLIP എന്നിവ ഉപയോഗിച്ച് തിരയാം | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| അഡ്മിനിസ്ട്രേറ്റീവ് പ്രവർത്തനങ്ങൾ (യൂസർ മാനേജ്മെന്റ്) | ഇല്ല | ഉണ്ട് |
|
||||||
|
| ബാക്ക്ഗ്രൗണ്ട് ബാക്കപ്പ് | ഉണ്ട് | N/A |
|
||||||
|
| വിർച്വൽ സ്ക്രോൾ | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| OAuth സപ്പോർട്ട് | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| API കീകൾ | N/A | ഉണ്ട് |
|
||||||
|
| ലൈവ് ഫോട്ടോ/മോഷൻ ഫോട്ടോ ബാക്കപ്പും പ്ലേബാക്കും | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| 360 ഡിഗ്രി ഇമേജ് ഡിസ്പ്ലേ പിന്തുണ | ഇല്ല | ഉണ്ട് |
|
||||||
|
| ഉപയോക്താവ് നിർവചിക്കുന്ന സ്റ്റോറേജ് ഘടന | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| പബ്ലിക് ഷെയറിംഗ് | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| ആർക്കൈവും പ്രിയപ്പെട്ടവയും (Favorites) | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| ആഗോള മാപ്പ് (Global Map) | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| പങ്കാളി പങ്കിടൽ (Partner Sharing) | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| മുഖം തിരിച്ചറിയലും ക്ലസ്റ്ററിംഗും | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| ഓർമ്മകൾ (Memories - x വർഷങ്ങൾക്ക് മുമ്പ്) | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| ഓഫ്ലൈൻ പിന്തുണ | ഉണ്ട് | ഇല്ല |
|
||||||
|
| റീഡ്-ഒൺലി ഗാലറി | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| അടുക്കിവെച്ച ഫോട്ടോകൾ (Stacked Photos) | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
| ടാഗുകൾ | ഇല്ല | ഉണ്ട് |
|
||||||
|
| ഫോൾഡർ കാഴ്ച | ഉണ്ട് | ഉണ്ട് |
|
||||||
|
|
||||||
|
## വിവർത്തനങ്ങൾ
|
||||||
|
|
||||||
|
വിവർത്തനങ്ങളെക്കുറിച്ച് കൂടുതൽ [ഇവിടെ](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="Translation status" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## റെപ്പോസിറ്ററി പ്രവർത്തനം
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## സ്റ്റാർ ചരിത്രം
|
||||||
|
|
||||||
|
<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="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=date" width="100%" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## സംഭാവന ചെയ്തവർ
|
||||||
|
|
||||||
|
<a href="https://github.com/immich-app/immich/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||||
|
</a>
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
|
<a href="README_ml_IN.md">മലയാളം</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
FROM ghcr.io/immich-app/base-server-dev:202606161235@sha256:9f88b07acc8b7bf37a1dd3d5a19193f664443eaaab4e08e9f9341414c5e4b23f AS builder
|
FROM ghcr.io/immich-app/base-server-dev:202607061334@sha256:c83ef9c6d7f5e7f6a276fe96647484d46b7bf6cacc1e35b823dd1d7a1aeda3b8 AS builder
|
||||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||||
CI=1 \
|
CI=1 \
|
||||||
COREPACK_HOME=/tmp \
|
COREPACK_HOME=/tmp \
|
||||||
@@ -56,7 +56,7 @@ FROM builder AS plugins
|
|||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
COPY --from=ghcr.io/jdx/mise:2026.6.10@sha256:f57ac375a262f52f8ac3f9101348dbff2187d5e4b59612154f2f2808dbe46ef6 /usr/local/bin/mise /usr/local/bin/mise
|
COPY --from=ghcr.io/jdx/mise:2026.6.14@sha256:b8f8c20fc3308f8b1d00ccca2bc968e4e208af1c5c1069e1ad9753baa099acff /usr/local/bin/mise /usr/local/bin/mise
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./mise.toml ./mise.toml
|
COPY ./mise.toml ./mise.toml
|
||||||
@@ -80,7 +80,7 @@ RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \
|
|||||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||||
mise //:plugins
|
mise //:plugins
|
||||||
|
|
||||||
FROM ghcr.io/immich-app/base-server-prod:202606161235@sha256:c6d59e3923f548d29a212b4dc51b6281a722cfa1da7972a009c0f3830f5762d6
|
FROM ghcr.io/immich-app/base-server-prod:202607061334@sha256:c07ef6510ff2ccb6ead28434ade7c7a0c08aef33d7d3466b14beccdd431e86f7
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# dev build
|
# dev build
|
||||||
FROM ghcr.io/immich-app/base-server-dev:202606161235@sha256:9f88b07acc8b7bf37a1dd3d5a19193f664443eaaab4e08e9f9341414c5e4b23f AS dev
|
FROM ghcr.io/immich-app/base-server-dev:202607061334@sha256:c83ef9c6d7f5e7f6a276fe96647484d46b7bf6cacc1e35b823dd1d7a1aeda3b8 AS dev
|
||||||
|
|
||||||
|
|
||||||
COPY --from=ghcr.io/jdx/mise:2026.6.10@sha256:f57ac375a262f52f8ac3f9101348dbff2187d5e4b59612154f2f2808dbe46ef6 /usr/local/bin/mise /usr/local/bin/mise
|
COPY --from=ghcr.io/jdx/mise:2026.6.14@sha256:b8f8c20fc3308f8b1d00ccca2bc968e4e208af1c5c1069e1ad9753baa099acff /usr/local/bin/mise /usr/local/bin/mise
|
||||||
|
|
||||||
RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \
|
RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \
|
||||||
echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \
|
echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
CQMode,
|
CQMode,
|
||||||
|
HlsVideoResolution,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
OAuthTokenEndpointAuthMethod,
|
OAuthTokenEndpointAuthMethod,
|
||||||
@@ -48,6 +49,8 @@ export type SystemConfig = {
|
|||||||
tonemap: ToneMapping;
|
tonemap: ToneMapping;
|
||||||
realtime: {
|
realtime: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
videoCodecs: VideoCodec[];
|
||||||
|
resolutions: HlsVideoResolution[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
integrityChecks: {
|
integrityChecks: {
|
||||||
@@ -247,6 +250,8 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
accelDecode: true,
|
accelDecode: true,
|
||||||
realtime: {
|
realtime: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
videoCodecs: [VideoCodec.H264, VideoCodec.Hevc],
|
||||||
|
resolutions: [HlsVideoResolution.p480, HlsVideoResolution.p720, HlsVideoResolution.p1080],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
integrityChecks: {
|
integrityChecks: {
|
||||||
|
|||||||
+60
-9
@@ -235,14 +235,65 @@ export const HLS_PLAYLIST_CONTENT_TYPE = 'application/vnd.apple.mpegurl';
|
|||||||
export const HLS_SEGMENT_DURATION = 2;
|
export const HLS_SEGMENT_DURATION = 2;
|
||||||
export const HLS_SEGMENT_FILENAME_REGEX = /^seg_(\d+)\.m4s$/;
|
export const HLS_SEGMENT_FILENAME_REGEX = /^seg_(\d+)\.m4s$/;
|
||||||
export const HLS_VARIANTS = [
|
export const HLS_VARIANTS = [
|
||||||
{ resolution: 480, codec: VideoCodec.Av1, bitrate: 1_000_000, codecString: 'av01.0.04M.08' },
|
{ resolution: 480, codec: VideoCodec.Av1, bitrate: 1_000_000 },
|
||||||
{ resolution: 480, codec: VideoCodec.Hevc, bitrate: 1_200_000, codecString: 'hvc1.1.6.L90.B0' },
|
{ resolution: 480, codec: VideoCodec.Hevc, bitrate: 1_200_000 },
|
||||||
{ resolution: 480, codec: VideoCodec.H264, bitrate: 2_500_000, codecString: 'avc1.64001e' },
|
{ resolution: 480, codec: VideoCodec.H264, bitrate: 2_500_000 },
|
||||||
{ resolution: 720, codec: VideoCodec.Av1, bitrate: 2_000_000, codecString: 'av01.0.08M.08' },
|
{ resolution: 720, codec: VideoCodec.Av1, bitrate: 2_000_000 },
|
||||||
{ resolution: 720, codec: VideoCodec.Hevc, bitrate: 2_500_000, codecString: 'hvc1.1.6.L93.B0' },
|
{ resolution: 720, codec: VideoCodec.Hevc, bitrate: 2_500_000 },
|
||||||
{ resolution: 720, codec: VideoCodec.H264, bitrate: 5_000_000, codecString: 'avc1.64001f' },
|
{ resolution: 720, codec: VideoCodec.H264, bitrate: 5_000_000 },
|
||||||
{ resolution: 1080, codec: VideoCodec.Av1, bitrate: 4_000_000, codecString: 'av01.0.09M.08' },
|
{ resolution: 1080, codec: VideoCodec.Av1, bitrate: 4_000_000 },
|
||||||
{ resolution: 1080, codec: VideoCodec.Hevc, bitrate: 4_500_000, codecString: 'hvc1.1.6.L120.B0' },
|
{ resolution: 1080, codec: VideoCodec.Hevc, bitrate: 4_500_000 },
|
||||||
{ resolution: 1080, codec: VideoCodec.H264, bitrate: 8_000_000, codecString: 'avc1.640028' },
|
{ resolution: 1080, codec: VideoCodec.H264, bitrate: 8_000_000 },
|
||||||
|
{ resolution: 1440, codec: VideoCodec.Av1, bitrate: 7_000_000 },
|
||||||
|
{ resolution: 1440, codec: VideoCodec.Hevc, bitrate: 8_000_000 },
|
||||||
|
{ resolution: 1440, codec: VideoCodec.H264, bitrate: 14_000_000 },
|
||||||
|
{ resolution: 2160, codec: VideoCodec.Av1, bitrate: 12_000_000 },
|
||||||
|
{ resolution: 2160, codec: VideoCodec.Hevc, bitrate: 14_000_000 },
|
||||||
|
{ resolution: 2160, codec: VideoCodec.H264, bitrate: 25_000_000 },
|
||||||
];
|
];
|
||||||
export const HLS_VERSION = 7;
|
export const HLS_VERSION = 7;
|
||||||
|
|
||||||
|
export type CodecLevel = { maxFrame: number; maxRate: number; token: string };
|
||||||
|
|
||||||
|
// H.264 High profile: token is the hex level_idc.
|
||||||
|
export const H264_LEVELS: CodecLevel[] = [
|
||||||
|
{ maxFrame: 1620, maxRate: 40_500, token: '1e' }, // 3.0
|
||||||
|
{ maxFrame: 3600, maxRate: 108_000, token: '1f' }, // 3.1
|
||||||
|
{ maxFrame: 5120, maxRate: 216_000, token: '20' }, // 3.2
|
||||||
|
{ maxFrame: 8192, maxRate: 245_760, token: '28' }, // 4.0
|
||||||
|
{ maxFrame: 8704, maxRate: 522_240, token: '2a' }, // 4.2
|
||||||
|
{ maxFrame: 22_080, maxRate: 589_824, token: '32' }, // 5.0
|
||||||
|
{ maxFrame: 36_864, maxRate: 983_040, token: '33' }, // 5.1
|
||||||
|
{ maxFrame: 36_864, maxRate: 2_073_600, token: '34' }, // 5.2
|
||||||
|
{ maxFrame: 139_264, maxRate: 4_177_920, token: '3c' }, // 6.0
|
||||||
|
{ maxFrame: 139_264, maxRate: 8_355_840, token: '3d' }, // 6.1
|
||||||
|
{ maxFrame: 139_264, maxRate: 16_711_680, token: '3e' }, // 6.2
|
||||||
|
];
|
||||||
|
|
||||||
|
// HEVC Main profile, Main tier: token is `L` + level_idc (level × 30).
|
||||||
|
export const HEVC_LEVELS: CodecLevel[] = [
|
||||||
|
{ maxFrame: 552_960, maxRate: 16_588_800, token: 'L90' }, // 3.0
|
||||||
|
{ maxFrame: 983_040, maxRate: 33_177_600, token: 'L93' }, // 3.1
|
||||||
|
{ maxFrame: 2_228_224, maxRate: 66_846_720, token: 'L120' }, // 4.0
|
||||||
|
{ maxFrame: 2_228_224, maxRate: 133_693_440, token: 'L123' }, // 4.1
|
||||||
|
{ maxFrame: 8_912_896, maxRate: 267_386_880, token: 'L150' }, // 5.0
|
||||||
|
{ maxFrame: 8_912_896, maxRate: 534_773_760, token: 'L153' }, // 5.1
|
||||||
|
{ maxFrame: 8_912_896, maxRate: 1_069_547_520, token: 'L156' }, // 5.2
|
||||||
|
{ maxFrame: 35_651_584, maxRate: 1_069_547_520, token: 'L180' }, // 6.0
|
||||||
|
{ maxFrame: 35_651_584, maxRate: 2_139_095_040, token: 'L183' }, // 6.1
|
||||||
|
{ maxFrame: 35_651_584, maxRate: 4_278_190_080, token: 'L186' }, // 6.2
|
||||||
|
];
|
||||||
|
|
||||||
|
// AV1 Main profile (0), Main tier (M): token is the two-digit seq_level_idx + `M`.
|
||||||
|
export const AV1_LEVELS: CodecLevel[] = [
|
||||||
|
{ maxFrame: 665_856, maxRate: 19_975_168, token: '04M' }, // 3.0
|
||||||
|
{ maxFrame: 1_065_024, maxRate: 31_950_336, token: '05M' }, // 3.1
|
||||||
|
{ maxFrame: 2_359_296, maxRate: 70_778_880, token: '08M' }, // 4.0
|
||||||
|
{ maxFrame: 2_359_296, maxRate: 141_557_760, token: '09M' }, // 4.1
|
||||||
|
{ maxFrame: 8_912_896, maxRate: 267_386_880, token: '12M' }, // 5.0
|
||||||
|
{ maxFrame: 8_912_896, maxRate: 534_773_760, token: '13M' }, // 5.1
|
||||||
|
{ maxFrame: 8_912_896, maxRate: 1_069_547_520, token: '14M' }, // 5.2
|
||||||
|
{ maxFrame: 35_651_584, maxRate: 1_069_547_520, token: '16M' }, // 6.0
|
||||||
|
{ maxFrame: 35_651_584, maxRate: 2_139_095_040, token: '17M' }, // 6.1
|
||||||
|
{ maxFrame: 35_651_584, maxRate: 4_278_190_080, token: '18M' }, // 6.2
|
||||||
|
];
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
AudioCodecSchema,
|
AudioCodecSchema,
|
||||||
ColorspaceSchema,
|
ColorspaceSchema,
|
||||||
CQModeSchema,
|
CQModeSchema,
|
||||||
|
HlsVideoResolutionSchema,
|
||||||
ImageFormatSchema,
|
ImageFormatSchema,
|
||||||
LogLevelSchema,
|
LogLevelSchema,
|
||||||
OAuthTokenEndpointAuthMethodSchema,
|
OAuthTokenEndpointAuthMethodSchema,
|
||||||
@@ -73,7 +74,12 @@ const SystemConfigIntegrityJobSchema = z
|
|||||||
|
|
||||||
const SystemConfigIntegrityChecksumJobSchema = SystemConfigIntegrityJobSchema.extend({
|
const SystemConfigIntegrityChecksumJobSchema = SystemConfigIntegrityJobSchema.extend({
|
||||||
timeLimit: z.int().nonnegative().describe('How long the integrity checksum job may run for'),
|
timeLimit: z.int().nonnegative().describe('How long the integrity checksum job may run for'),
|
||||||
percentageLimit: z.int().nonnegative().describe('Percentage limit of the integrity checksum job'),
|
percentageLimit: z
|
||||||
|
.float32()
|
||||||
|
.nonnegative()
|
||||||
|
.max(1)
|
||||||
|
.describe('Percentage limit of the integrity checksum job')
|
||||||
|
.meta({ format: 'double' }),
|
||||||
})
|
})
|
||||||
.describe('Integrity checksum job config')
|
.describe('Integrity checksum job config')
|
||||||
.meta({ id: 'SystemConfigIntegrityChecksumJob' });
|
.meta({ id: 'SystemConfigIntegrityChecksumJob' });
|
||||||
@@ -115,6 +121,8 @@ const SystemConfigFFmpegSchema = z
|
|||||||
realtime: z
|
realtime: z
|
||||||
.object({
|
.object({
|
||||||
enabled: configBool.describe('Enable real-time HLS transcoding (alpha)'),
|
enabled: configBool.describe('Enable real-time HLS transcoding (alpha)'),
|
||||||
|
videoCodecs: z.array(VideoCodecSchema).describe('Video codecs to use for real-time HLS transcoding'),
|
||||||
|
resolutions: z.array(HlsVideoResolutionSchema).describe('Resolutions to use for real-time HLS transcoding'),
|
||||||
})
|
})
|
||||||
.meta({ id: 'SystemConfigFFmpegRealtimeDto' }),
|
.meta({ id: 'SystemConfigFFmpegRealtimeDto' }),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -529,6 +529,19 @@ export enum CQMode {
|
|||||||
|
|
||||||
export const CQModeSchema = z.enum(CQMode).describe('CQ mode').meta({ id: 'CQMode' });
|
export const CQModeSchema = z.enum(CQMode).describe('CQ mode').meta({ id: 'CQMode' });
|
||||||
|
|
||||||
|
export enum HlsVideoResolution {
|
||||||
|
p480 = 480,
|
||||||
|
p720 = 720,
|
||||||
|
p1080 = 1080,
|
||||||
|
p1440 = 1440,
|
||||||
|
p2160 = 2160,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HlsVideoResolutionSchema = z
|
||||||
|
.enum(HlsVideoResolution)
|
||||||
|
.describe('HLS video resolution')
|
||||||
|
.meta({ id: 'HlsVideoResolution', type: 'integer' });
|
||||||
|
|
||||||
export enum Colorspace {
|
export enum Colorspace {
|
||||||
Srgb = 'srgb',
|
Srgb = 'srgb',
|
||||||
P3 = 'p3',
|
P3 = 'p3',
|
||||||
|
|||||||
@@ -130,10 +130,9 @@ from
|
|||||||
where
|
where
|
||||||
"asset"."deletedAt" is null
|
"asset"."deletedAt" is null
|
||||||
and "asset"."isExternal" = false
|
and "asset"."isExternal" = false
|
||||||
and "integrity_report"."createdAt" >= $2
|
and "asset"."createdAt" >= $2
|
||||||
and "integrity_report"."createdAt" <= $3
|
|
||||||
order by
|
order by
|
||||||
"integrity_report"."createdAt" asc
|
"asset"."createdAt" asc
|
||||||
|
|
||||||
-- IntegrityRepository.streamIntegrityReports
|
-- IntegrityRepository.streamIntegrityReports
|
||||||
select
|
select
|
||||||
|
|||||||
@@ -160,8 +160,8 @@ export class IntegrityRepository {
|
|||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.DATE, DummyValue.DATE], stream: true })
|
@GenerateSql({ params: [DummyValue.DATE], stream: true })
|
||||||
streamAssetChecksums(startMarker?: Date, endMarker?: Date) {
|
streamAssetChecksums(startMarker?: Date) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.where('asset.deletedAt', 'is', null)
|
.where('asset.deletedAt', 'is', null)
|
||||||
@@ -178,9 +178,8 @@ export class IntegrityRepository {
|
|||||||
'integrity_report.id as reportId',
|
'integrity_report.id as reportId',
|
||||||
])
|
])
|
||||||
.where('asset.isExternal', '=', sql.lit(false))
|
.where('asset.isExternal', '=', sql.lit(false))
|
||||||
.$if(startMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '>=', startMarker!))
|
.$if(startMarker !== undefined, (qb) => qb.where('asset.createdAt', '>=', startMarker!))
|
||||||
.$if(endMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '<=', endMarker!))
|
.orderBy('asset.createdAt', 'asc')
|
||||||
.orderBy('integrity_report.createdAt', 'asc')
|
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -426,7 +426,7 @@ export class AuthService extends BaseService {
|
|||||||
throw new BadRequestException('This OAuth account has already been linked to another user.');
|
throw new BadRequestException('This OAuth account has already been linked to another user.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.session) {
|
if (auth.session && sid) {
|
||||||
await this.sessionRepository.update(auth.session.id, { oauthSid: sid });
|
await this.sessionRepository.update(auth.session.id, { oauthSid: sid });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ export class DatabaseBackupService {
|
|||||||
progressCb?.('restore', progress);
|
progressCb?.('restore', progress);
|
||||||
});
|
});
|
||||||
|
|
||||||
await pipeline(sqlStream, progressSource, psql, progressSink);
|
await pipeline(sqlStream, createSqlOwnerTransformStream(databaseUsername), progressSource, psql, progressSink);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
progressCb?.('migrations', 0.9);
|
progressCb?.('migrations', 0.9);
|
||||||
@@ -572,3 +572,69 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
|
|||||||
|
|
||||||
return [source, sink];
|
return [source, sink];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSqlOwnerTransformStream(databaseUsername: string) {
|
||||||
|
const OWNER_MARKER_START = new TextEncoder().encode('OWNER TO ');
|
||||||
|
const DATA_MARKER_START = new TextEncoder().encode('FROM stdin');
|
||||||
|
const LINE_END = new TextEncoder().encode(';');
|
||||||
|
|
||||||
|
const owner = new TextEncoder().encode(databaseUsername);
|
||||||
|
|
||||||
|
let ownerSequenceIndex = 0;
|
||||||
|
|
||||||
|
let replacingOwnerIndex = 0;
|
||||||
|
let replacingOwner = false;
|
||||||
|
|
||||||
|
let readingDataIndex = 0;
|
||||||
|
|
||||||
|
let dataPart = false;
|
||||||
|
|
||||||
|
return new PassThrough({
|
||||||
|
transform(chunk, _encoding, callback) {
|
||||||
|
let result = chunk;
|
||||||
|
if (!dataPart) {
|
||||||
|
for (let index = 0; index < result.length; index++) {
|
||||||
|
if (replacingOwner) {
|
||||||
|
if (result[index] === LINE_END[0]) {
|
||||||
|
result = Buffer.concat([result.slice(0, index), owner.slice(replacingOwnerIndex), result.slice(index)]);
|
||||||
|
replacingOwnerIndex = owner.length;
|
||||||
|
} else {
|
||||||
|
result[index] = owner[replacingOwnerIndex];
|
||||||
|
replacingOwnerIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replacingOwnerIndex === owner.length) {
|
||||||
|
replacingOwner = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result[index] === OWNER_MARKER_START[ownerSequenceIndex]) {
|
||||||
|
ownerSequenceIndex++;
|
||||||
|
} else {
|
||||||
|
ownerSequenceIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownerSequenceIndex === OWNER_MARKER_START.length) {
|
||||||
|
ownerSequenceIndex = 0;
|
||||||
|
replacingOwner = true;
|
||||||
|
replacingOwnerIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result[index] === DATA_MARKER_START[readingDataIndex]) {
|
||||||
|
readingDataIndex++;
|
||||||
|
} else {
|
||||||
|
readingDataIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readingDataIndex === DATA_MARKER_START.length) {
|
||||||
|
dataPart = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.push(result);
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
import { TranscodeHardwareAcceleration } from 'src/enum';
|
import { HlsVideoResolution, VideoCodec } from 'src/enum';
|
||||||
import { HlsService } from 'src/services/hls.service';
|
import { HlsService } from 'src/services/hls.service';
|
||||||
import { eiffelTower, train, waterfall } from 'test/fixtures/media.stub';
|
import { eiffelTower, train, waterfall } from 'test/fixtures/media.stub';
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
@@ -96,67 +96,79 @@ seg_10.m4s
|
|||||||
|
|
||||||
const sessionId = '00000000-0000-0000-0000-000000000000';
|
const sessionId = '00000000-0000-0000-0000-000000000000';
|
||||||
|
|
||||||
const eiffelExpectedMasterDisabled = `#EXTM3U
|
const eiffelExpectedMasterAv1 = `#EXTM3U
|
||||||
#EXT-X-VERSION:7
|
#EXT-X-VERSION:7
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=1350000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/0/playlist.m3u8
|
${sessionId}/0/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=1620000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/1/playlist.m3u8
|
${sessionId}/1/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/2/playlist.m3u8
|
${sessionId}/2/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=720x1280,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=2700000,RESOLUTION=720x1280,CODECS="av01.0.05M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/3/playlist.m3u8
|
${sessionId}/3/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/4/playlist.m3u8
|
${sessionId}/4/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=6750000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/5/playlist.m3u8
|
${sessionId}/5/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1080x1920,CODECS="av01.0.09M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=5400000,RESOLUTION=1080x1920,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/6/playlist.m3u8
|
${sessionId}/6/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=6075000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/7/playlist.m3u8
|
${sessionId}/7/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=10800000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/8/playlist.m3u8
|
${sessionId}/8/playlist.m3u8
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const eiffelExpectedMasterRkmpp = `#EXTM3U
|
const eiffelExpectedMasterNoAv1 = `#EXTM3U
|
||||||
#EXT-X-VERSION:7
|
#EXT-X-VERSION:7
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=1620000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/1/playlist.m3u8
|
${sessionId}/1/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/2/playlist.m3u8
|
${sessionId}/2/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/4/playlist.m3u8
|
${sessionId}/4/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=6750000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/5/playlist.m3u8
|
${sessionId}/5/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=6075000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/7/playlist.m3u8
|
${sessionId}/7/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=10800000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/8/playlist.m3u8
|
${sessionId}/8/playlist.m3u8
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const waterfallExpectedMasterDisabled = `#EXTM3U
|
const waterfallExpectedMasterAv1 = `#EXTM3U
|
||||||
#EXT-X-VERSION:7
|
#EXT-X-VERSION:7
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=1350000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/0/playlist.m3u8
|
${sessionId}/0/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=1620000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/1/playlist.m3u8
|
${sessionId}/1/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=480x852,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/2/playlist.m3u8
|
${sessionId}/2/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=720x1280,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=2700000,RESOLUTION=720x1280,CODECS="av01.0.05M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/3/playlist.m3u8
|
${sessionId}/3/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/4/playlist.m3u8
|
${sessionId}/4/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=6750000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/5/playlist.m3u8
|
${sessionId}/5/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1080x1920,CODECS="av01.0.09M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=5400000,RESOLUTION=1080x1920,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/6/playlist.m3u8
|
${sessionId}/6/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=6075000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/7/playlist.m3u8
|
${sessionId}/7/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=10800000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/8/playlist.m3u8
|
${sessionId}/8/playlist.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=9450000,RESOLUTION=1440x2560,CODECS="av01.0.12M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
|
${sessionId}/9/playlist.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=10800000,RESOLUTION=1440x2560,CODECS="hvc1.1.6.L150.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
|
${sessionId}/10/playlist.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=18900000,RESOLUTION=1440x2560,CODECS="avc1.640032,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
|
${sessionId}/11/playlist.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=16200000,RESOLUTION=2160x3840,CODECS="av01.0.12M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
|
${sessionId}/12/playlist.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=18900000,RESOLUTION=2160x3840,CODECS="hvc1.1.6.L150.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
|
${sessionId}/13/playlist.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=33750000,RESOLUTION=2160x3840,CODECS="avc1.640033,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
|
${sessionId}/14/playlist.m3u8
|
||||||
`;
|
`;
|
||||||
|
|
||||||
describe(HlsService.name, () => {
|
describe(HlsService.name, () => {
|
||||||
@@ -171,9 +183,24 @@ describe(HlsService.name, () => {
|
|||||||
const auth = factory.auth();
|
const auth = factory.auth();
|
||||||
const assetId = 'asset-1';
|
const assetId = 'asset-1';
|
||||||
|
|
||||||
const setup = (asset: typeof eiffelTower | typeof waterfall, accel: TranscodeHardwareAcceleration) => {
|
const allCodecs = [VideoCodec.Av1, VideoCodec.Hevc, VideoCodec.H264];
|
||||||
|
const allResolutions = [
|
||||||
|
HlsVideoResolution.p480,
|
||||||
|
HlsVideoResolution.p720,
|
||||||
|
HlsVideoResolution.p1080,
|
||||||
|
HlsVideoResolution.p1440,
|
||||||
|
HlsVideoResolution.p2160,
|
||||||
|
];
|
||||||
|
|
||||||
|
const setup = (
|
||||||
|
asset: typeof eiffelTower | typeof waterfall,
|
||||||
|
videoCodecs?: VideoCodec[],
|
||||||
|
resolutions?: HlsVideoResolution[],
|
||||||
|
) => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true }, accel } });
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
|
ffmpeg: { realtime: { enabled: true, videoCodecs, resolutions } },
|
||||||
|
});
|
||||||
mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset);
|
mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset);
|
||||||
mocks.crypto.randomUUID.mockReturnValue(sessionId);
|
mocks.crypto.randomUUID.mockReturnValue(sessionId);
|
||||||
mocks.websocket.serverSend.mockImplementation((event, ...rest) => {
|
mocks.websocket.serverSend.mockImplementation((event, ...rest) => {
|
||||||
@@ -184,19 +211,19 @@ describe(HlsService.name, () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
it('returns main playlist for eiffel-tower (1080p portrait, no acceleration)', async () => {
|
it('offers AV1, HEVC, and H.264 when AV1 is configured and the accelerator supports it', async () => {
|
||||||
setup(eiffelTower, TranscodeHardwareAcceleration.Disabled);
|
setup(eiffelTower, allCodecs);
|
||||||
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterDisabled);
|
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterAv1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns main playlist for eiffel-tower with RKMPP (no AV1 variants)', async () => {
|
it('omits AV1 when it is not in the configured codecs', async () => {
|
||||||
setup(eiffelTower, TranscodeHardwareAcceleration.Rkmpp);
|
setup(eiffelTower);
|
||||||
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterRkmpp);
|
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterNoAv1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns main playlist for waterfall (4K landscape) with no acceleration', async () => {
|
it('offers every resolution up to the source and derives 4K codec levels (waterfall, 4K, 29.83fps)', async () => {
|
||||||
setup(waterfall, TranscodeHardwareAcceleration.Disabled);
|
setup(waterfall, allCodecs, allResolutions);
|
||||||
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(waterfallExpectedMasterDisabled);
|
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(waterfallExpectedMasterAv1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws BadRequestException when realtime transcoding is disabled', async () => {
|
it('throws BadRequestException when realtime transcoding is disabled', async () => {
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { constants } from 'node:fs';
|
import { constants } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import {
|
import { HLS_SEGMENT_DURATION, HLS_SEGMENT_FILENAME_REGEX, HLS_VARIANTS, HLS_VERSION } from 'src/constants';
|
||||||
HLS_SEGMENT_DURATION,
|
|
||||||
HLS_SEGMENT_FILENAME_REGEX,
|
|
||||||
HLS_VARIANTS,
|
|
||||||
HLS_VERSION,
|
|
||||||
SUPPORTED_HWA_CODECS,
|
|
||||||
} from 'src/constants';
|
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
@@ -18,7 +12,7 @@ import { BaseService } from 'src/services/base.service';
|
|||||||
import { VideoPacketInfo, VideoStreamInfo } from 'src/types';
|
import { VideoPacketInfo, VideoStreamInfo } from 'src/types';
|
||||||
import { PendingEvents } from 'src/utils/event';
|
import { PendingEvents } from 'src/utils/event';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { getOutputSize } from 'src/utils/media';
|
import { getCodecString, getOutputSize } from 'src/utils/media';
|
||||||
|
|
||||||
type AssetWithStreamInfo = { videoStream: VideoStreamInfo & { timeBase: number }; packets: VideoPacketInfo };
|
type AssetWithStreamInfo = { videoStream: VideoStreamInfo & { timeBase: number }; packets: VideoPacketInfo };
|
||||||
type Segmentation = { fps: number; framesPerSegment: number; segmentCount: number; segmentDuration: number };
|
type Segmentation = { fps: number; framesPerSegment: number; segmentCount: number; segmentDuration: number };
|
||||||
@@ -131,18 +125,21 @@ export class HlsService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private generateMainPlaylist(sessionId: string, ffmpeg: SystemConfigFFmpegDto, asset: AssetWithStreamInfo) {
|
private generateMainPlaylist(sessionId: string, ffmpeg: SystemConfigFFmpegDto, asset: AssetWithStreamInfo) {
|
||||||
const fps = ((asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration).toFixed(3);
|
const fps = (asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration;
|
||||||
|
const roundedFps = fps.toFixed(3);
|
||||||
const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width);
|
const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width);
|
||||||
const targetResolution = Math.max(sourceResolution, HLS_VARIANTS[0].resolution);
|
const targetResolution = Math.max(sourceResolution, HLS_VARIANTS[0].resolution);
|
||||||
const lines = ['#EXTM3U', `#EXT-X-VERSION:${HLS_VERSION}`, '#EXT-X-INDEPENDENT-SEGMENTS'];
|
const lines = ['#EXTM3U', `#EXT-X-VERSION:${HLS_VERSION}`, '#EXT-X-INDEPENDENT-SEGMENTS'];
|
||||||
|
const { videoCodecs, resolutions } = ffmpeg.realtime;
|
||||||
for (let i = 0; i < HLS_VARIANTS.length; i++) {
|
for (let i = 0; i < HLS_VARIANTS.length; i++) {
|
||||||
const { resolution, bitrate, codec, codecString } = HLS_VARIANTS[i];
|
const { resolution, bitrate, codec } = HLS_VARIANTS[i];
|
||||||
if (resolution > targetResolution || !SUPPORTED_HWA_CODECS[ffmpeg.accel].includes(codec)) {
|
if (resolution > targetResolution || !videoCodecs.includes(codec) || !resolutions.includes(resolution)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const { width, height } = getOutputSize(asset.videoStream, resolution);
|
const { width, height } = getOutputSize(asset.videoStream, resolution);
|
||||||
|
const codecString = getCodecString(codec, width, height, fps);
|
||||||
lines.push(
|
lines.push(
|
||||||
`#EXT-X-STREAM-INF:BANDWIDTH=${bitrate},RESOLUTION=${width}x${height},CODECS="${codecString},mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=${fps}`,
|
`#EXT-X-STREAM-INF:BANDWIDTH=${Math.round(bitrate * 1.35)},RESOLUTION=${width}x${height},CODECS="${codecString},mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=${roundedFps}`,
|
||||||
`${sessionId}/${i}/playlist.m3u8`,
|
`${sessionId}/${i}/playlist.m3u8`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -532,8 +532,7 @@ export class IntegrityService extends BaseService {
|
|||||||
const { count } = await this.integrityRepository.getAssetCount();
|
const { count } = await this.integrityRepository.getAssetCount();
|
||||||
const checkpoint = await this.systemMetadataRepository.get(SystemMetadataKey.IntegrityChecksumCheckpoint);
|
const checkpoint = await this.systemMetadataRepository.get(SystemMetadataKey.IntegrityChecksumCheckpoint);
|
||||||
|
|
||||||
let startMarker: Date | undefined = checkpoint?.date ? new Date(checkpoint.date) : undefined;
|
const startMarker = checkpoint?.date ? new Date(checkpoint.date) : undefined;
|
||||||
let endMarker: Date | undefined;
|
|
||||||
|
|
||||||
const printStats = () => {
|
const printStats = () => {
|
||||||
const averageTime = ((Date.now() - startedAt) / processed).toFixed(2);
|
const averageTime = ((Date.now() - startedAt) / processed).toFixed(2);
|
||||||
@@ -546,31 +545,25 @@ export class IntegrityService extends BaseService {
|
|||||||
|
|
||||||
let lastCreatedAt: Date | undefined;
|
let lastCreatedAt: Date | undefined;
|
||||||
|
|
||||||
finishEarly: do {
|
this.logger.log(`Processing assets from ${startMarker?.toISOString() ?? 'beginning'}`);
|
||||||
this.logger.log(
|
|
||||||
`Processing assets in range [${startMarker?.toISOString() ?? 'beginning'}, ${endMarker?.toISOString() ?? 'end'}]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const assets = this.integrityRepository.streamAssetChecksums(startMarker, endMarker);
|
const assets = this.integrityRepository.streamAssetChecksums(startMarker);
|
||||||
endMarker = startMarker;
|
|
||||||
startMarker = undefined;
|
|
||||||
|
|
||||||
for await (const { originalPath, checksum, createdAt, assetId, reportId } of assets) {
|
for await (const { originalPath, checksum, createdAt, assetId, reportId } of assets) {
|
||||||
await this.checkAssetChecksum(originalPath, checksum, assetId, reportId);
|
await this.checkAssetChecksum(originalPath, checksum, assetId, reportId);
|
||||||
|
|
||||||
processed++;
|
processed++;
|
||||||
|
|
||||||
if (processed % 100 === 0) {
|
if (processed % 100 === 0) {
|
||||||
printStats();
|
printStats();
|
||||||
}
|
|
||||||
|
|
||||||
if (Date.now() > startedAt + timeLimit || processed > count * percentageLimit) {
|
|
||||||
this.logger.log('Reached stop criteria.');
|
|
||||||
lastCreatedAt = createdAt;
|
|
||||||
break finishEarly;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} while (endMarker);
|
|
||||||
|
if (Date.now() > startedAt + timeLimit || processed > count * percentageLimit) {
|
||||||
|
this.logger.log('Reached stop criteria.');
|
||||||
|
lastCreatedAt = createdAt;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, {
|
await this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, {
|
||||||
date: lastCreatedAt?.toISOString(),
|
date: lastCreatedAt?.toISOString(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
CQMode,
|
CQMode,
|
||||||
|
HlsVideoResolution,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
OAuthTokenEndpointAuthMethod,
|
OAuthTokenEndpointAuthMethod,
|
||||||
@@ -76,6 +77,8 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
tonemap: ToneMapping.Hable,
|
tonemap: ToneMapping.Hable,
|
||||||
realtime: {
|
realtime: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
videoCodecs: [VideoCodec.H264, VideoCodec.Hevc],
|
||||||
|
resolutions: [HlsVideoResolution.p480, HlsVideoResolution.p720, HlsVideoResolution.p1080],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
integrityChecks: {
|
integrityChecks: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AUDIO_ENCODER, SUPPORTED_HWA_CODECS } from 'src/constants';
|
import { AUDIO_ENCODER, AV1_LEVELS, CodecLevel, H264_LEVELS, HEVC_LEVELS, SUPPORTED_HWA_CODECS } from 'src/constants';
|
||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
import {
|
import {
|
||||||
ColorMatrix,
|
ColorMatrix,
|
||||||
@@ -36,6 +36,29 @@ export const getOutputSize = (videoStream: VideoStreamInfo, targetRes: number) =
|
|||||||
return isVideoVertical(videoStream) ? { width: targetRes, height: larger } : { width: larger, height: targetRes };
|
return isVideoVertical(videoStream) ? { width: targetRes, height: larger } : { width: larger, height: targetRes };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pickLevel = (levels: CodecLevel[], frame: number, rate: number) =>
|
||||||
|
levels.find((level) => frame <= level.maxFrame && rate <= level.maxRate) ?? levels.at(-1)!;
|
||||||
|
|
||||||
|
export const getCodecString = (codec: VideoCodec, width: number, height: number, fps: number): string => {
|
||||||
|
switch (codec) {
|
||||||
|
case VideoCodec.H264: {
|
||||||
|
const macroblocks = Math.ceil(width / 16) * Math.ceil(height / 16);
|
||||||
|
return `avc1.6400${pickLevel(H264_LEVELS, macroblocks, macroblocks * fps).token}`;
|
||||||
|
}
|
||||||
|
case VideoCodec.Hevc: {
|
||||||
|
const samples = width * height;
|
||||||
|
return `hvc1.1.6.${pickLevel(HEVC_LEVELS, samples, samples * fps).token}.B0`;
|
||||||
|
}
|
||||||
|
case VideoCodec.Av1: {
|
||||||
|
const samples = width * height;
|
||||||
|
return `av01.0.${pickLevel(AV1_LEVELS, samples, samples * fps).token}.08`;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Codec '${codec}' does not support HLS codec strings`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export class BaseConfig implements VideoCodecSWConfig {
|
export class BaseConfig implements VideoCodecSWConfig {
|
||||||
readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast'];
|
readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast'];
|
||||||
protected constructor(
|
protected constructor(
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { createHash, randomUUID } from 'node:crypto';
|
import { createHash, randomUUID } from 'node:crypto';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import { text } from 'node:stream/consumers';
|
import { text } from 'node:stream/consumers';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { AssetFileType, IntegrityReport, JobName, JobStatus } from 'src/enum';
|
import { AssetFileType, IntegrityReport, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { EventRepository } from 'src/repositories/event.repository';
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
@@ -702,6 +703,30 @@ describe(IntegrityService.name, () => {
|
|||||||
ctx.get(IntegrityRepository).getIntegrityReport({ limit: 100 }, IntegrityReport.ChecksumFail),
|
ctx.get(IntegrityRepository).getIntegrityReport({ limit: 100 }, IntegrityReport.ChecksumFail),
|
||||||
).resolves.toEqual({ items: [], nextCursor: undefined });
|
).resolves.toEqual({ items: [], nextCursor: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should continue from checkpoint', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const job = ctx.getMock(JobRepository);
|
||||||
|
job.queue.mockResolvedValue();
|
||||||
|
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
|
||||||
|
await ctx.newAsset({
|
||||||
|
ownerId: user.id,
|
||||||
|
createdAt: DateTime.now().minus({ days: 1 }).toISO(),
|
||||||
|
originalPath: '/foo/bar',
|
||||||
|
});
|
||||||
|
await ctx.newAsset({ ownerId: user.id, originalPath: '/foo/baz' });
|
||||||
|
|
||||||
|
await ctx
|
||||||
|
.get(SystemMetadataRepository)
|
||||||
|
.set(SystemMetadataKey.IntegrityChecksumCheckpoint, { date: DateTime.now().minus({ minutes: 5 }).toISO() });
|
||||||
|
|
||||||
|
await sut.handleChecksumFiles({ refreshOnly: false });
|
||||||
|
|
||||||
|
expect(ctx.getMock(StorageRepository).createPlainReadStream).not.toHaveBeenCalledWith('/foo/bar');
|
||||||
|
expect(ctx.getMock(StorageRepository).createPlainReadStream).toHaveBeenCalledWith('/foo/baz');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleChecksumRefresh', () => {
|
describe('handleChecksumRefresh', () => {
|
||||||
|
|||||||
@@ -107,6 +107,6 @@
|
|||||||
<Input bind:value={() => getValue<string>(), setValue} />
|
<Input bind:value={() => getValue<string>(), setValue} />
|
||||||
</Field>
|
</Field>
|
||||||
{:else}
|
{:else}
|
||||||
<Text>Unknown schema</Text>
|
<Text>{$t('unknown_schema')}</Text>
|
||||||
<CodeBlock code={JSON.stringify(schema, null, 2)} />
|
<CodeBlock code={JSON.stringify(schema, null, 2)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -37,18 +37,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#await valuePromise}
|
{#await valuePromise}
|
||||||
<div class="relative mx-auto font-mono text-2xl font-medium">
|
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="0">
|
||||||
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros()}</span>
|
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros()}</span>
|
||||||
</div>
|
</div>
|
||||||
{:then data}
|
{:then data}
|
||||||
<div class="relative mx-auto font-mono text-2xl font-medium">
|
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="{data.value} {data.unit ?? ''}">
|
||||||
<span class="text-gray-300 dark:text-gray-600">{zeros(data)}</span><span>{data.value}</span>
|
<span class="text-gray-300 dark:text-gray-600">{zeros(data)}</span><span>{data.value}</span>
|
||||||
{#if data.unit}
|
{#if data.unit}
|
||||||
<code class="font-mono text-base font-normal">{data.unit}</code>
|
<code class="font-mono text-base font-normal">{data.unit}</code>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:catch _}
|
{:catch _}
|
||||||
<div class="relative mx-auto font-mono text-2xl font-medium">
|
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="0">
|
||||||
<span class="text-gray-300 dark:text-gray-600">{zeros()}</span>
|
<span class="text-gray-300 dark:text-gray-600">{zeros()}</span>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@@ -58,5 +58,14 @@ describe('i18n', () => {
|
|||||||
expect(getClosestAvailableLocale(['sr_Cyrl'], allLocales)).toBe('sr_Cyrl');
|
expect(getClosestAvailableLocale(['sr_Cyrl'], allLocales)).toBe('sr_Cyrl');
|
||||||
expect(getClosestAvailableLocale(['zh_Hant'], allLocales)).toBe('zh_Hant');
|
expect(getClosestAvailableLocale(['zh_Hant'], allLocales)).toBe('zh_Hant');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle language aliases', () => {
|
||||||
|
const allLocales = ['zh-Hans', 'zh-Hant'];
|
||||||
|
expect(getClosestAvailableLocale(['zh-CN'], allLocales)).toBe('zh-Hans');
|
||||||
|
expect(getClosestAvailableLocale(['zh-HK'], allLocales)).toBe('zh-Hant');
|
||||||
|
expect(getClosestAvailableLocale(['zh-MO'], allLocales)).toBe('zh-Hant');
|
||||||
|
expect(getClosestAvailableLocale(['zh-SG'], allLocales)).toBe('zh-Hans');
|
||||||
|
expect(getClosestAvailableLocale(['zh-TW'], allLocales)).toBe('zh-Hant');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import {
|
|||||||
fromTimelinePlainDate,
|
fromTimelinePlainDate,
|
||||||
fromTimelinePlainDateTime,
|
fromTimelinePlainDateTime,
|
||||||
fromTimelinePlainYearMonth,
|
fromTimelinePlainYearMonth,
|
||||||
fromISODateTimeUTCToObject,
|
|
||||||
getTimes,
|
getTimes,
|
||||||
setDifference,
|
setDifference,
|
||||||
type TimelineDateTime,
|
type TimelineDateTime,
|
||||||
type TimelineYearMonth,
|
type TimelineYearMonth,
|
||||||
getOrderingDate,
|
getOrderingDate,
|
||||||
|
fromISODateTimeUTC,
|
||||||
} from '$lib/utils/timeline-util';
|
} from '$lib/utils/timeline-util';
|
||||||
import { GroupInsertionCache } from './group-insertion-cache.svelte';
|
import { GroupInsertionCache } from './group-insertion-cache.svelte';
|
||||||
import { TimelineDay } from './timeline-day.svelte';
|
import { TimelineDay } from './timeline-day.svelte';
|
||||||
@@ -190,7 +190,7 @@ export class TimelineMonth {
|
|||||||
isVideo: !bucketAssets.isImage[i],
|
isVideo: !bucketAssets.isImage[i],
|
||||||
livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
|
livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
|
||||||
localDateTime,
|
localDateTime,
|
||||||
createdAt: fromISODateTimeUTCToObject(bucketAssets.createdAt[i]),
|
createdAt: fromISODateTimeUTC(bucketAssets.createdAt[i]).toLocal().toObject(),
|
||||||
fileCreatedAt,
|
fileCreatedAt,
|
||||||
ownerId: bucketAssets.ownerId[i],
|
ownerId: bucketAssets.ownerId[i],
|
||||||
projectionType: bucketAssets.projectionType[i],
|
projectionType: bucketAssets.projectionType[i],
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
options={[
|
options={[
|
||||||
{ label: $t('role_editor'), value: AlbumUserRole.Editor },
|
{ label: $t('role_editor'), value: AlbumUserRole.Editor },
|
||||||
{ label: $t('role_viewer'), value: AlbumUserRole.Viewer },
|
{ label: $t('role_viewer'), value: AlbumUserRole.Viewer },
|
||||||
{ label: $t('owner'), value: AlbumUserRole.Owner },
|
{ label: $t('owner'), value: AlbumUserRole.Owner, disabled: true },
|
||||||
{ label: $t('remove_user'), value: 'none' },
|
{ label: $t('remove_user'), value: 'none' },
|
||||||
] as SelectOption<AlbumUserRole | 'none'>[]}
|
] as SelectOption<AlbumUserRole | 'none'>[]}
|
||||||
onChange={(value) => handleRoleSelect(user, value)}
|
onChange={(value) => handleRoleSelect(user, value)}
|
||||||
|
|||||||
@@ -134,10 +134,13 @@ const notifyAddToAlbum = ($t: MessageFormatter, albumId: string, assetIds: strin
|
|||||||
const successCount = results.filter(({ success }) => success).length;
|
const successCount = results.filter(({ success }) => success).length;
|
||||||
const duplicateCount = results.filter(({ error }) => error === 'duplicate').length;
|
const duplicateCount = results.filter(({ error }) => error === 'duplicate').length;
|
||||||
let description = $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } });
|
let description = $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } });
|
||||||
if (successCount > 0) {
|
|
||||||
description = $t('assets_added_to_album_count', { values: { count: successCount } });
|
if (duplicateCount === assetIds.length) {
|
||||||
} else if (duplicateCount > 0) {
|
|
||||||
description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } });
|
description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } });
|
||||||
|
} else if (successCount === assetIds.length) {
|
||||||
|
description = $t('assets_added_to_album_count', { values: { count: successCount } });
|
||||||
|
} else if (successCount > 0) {
|
||||||
|
description = $t('assets_added_to_album_partial_count', { values: { successCount, totalCount: assetIds.length } });
|
||||||
}
|
}
|
||||||
|
|
||||||
toastManager.primary(
|
toastManager.primary(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user