Compare commits
38 Commits
feat/rotat
...
feat/share
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b0684ee9c | ||
|
|
7bf142dc43 | ||
|
|
d8cda6ee40 | ||
|
|
a31bc94460 | ||
|
|
516709ffe1 | ||
|
|
425cf62482 | ||
|
|
58242b3b4a | ||
|
|
9d4aee36e2 | ||
|
|
70d08a2b2a | ||
|
|
f1b98d5f45 | ||
|
|
749eff03d5 | ||
|
|
5f257b9a84 | ||
|
|
0cae20033c | ||
|
|
115ee0d6cc | ||
|
|
bfdd6eac01 | ||
|
|
9eab770e79 | ||
|
|
efd8d8b884 | ||
|
|
25e1c8cc7f | ||
|
|
7c26663013 | ||
|
|
2c88ce8559 | ||
|
|
50b072803d | ||
|
|
1689cecaf7 | ||
|
|
5cd1018db3 | ||
|
|
31e6270a28 | ||
|
|
b3fbd0809b | ||
|
|
129a4a82e0 | ||
|
|
924d11a913 | ||
|
|
425c87bce4 | ||
|
|
25fcda6eeb | ||
|
|
f386b4d377 | ||
|
|
c524fcf084 | ||
|
|
194c567a45 | ||
|
|
411f96ef49 | ||
|
|
4f912de018 | ||
|
|
47203d2760 | ||
|
|
8ab87a8803 | ||
|
|
5b4f894211 | ||
|
|
b1f05fc18b |
4
.github/workflows/build-mobile.yml
vendored
4
.github/workflows/build-mobile.yml
vendored
@@ -29,9 +29,11 @@ jobs:
|
||||
filters: |
|
||||
mobile:
|
||||
- 'mobile/**'
|
||||
workflow:
|
||||
- '.github/workflows/build-mobile.yml'
|
||||
- name: Check if we should force jobs to run
|
||||
id: should_force
|
||||
run: echo "should_force=${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build-sign-android:
|
||||
name: Build and sign Android
|
||||
|
||||
73
.github/workflows/docker-cleanup.yml
vendored
73
.github/workflows/docker-cleanup.yml
vendored
@@ -1,73 +0,0 @@
|
||||
# This workflow runs on certain conditions to check for and potentially
|
||||
# delete container images from the GHCR which no longer have an associated
|
||||
# code branch.
|
||||
# Requires a PAT with the correct scope set in the secrets.
|
||||
#
|
||||
# This workflow will not trigger runs on forked repos.
|
||||
|
||||
name: Docker Cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- "closed"
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/docker-cleanup.yml"
|
||||
|
||||
concurrency:
|
||||
group: registry-tags-cleanup
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
cleanup-images:
|
||||
name: Cleanup Stale Images Tags for ${{ matrix.primary-name }}
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- primary-name: "immich-server"
|
||||
- primary-name: "immich-machine-learning"
|
||||
env:
|
||||
# Requires a personal access token with the OAuth scope delete:packages
|
||||
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
|
||||
steps:
|
||||
- name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.9.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
is_org: "true"
|
||||
do_delete: "true"
|
||||
package_name: "${{ matrix.primary-name }}"
|
||||
scheme: "pull_request"
|
||||
repo_name: "immich"
|
||||
match_regex: '^pr-(\d+)$|^(\d+)$'
|
||||
|
||||
cleanup-untagged-images:
|
||||
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- cleanup-images
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- primary-name: "immich-server"
|
||||
- primary-name: "immich-machine-learning"
|
||||
- primary-name: "immich-build-cache"
|
||||
env:
|
||||
# Requires a personal access token with the OAuth scope delete:packages
|
||||
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
|
||||
steps:
|
||||
- name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.9.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
do_delete: "true"
|
||||
is_org: "true"
|
||||
package_name: "${{ matrix.primary-name }}"
|
||||
355
.github/workflows/docker.yml
vendored
355
.github/workflows/docker.yml
vendored
@@ -36,10 +36,12 @@ jobs:
|
||||
- 'i18n/**'
|
||||
machine-learning:
|
||||
- 'machine-learning/**'
|
||||
workflow:
|
||||
- '.github/workflows/docker.yml'
|
||||
|
||||
- name: Check if we should force jobs to run
|
||||
id: should_force
|
||||
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
retag_ml:
|
||||
name: Re-Tag ML
|
||||
@@ -61,8 +63,10 @@ jobs:
|
||||
REGISTRY_NAME="ghcr.io"
|
||||
REPOSITORY=${{ github.repository_owner }}/immich-machine-learning
|
||||
TAG_OLD=main${{ matrix.suffix }}
|
||||
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
|
||||
retag_server:
|
||||
name: Re-Tag Server
|
||||
@@ -84,133 +88,49 @@ jobs:
|
||||
REGISTRY_NAME="ghcr.io"
|
||||
REPOSITORY=${{ github.repository_owner }}/immich-server
|
||||
TAG_OLD=main${{ matrix.suffix }}
|
||||
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
|
||||
build_and_push_ml:
|
||||
name: Build and Push ML
|
||||
needs: pre-job
|
||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
image: immich-machine-learning
|
||||
context: machine-learning
|
||||
file: machine-learning/Dockerfile
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
|
||||
strategy:
|
||||
# Prevent a failure in one image from stopping the other builds
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platforms: linux/amd64,linux/arm64
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
device: cpu
|
||||
|
||||
- platforms: linux/amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
device: cpu
|
||||
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
device: cuda
|
||||
suffix: -cuda
|
||||
|
||||
- platforms: linux/amd64
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
device: openvino
|
||||
suffix: -openvino
|
||||
|
||||
- platforms: linux/arm64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
device: armnn
|
||||
suffix: -armnn
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.9.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
# Skip when PR from a fork
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate docker image tags
|
||||
id: metadata
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
# Disable latest tag
|
||||
latest=false
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
|
||||
name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
# Tag with branch name
|
||||
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
||||
# Tag with pr-number
|
||||
type=ref,event=pr,suffix=${{ matrix.suffix }}
|
||||
# Tag with git tag on release
|
||||
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
||||
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
||||
|
||||
- name: Determine build cache output
|
||||
id: cache-target
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
# Essentially just ignore the cache output (PR can't write to registry cache)
|
||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
# Skip pushing when PR from a fork
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
build-args: |
|
||||
DEVICE=${{ matrix.device }}
|
||||
BUILD_ID=${{ github.run_id }}
|
||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||
|
||||
|
||||
build_and_push_server:
|
||||
name: Build and Push Server
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: pre-job
|
||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||
env:
|
||||
image: immich-server
|
||||
context: .
|
||||
file: server/Dockerfile
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||
DOCKER_REPO: altran1502/immich-server
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -221,37 +141,49 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
uses: docker/setup-buildx-action@v3.9.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
# Skip when PR from a fork
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate cache key suffix
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Generate cache target
|
||||
id: cache-target
|
||||
run: |
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
# Essentially just ignore the cache output (forks can't write to registry cache)
|
||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
id: build
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
# Skip pushing when PR from a fork
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.GHCR_REPO }},${{ env.DOCKER_REPO }}",push-by-digest=true,name-canonical=true,push=true
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }}
|
||||
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-main
|
||||
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
|
||||
build-args: |
|
||||
DEVICE=cpu
|
||||
DEVICE=${{ matrix.device }}
|
||||
BUILD_ID=${{ github.run_id }}
|
||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||
@@ -266,29 +198,40 @@ jobs:
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
name: ml-digests-${{ matrix.device }}-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge_server:
|
||||
name: Merge & Push Server
|
||||
merge_ml:
|
||||
name: Merge & Push ML
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||
env:
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||
DOCKER_REPO: altran1502/immich-server
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
|
||||
DOCKER_REPO: altran1502/immich-machine-learning
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- device: cpu
|
||||
- device: cuda
|
||||
suffix: -cuda
|
||||
- device: openvino
|
||||
suffix: -openvino
|
||||
- device: armnn
|
||||
suffix: -armnn
|
||||
needs:
|
||||
- build_and_push_server
|
||||
- build_and_push_ml
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
pattern: ml-digests-${{ matrix.device }}-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -319,6 +262,8 @@ jobs:
|
||||
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
||||
# Tag with pr-number
|
||||
type=ref,event=pr,suffix=${{ matrix.suffix }}
|
||||
# Tag with long commit sha hash
|
||||
type=sha,format=long,prefix=commit-,suffix=${{ matrix.suffix }}
|
||||
# Tag with git tag on release
|
||||
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
||||
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
||||
@@ -327,8 +272,158 @@ jobs:
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *) \
|
||||
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
|
||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
||||
|
||||
build_and_push_server:
|
||||
name: Build and Push Server
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: pre-job
|
||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||
env:
|
||||
image: immich-server
|
||||
context: .
|
||||
file: server/Dockerfile
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate cache key suffix
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Generate cache target
|
||||
id: cache-target
|
||||
run: |
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
# Essentially just ignore the cache output (forks can't write to registry cache)
|
||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
id: build
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }}
|
||||
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-main
|
||||
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
|
||||
build-args: |
|
||||
DEVICE=cpu
|
||||
BUILD_ID=${{ github.run_id }}
|
||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: server-digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge_server:
|
||||
name: Merge & Push Server
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||
env:
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||
DOCKER_REPO: altran1502/immich-server
|
||||
needs:
|
||||
- build_and_push_server
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: server-digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate docker image tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
# Disable latest tag
|
||||
latest=false
|
||||
images: |
|
||||
name=${{ env.GHCR_REPO }}
|
||||
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
# Tag with branch name
|
||||
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
||||
# Tag with pr-number
|
||||
type=ref,event=pr,suffix=${{ matrix.suffix }}
|
||||
# Tag with long commit sha hash
|
||||
type=sha,format=long,prefix=commit-,suffix=${{ matrix.suffix }}
|
||||
# Tag with git tag on release
|
||||
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
||||
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
||||
|
||||
success-check-server:
|
||||
name: Docker Build & Push Server Success
|
||||
@@ -345,7 +440,7 @@ jobs:
|
||||
|
||||
success-check-ml:
|
||||
name: Docker Build & Push ML Success
|
||||
needs: [build_and_push_ml, retag_ml]
|
||||
needs: [merge_ml, retag_ml]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
|
||||
6
.github/workflows/docs-build.yml
vendored
6
.github/workflows/docs-build.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
pre-job:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -25,9 +25,11 @@ jobs:
|
||||
filters: |
|
||||
docs:
|
||||
- 'docs/**'
|
||||
workflow:
|
||||
- '.github/workflows/docs-build.yml'
|
||||
- name: Check if we should force jobs to run
|
||||
id: should_force
|
||||
run: echo "should_force=${{ github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
|
||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Docs Build
|
||||
|
||||
4
.github/workflows/static_analysis.yml
vendored
4
.github/workflows/static_analysis.yml
vendored
@@ -23,9 +23,11 @@ jobs:
|
||||
filters: |
|
||||
mobile:
|
||||
- 'mobile/**'
|
||||
workflow:
|
||||
- '.github/workflows/static_analysis.yml'
|
||||
- name: Check if we should force jobs to run
|
||||
id: should_force
|
||||
run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
mobile-dart-analyze:
|
||||
name: Run Dart Code Analysis
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -43,10 +43,12 @@ jobs:
|
||||
- 'mobile/**'
|
||||
machine-learning:
|
||||
- 'machine-learning/**'
|
||||
workflow:
|
||||
- '.github/workflows/test.yml'
|
||||
|
||||
- name: Check if we should force jobs to run
|
||||
id: should_force
|
||||
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
server-unit-tests:
|
||||
name: Test & Lint Server
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.13.1
|
||||
22.14.0
|
||||
|
||||
135
cli/package-lock.json
generated
135
cli/package-lock.json
generated
@@ -24,7 +24,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
@@ -59,7 +59,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -881,9 +881,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
|
||||
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
|
||||
"version": "9.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
|
||||
"integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1482,9 +1482,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
|
||||
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
|
||||
"version": "22.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
|
||||
"integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1498,17 +1498,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz",
|
||||
"integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz",
|
||||
"integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.23.0",
|
||||
"@typescript-eslint/type-utils": "8.23.0",
|
||||
"@typescript-eslint/utils": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||
"@typescript-eslint/scope-manager": "8.24.0",
|
||||
"@typescript-eslint/type-utils": "8.24.0",
|
||||
"@typescript-eslint/utils": "8.24.0",
|
||||
"@typescript-eslint/visitor-keys": "8.24.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1528,16 +1528,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz",
|
||||
"integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz",
|
||||
"integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.23.0",
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/typescript-estree": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||
"@typescript-eslint/scope-manager": "8.24.0",
|
||||
"@typescript-eslint/types": "8.24.0",
|
||||
"@typescript-eslint/typescript-estree": "8.24.0",
|
||||
"@typescript-eslint/visitor-keys": "8.24.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1553,14 +1553,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz",
|
||||
"integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz",
|
||||
"integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0"
|
||||
"@typescript-eslint/types": "8.24.0",
|
||||
"@typescript-eslint/visitor-keys": "8.24.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1571,14 +1571,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz",
|
||||
"integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz",
|
||||
"integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.23.0",
|
||||
"@typescript-eslint/utils": "8.23.0",
|
||||
"@typescript-eslint/typescript-estree": "8.24.0",
|
||||
"@typescript-eslint/utils": "8.24.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.0.1"
|
||||
},
|
||||
@@ -1595,9 +1595,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz",
|
||||
"integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz",
|
||||
"integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1609,14 +1609,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz",
|
||||
"integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz",
|
||||
"integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||
"@typescript-eslint/types": "8.24.0",
|
||||
"@typescript-eslint/visitor-keys": "8.24.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1636,16 +1636,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz",
|
||||
"integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz",
|
||||
"integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.23.0",
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/typescript-estree": "8.23.0"
|
||||
"@typescript-eslint/scope-manager": "8.24.0",
|
||||
"@typescript-eslint/types": "8.24.0",
|
||||
"@typescript-eslint/typescript-estree": "8.24.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1660,13 +1660,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz",
|
||||
"integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz",
|
||||
"integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/types": "8.24.0",
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2334,18 +2334,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
|
||||
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
|
||||
"version": "9.20.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz",
|
||||
"integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.19.0",
|
||||
"@eslint/core": "^0.10.0",
|
||||
"@eslint/core": "^0.11.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "9.19.0",
|
||||
"@eslint/js": "9.20.0",
|
||||
"@eslint/plugin-kit": "^0.2.5",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -2500,6 +2500,19 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/@eslint/core": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz",
|
||||
"integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@@ -2828,9 +2841,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "15.14.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
|
||||
"integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
|
||||
"version": "15.15.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
|
||||
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3569,9 +3582,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz",
|
||||
"integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
@@ -67,6 +67,6 @@
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.1"
|
||||
"node": "22.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
# See:
|
||||
#
|
||||
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
|
||||
#
|
||||
# Make sure to use the docker-compose.yml of the current release:
|
||||
#
|
||||
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
#
|
||||
# The compose file on main may not be compatible with the latest release.
|
||||
|
||||
# For development see:
|
||||
# - https://immich.app/docs/developer/setup
|
||||
# - https://immich.app/docs/developer/troubleshooting
|
||||
|
||||
@@ -107,7 +116,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
|
||||
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
#
|
||||
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
|
||||
#
|
||||
# Make sure to use the docker-compose.yml of the current release:
|
||||
#
|
||||
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
#
|
||||
# The compose file on main may not be compatible with the latest release.
|
||||
|
||||
name: immich-prod
|
||||
|
||||
services:
|
||||
@@ -47,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
|
||||
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
@@ -91,7 +100,7 @@ services:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:6559acbd5d770b15bb3c954629ce190ac3cbbdb2b7f1c30f0385c4e05104e218
|
||||
image: prom/prometheus@sha256:5888c188cf09e3f7eebc97369c3b2ce713e844cdbd88ccf36f5047c958aea120
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#
|
||||
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
||||
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
|
||||
#
|
||||
# Make sure to use the docker-compose.yml of the current release:
|
||||
#
|
||||
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
#
|
||||
# The compose file on main may not be compatible with the latest release.
|
||||
#
|
||||
|
||||
name: immich
|
||||
|
||||
@@ -48,7 +49,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
|
||||
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.13.1
|
||||
22.14.0
|
||||
|
||||
@@ -70,4 +70,4 @@ When installing a new version of pgvecto.rs, you will need to manually update th
|
||||
|
||||
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO <immichdbusername>;`.
|
||||
|
||||
[vectors-install]: https://docs.pgvecto.rs/getting-started/installation.html
|
||||
[vectors-install]: https://docs.vectorchord.ai/getting-started/installation.html
|
||||
|
||||
76
docs/docs/install/synology.md
Normal file
76
docs/docs/install/synology.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
sidebar_position: 85
|
||||
---
|
||||
|
||||
# Synology [Community]
|
||||
|
||||
:::note
|
||||
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
|
||||
|
||||
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
|
||||
|
||||
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
|
||||
:::
|
||||
|
||||
Immich can easily be installed on a Synology NAS using Container Manager within DSM. If you have not installed Container Manager already, you can install it in the Packages Center. Refer to the [Container Manager docs](https://kb.synology.com/en-us/DSM/help/ContainerManager/docker_desc?version=7) for more information on using Container Manager.
|
||||
|
||||
## Step 1 - Download the required files
|
||||
|
||||
Create a directory of your choice (e.g. `./immich-app`) to house Immich. In general, it's a best practice to have all Docker-based applications running under the `./docker` directory, so in this case, your directory structure will look like `./docker/immich-app`.
|
||||
|
||||
Now create a `./postgres` and `./library` directory as sub-directories of the `./docker/immich-app`.
|
||||
|
||||
When you're all done, you should have the following:
|
||||
|
||||
- `./docker/immich-app/postgres`
|
||||
- `./docker/immich-app/library`
|
||||
|
||||
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) to your computer. Upload the files to the `./docker/immich-app` directory.
|
||||
|
||||
## Step 2 - Populate the .env file with custom values
|
||||
|
||||
Follow [Step 2 in Docker Compose](./docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue.
|
||||
|
||||
## Step 3 - Create a new project in Container Manager
|
||||
|
||||
Open Container Manager, and select the "**Project**" action on the left navigation bar and then click "**Create**".
|
||||

|
||||
|
||||
In the settings of your new project, set "**Project name**" to a name you'll remember, such as _immich-app_. When setting the "**Path**", select the `./docker/immich-app` directory you created earlier. Doing so will prompt a message to use the existing `docker-compose.yml` already present in the directory for your project. Click "**OK**" to continue.
|
||||
|
||||

|
||||
|
||||
The following screen will give you the option to further customize your `docker-compose.yml` file, giving you a warning regarding the `start_interval` property. Under the `healthcheck` heading, remove the `start_interval: 30s` completely and click "**Next**".
|
||||
|
||||

|
||||
|
||||
Skip the section asking to set-up a portal for Web Station, and then complete the wizard which will build and start the containers for your project.
|
||||
|
||||
Once your containers are successfully running, navigate to the "**Container**" section of Container Manager, right-click on the "**immich-server**" container, and choose the "**Details**".
|
||||
|
||||
Scroll to the bottom of the "**Details**" section, and find the `IP Address` of the container, located in the `Network` section. Take note of the container's IP address as you will need it for **Step 4**.
|
||||
|
||||

|
||||
|
||||
## Step 4 - Configure Firewall Settings
|
||||
|
||||
Once your project completes the build process, your containers will start. In order to be able to access Immich from your browser, you need to configure the firewall settings for your Synology NAS.
|
||||
|
||||
Open "**Control Panel**" on your Synology NAS, and select "**Security**". Navigate to "**Firewall**"
|
||||
|
||||

|
||||
|
||||
Click "**Edit Rules**" and add the following firewall rules:
|
||||
|
||||
- Add a "**Source IP**" rule for the IP address of your container that you obtained in Step 3 above
|
||||
- Add a "**Ports**" rule for the port specified in the `docker-compose.yml`, which should be `2283`
|
||||
|
||||
## Next Steps
|
||||
|
||||
Read the [Post Installation](/docs/install/post-install.mdx) steps or setup optional features below.
|
||||
|
||||
### Setting up optional features
|
||||
|
||||
- [External Libraries](/docs/features/libraries.md): Adding your existing photo library to Immich
|
||||
- [Hardware Transcoding](/docs/features/hardware-transcoding.md): Speeding up video transcoding
|
||||
- [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich
|
||||
12
docs/package-lock.json
generated
12
docs/package-lock.json
generated
@@ -14061,9 +14061,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
|
||||
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
|
||||
"integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -15725,9 +15725,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz",
|
||||
"integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -55,6 +55,6 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.1"
|
||||
"node": "22.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
docs/static/img/synology-container-manager-container-details.png
vendored
Normal file
BIN
docs/static/img/synology-container-manager-container-details.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
BIN
docs/static/img/synology-container-manager-create-project.png
vendored
Normal file
BIN
docs/static/img/synology-container-manager-create-project.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
BIN
docs/static/img/synology-container-manager-customize-docker-compose.png
vendored
Normal file
BIN
docs/static/img/synology-container-manager-customize-docker-compose.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
docs/static/img/synology-container-manager-set-path.png
vendored
Normal file
BIN
docs/static/img/synology-container-manager-set-path.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/static/img/synology-firewall-rules.png
vendored
Normal file
BIN
docs/static/img/synology-firewall-rules.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -1 +1 @@
|
||||
22.13.1
|
||||
22.14.0
|
||||
|
||||
@@ -34,7 +34,7 @@ services:
|
||||
- 2285:2285
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
|
||||
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
|
||||
database:
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||
|
||||
174
e2e/package-lock.json
generated
174
e2e/package-lock.json
generated
@@ -15,7 +15,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.2",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -64,7 +64,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
@@ -99,7 +99,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -876,9 +876,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
|
||||
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
|
||||
"version": "9.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
|
||||
"integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1714,9 +1714,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
|
||||
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
|
||||
"version": "22.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
|
||||
"integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1878,17 +1878,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz",
|
||||
"integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz",
|
||||
"integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.23.0",
|
||||
"@typescript-eslint/type-utils": "8.23.0",
|
||||
"@typescript-eslint/utils": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||
"@typescript-eslint/scope-manager": "8.24.0",
|
||||
"@typescript-eslint/type-utils": "8.24.0",
|
||||
"@typescript-eslint/utils": "8.24.0",
|
||||
"@typescript-eslint/visitor-keys": "8.24.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1908,16 +1908,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz",
|
||||
"integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz",
|
||||
"integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.23.0",
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/typescript-estree": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||
"@typescript-eslint/scope-manager": "8.24.0",
|
||||
"@typescript-eslint/types": "8.24.0",
|
||||
"@typescript-eslint/typescript-estree": "8.24.0",
|
||||
"@typescript-eslint/visitor-keys": "8.24.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1933,14 +1933,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz",
|
||||
"integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz",
|
||||
"integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0"
|
||||
"@typescript-eslint/types": "8.24.0",
|
||||
"@typescript-eslint/visitor-keys": "8.24.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1951,14 +1951,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz",
|
||||
"integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz",
|
||||
"integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.23.0",
|
||||
"@typescript-eslint/utils": "8.23.0",
|
||||
"@typescript-eslint/typescript-estree": "8.24.0",
|
||||
"@typescript-eslint/utils": "8.24.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.0.1"
|
||||
},
|
||||
@@ -1975,9 +1975,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz",
|
||||
"integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz",
|
||||
"integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1989,14 +1989,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz",
|
||||
"integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz",
|
||||
"integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||
"@typescript-eslint/types": "8.24.0",
|
||||
"@typescript-eslint/visitor-keys": "8.24.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -2042,16 +2042,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz",
|
||||
"integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz",
|
||||
"integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.23.0",
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/typescript-estree": "8.23.0"
|
||||
"@typescript-eslint/scope-manager": "8.24.0",
|
||||
"@typescript-eslint/types": "8.24.0",
|
||||
"@typescript-eslint/typescript-estree": "8.24.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2066,13 +2066,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz",
|
||||
"integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz",
|
||||
"integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/types": "8.24.0",
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3153,18 +3153,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
|
||||
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
|
||||
"version": "9.20.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz",
|
||||
"integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.19.0",
|
||||
"@eslint/core": "^0.10.0",
|
||||
"@eslint/core": "^0.11.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "9.19.0",
|
||||
"@eslint/js": "9.20.0",
|
||||
"@eslint/plugin-kit": "^0.2.5",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -3319,6 +3319,19 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/@eslint/core": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz",
|
||||
"integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
|
||||
@@ -3806,9 +3819,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "15.14.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
|
||||
"integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
|
||||
"version": "15.15.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
|
||||
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4400,10 +4413,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/koa": {
|
||||
"version": "2.15.3",
|
||||
"resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz",
|
||||
"integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==",
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/koa/-/koa-2.15.4.tgz",
|
||||
"integrity": "sha512-7fNBIdrU2PEgLljXoPWoyY4r1e+ToWCmzS/wwMPbUNs7X+5MMET1ObhJBlUkF5uZG9B6QhM2zS1TsH6adegkiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^1.3.5",
|
||||
"cache-content-type": "^1.0.0",
|
||||
@@ -4920,9 +4934,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/oidc-provider": {
|
||||
"version": "8.6.1",
|
||||
"resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.6.1.tgz",
|
||||
"integrity": "sha512-wJ+nhwkCjRtQiwJKACjjV8FAIn7QXGDc1UOAE5WW0i8fsqN1GgXi42S/ccOxEx/JV3tyVLEwIipAvJNsJ/3djA==",
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.7.0.tgz",
|
||||
"integrity": "sha512-H0AE07n7d5zBHwP8bDb1Yg+NokP+BybisUPB2kGG/lI0aPvLL/JcEhh3vJsz3UbThoz2p+6a0xGTBA8a3yDUGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4933,7 +4947,7 @@
|
||||
"got": "^13.0.0",
|
||||
"jose": "^5.9.6",
|
||||
"jsesc": "^3.1.0",
|
||||
"koa": "^2.15.3",
|
||||
"koa": "^2.15.4",
|
||||
"nanoid": "^5.0.9",
|
||||
"object-hash": "^3.0.0",
|
||||
"oidc-token-hash": "^5.0.3",
|
||||
@@ -5193,15 +5207,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.13.1",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
|
||||
"integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
|
||||
"version": "8.13.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz",
|
||||
"integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.7.0",
|
||||
"pg-pool": "^3.7.0",
|
||||
"pg-protocol": "^1.7.0",
|
||||
"pg-pool": "^3.7.1",
|
||||
"pg-protocol": "^1.7.1",
|
||||
"pg-types": "^2.1.0",
|
||||
"pgpass": "1.x"
|
||||
},
|
||||
@@ -5253,9 +5267,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
|
||||
"integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz",
|
||||
"integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@@ -5263,9 +5277,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
|
||||
"integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz",
|
||||
"integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -5467,9 +5481,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz",
|
||||
"integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.2",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -53,6 +53,6 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.1"
|
||||
"node": "22.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
|
||||
import { LibraryResponseDto, LoginResponseDto, getAllLibraries } from '@immich/sdk';
|
||||
import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { userDto, uuidDto } from 'src/fixtures';
|
||||
@@ -8,8 +8,6 @@ import request from 'supertest';
|
||||
import { utimes } from 'utimes';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
|
||||
|
||||
describe('/libraries', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user: LoginResponseDto;
|
||||
@@ -298,6 +296,7 @@ describe('/libraries', () => {
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
@@ -313,15 +312,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
|
||||
});
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
|
||||
@@ -341,13 +332,7 @@ describe('/libraries', () => {
|
||||
exclusionPatterns: ['**/directoryA'],
|
||||
});
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -361,13 +346,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
|
||||
});
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -386,13 +365,7 @@ describe('/libraries', () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder, a/assetA.png`);
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder, b/assetB.png`);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -414,13 +387,7 @@ describe('/libraries', () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder{ a/assetA.png`);
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder} b/assetB.png`);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -472,13 +439,7 @@ describe('/libraries', () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -502,23 +463,12 @@ describe('/libraries', () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
@@ -549,21 +499,12 @@ describe('/libraries', () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
@@ -593,21 +534,14 @@ describe('/libraries', () => {
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
expect(assets.count).toBe(1);
|
||||
|
||||
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
||||
@@ -625,8 +559,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
expect(assets.count).toBe(1);
|
||||
@@ -637,13 +570,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/another-path/`],
|
||||
});
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
||||
@@ -663,8 +590,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
@@ -674,8 +600,7 @@ describe('/libraries', () => {
|
||||
|
||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/directoryB/**'] });
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
expect(trashedAsset.isTrashed).toBe(true);
|
||||
@@ -697,19 +622,12 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: assetsBefore } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
expect(assetsBefore.count).toBeGreaterThan(1);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -726,11 +644,7 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -753,10 +667,7 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -780,10 +691,7 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -807,19 +715,13 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -842,18 +744,12 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -876,18 +772,12 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -911,19 +801,13 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -947,18 +831,12 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -982,18 +860,12 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -1016,22 +888,13 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
expect(offlineAsset.isTrashed).toBe(true);
|
||||
@@ -1045,15 +908,7 @@ describe('/libraries', () => {
|
||||
|
||||
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const backOnlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
|
||||
@@ -1075,22 +930,13 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
{
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
||||
@@ -1111,15 +957,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/another-path`],
|
||||
});
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
|
||||
@@ -1143,22 +981,13 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
{
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
||||
@@ -1175,15 +1004,7 @@ describe('/libraries', () => {
|
||||
|
||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
|
||||
@@ -1303,8 +1124,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/libraries/${library.id}`)
|
||||
|
||||
@@ -204,6 +204,12 @@ describe('/shared-links', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should increment the view count', async () => {
|
||||
const request1 = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
|
||||
const request2 = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
|
||||
expect(request2.body.viewCount).toBe(request1.body.viewCount + 1);
|
||||
});
|
||||
|
||||
it('should return unauthorized for incorrect shared link', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/shared-links/me')
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
getAssetInfo,
|
||||
getConfigDefaults,
|
||||
login,
|
||||
scanLibrary,
|
||||
searchAssets,
|
||||
sendJobCommand,
|
||||
setBaseUrl,
|
||||
@@ -553,6 +554,14 @@ export const utils = {
|
||||
await immichCli(['login', app, `${key.secret}`]);
|
||||
return key.secret;
|
||||
},
|
||||
|
||||
scan: async (accessToken: string, id: string) => {
|
||||
await scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
|
||||
|
||||
await utils.waitForQueueFinish(accessToken, 'library');
|
||||
await utils.waitForQueueFinish(accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(accessToken, 'metadataExtraction');
|
||||
},
|
||||
};
|
||||
|
||||
utils.initSdk();
|
||||
|
||||
@@ -602,8 +602,6 @@
|
||||
"enable": "Enable",
|
||||
"enabled": "Enabled",
|
||||
"end_date": "End date",
|
||||
"rotate_left": "Rotate left",
|
||||
"rotate_right": "Rotate right",
|
||||
"error": "Error",
|
||||
"error_loading_image": "Error loading image",
|
||||
"error_title": "Error - Something went wrong",
|
||||
@@ -646,7 +644,6 @@
|
||||
"quota_higher_than_disk_size": "You set a quota higher than the disk size",
|
||||
"repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}",
|
||||
"unable_to_add_album_users": "Unable to add users to album",
|
||||
"unable_to_rotate_image": "Unable to rotate image",
|
||||
"unable_to_add_assets_to_shared_link": "Unable to add assets to shared link",
|
||||
"unable_to_add_comment": "Unable to add comment",
|
||||
"unable_to_add_exclusion_pattern": "Unable to add exclusion pattern",
|
||||
@@ -819,6 +816,7 @@
|
||||
"invite_people": "Invite People",
|
||||
"invite_to_album": "Invite to album",
|
||||
"items_count": "{count, plural, one {# item} other {# items}}",
|
||||
"views_count": "{count, plural, one {# view} other {# views}}",
|
||||
"jobs": "Jobs",
|
||||
"keep": "Keep",
|
||||
"keep_all": "Keep All",
|
||||
|
||||
24
i18n/it.json
24
i18n/it.json
@@ -10,14 +10,14 @@
|
||||
"activity_changed": "L'attività è {enabled, select, true {abilitata} other {disabilitata}}",
|
||||
"add": "Aggiungi",
|
||||
"add_a_description": "Aggiungi una descrizione",
|
||||
"add_a_location": "Aggiungi un luogo",
|
||||
"add_a_location": "Aggiungi una posizione",
|
||||
"add_a_name": "Aggiungi un nome",
|
||||
"add_a_title": "Aggiungi un titolo",
|
||||
"add_exclusion_pattern": "Aggiungi un pattern di esclusione",
|
||||
"add_import_path": "Aggiungi un percorso di importazione",
|
||||
"add_location": "Aggiungi posizione",
|
||||
"add_more_users": "Aggiungi altri utenti",
|
||||
"add_partner": "Aggiungi un partner",
|
||||
"add_partner": "Aggiungi partner",
|
||||
"add_path": "Aggiungi percorso",
|
||||
"add_photos": "Aggiungi foto",
|
||||
"add_to": "Aggiungi a...",
|
||||
@@ -374,11 +374,11 @@
|
||||
"album_name": "Nome Album",
|
||||
"album_options": "Impostazioni Album",
|
||||
"album_remove_user": "Rimuovi l'utente?",
|
||||
"album_remove_user_confirmation": "Sicuro di voler cancellare l'utente {user}?",
|
||||
"album_remove_user_confirmation": "Sicuro di voler rimuovere l'utente {user}?",
|
||||
"album_share_no_users": "Sembra che tu abbia condiviso questo album con tutti gli utenti oppure non hai nessun utente con cui condividere.",
|
||||
"album_updated": "Album aggiornato",
|
||||
"album_updated_setting_description": "Ricevi una notifica email quando un album condiviso ha nuovi asset",
|
||||
"album_user_left": "Abbandona {album}",
|
||||
"album_updated_setting_description": "Ricevi una notifica email quando un album condiviso ha nuovi media",
|
||||
"album_user_left": "{album} abbandonato",
|
||||
"album_user_removed": "Utente {user} rimosso",
|
||||
"album_with_link_access": "Permetti a chiunque possieda il link di visualizzare le foto e le persone dell'album.",
|
||||
"albums": "Album",
|
||||
@@ -391,10 +391,10 @@
|
||||
"allow_edits": "Permetti Modifiche",
|
||||
"allow_public_user_to_download": "Permetti agli utenti pubblici di scaricare",
|
||||
"allow_public_user_to_upload": "Permetti agli utenti pubblici di caricare",
|
||||
"anti_clockwise": "Senso Anti-Orario",
|
||||
"anti_clockwise": "Senso anti-orario",
|
||||
"api_key": "Chiave API",
|
||||
"api_key_description": "Il campo verrà mostrato solo una volta. Abbi cura di copiarlo prima di chiudere la finestra.",
|
||||
"api_key_empty": "Il Nome dell'API Key non può essere vuoto",
|
||||
"api_key_description": "Il valore verrà mostrato solo una volta. Assicurati di copiarlo prima di chiudere la finestra.",
|
||||
"api_key_empty": "Il nome della chiave API non può essere vuoto",
|
||||
"api_keys": "Chiavi API",
|
||||
"app_settings": "Impostazioni Applicazione",
|
||||
"appears_in": "Compare in",
|
||||
@@ -407,14 +407,14 @@
|
||||
"are_you_sure_to_do_this": "Sei sicuro di voler procedere?",
|
||||
"asset_added_to_album": "Aggiunto all'album",
|
||||
"asset_adding_to_album": "In aggiunta all'album...",
|
||||
"asset_description_updated": "La descrizione del media non è stata aggiornata",
|
||||
"asset_description_updated": "La descrizione del media è stata aggiornata",
|
||||
"asset_filename_is_offline": "Il media {filename} è offline",
|
||||
"asset_has_unassigned_faces": "Il media ha dei volti non categorizzati",
|
||||
"asset_hashing": "Hashing in corso ...",
|
||||
"asset_offline": "Risorsa Offline",
|
||||
"asset_offline_description": "Questo media non è stato trovato nel disco. Contatta il tuo amministratore di Immich per assistenza.",
|
||||
"asset_skipped": "Saltato",
|
||||
"asset_skipped_in_trash": "In cestino",
|
||||
"asset_skipped_in_trash": "Nel cestino",
|
||||
"asset_uploaded": "Caricato",
|
||||
"asset_uploading": "Caricamento...",
|
||||
"assets": "Risorse",
|
||||
@@ -434,7 +434,7 @@
|
||||
"back_close_deselect": "Indietro, chiudi o deseleziona",
|
||||
"backward": "Indietro",
|
||||
"birthdate_saved": "Data di nascita salvata con successo",
|
||||
"birthdate_set_description": "La data di nascita è usata per calcolare l'età di questa persona nel momento dello scatto della foto.",
|
||||
"birthdate_set_description": "La data di nascita è usata per calcolare l'età di questa persona al momento dello scatto della foto.",
|
||||
"blurred_background": "Sfondo sfocato",
|
||||
"bugs_and_feature_requests": "Bug & Richieste di nuove funzionalità",
|
||||
"build": "Compilazione",
|
||||
@@ -442,7 +442,7 @@
|
||||
"bulk_delete_duplicates_confirmation": "Sei sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati. Non puoi annullare questa operazione!",
|
||||
"bulk_keep_duplicates_confirmation": "Sei sicuro di voler tenere {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione risolverà tutti i gruppi duplicati senza cancellare nulla.",
|
||||
"bulk_trash_duplicates_confirmation": "Sei davvero sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati.",
|
||||
"buy": "Acquistare Immich",
|
||||
"buy": "Acquista Immich",
|
||||
"camera": "Fotocamera",
|
||||
"camera_brand": "Marca fotocamera",
|
||||
"camera_model": "Modello fotocamera",
|
||||
|
||||
@@ -96,8 +96,8 @@ download:
|
||||
locale_code: ro-RO
|
||||
- file: mobile/assets/i18n/id-ID.json
|
||||
locale_code: id-ID
|
||||
- file: mobile/assets/i18n/gl.json
|
||||
locale_code: gl
|
||||
- file: mobile/assets/i18n/gl-ES.json
|
||||
locale_code: gl-ES
|
||||
- file: mobile/assets/i18n/ga.json
|
||||
locale_code: ga
|
||||
- file: mobile/assets/i18n/tr-TR.json
|
||||
|
||||
@@ -77,7 +77,7 @@ custom_lint:
|
||||
- test/**.dart
|
||||
# refactor the remaining providers
|
||||
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
|
||||
- lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,search/all_motion_photos,search/recently_added_asset}.provider.dart
|
||||
- lib/providers/{asset_viewer/render_list,backup/backup,search/all_motion_photos,search/recently_added_asset}.provider.dart
|
||||
|
||||
- import_rule_openapi:
|
||||
message: openapi must only be used through ApiRepositories
|
||||
@@ -110,51 +110,81 @@ custom_lint:
|
||||
- test/**.dart
|
||||
|
||||
dart_code_metrics:
|
||||
metrics:
|
||||
cyclomatic-complexity: 20
|
||||
number-of-parameters: 4
|
||||
maximum-nesting-level: 5
|
||||
extends:
|
||||
- recommended
|
||||
rules:
|
||||
# Common
|
||||
- avoid-accessing-collections-by-constant-index
|
||||
- arguments-ordering:
|
||||
last:
|
||||
- child
|
||||
- children
|
||||
- avoid-accessing-other-classes-private-members
|
||||
- avoid-cascade-after-if-null
|
||||
- avoid-assigning-to-static-field
|
||||
- avoid-assignments-as-conditions
|
||||
- avoid-async-call-in-sync-function
|
||||
- avoid-collapsible-if
|
||||
- avoid-collection-methods-with-unrelated-types
|
||||
- avoid-double-slash-imports
|
||||
- avoid-duplicate-cascades
|
||||
- avoid-duplicate-patterns
|
||||
- avoid-generics-shadowing
|
||||
- avoid-collection-equality-checks
|
||||
- avoid-complex-loop-conditions
|
||||
- avoid-declaring-call-method
|
||||
- avoid-extensions-on-records
|
||||
- avoid-function-type-in-records
|
||||
- avoid-future-ignore
|
||||
- avoid-global-state
|
||||
- avoid-inverted-boolean-checks
|
||||
- avoid-late-final-reassignment
|
||||
- avoid-local-functions
|
||||
- avoid-negated-conditions
|
||||
- avoid-nested-streams-and-futures
|
||||
- avoid-referencing-subclasses
|
||||
- avoid-unnecessary-continue
|
||||
- avoid-unnecessary-nullable-return-type: false
|
||||
- binary-expression-operand-order
|
||||
- move-variable-outside-iteration
|
||||
- pattern-fields-ordering
|
||||
- prefer-abstract-final-static-class
|
||||
- prefer-commenting-future-delayed
|
||||
- prefer-early-return
|
||||
- prefer-first
|
||||
- prefer-immediate-return
|
||||
- prefer-last
|
||||
- prefer-simpler-boolean-expressions
|
||||
- prefer-switch-expression
|
||||
- prefer-type-over-var
|
||||
- use-existing-destructuring
|
||||
- use-existing-variable
|
||||
# Flutter
|
||||
- add-copy-with:
|
||||
file-name-pattern: '.model.dart'
|
||||
- always-remove-listener
|
||||
- avoid-border-all
|
||||
- avoid-empty-setstate
|
||||
- avoid-complex-arithmetic-expressions
|
||||
- avoid-expanded-as-spacer
|
||||
- avoid-incomplete-copy-with
|
||||
- avoid-if-with-many-branches
|
||||
- avoid-inherited-widget-in-initstate
|
||||
- avoid-late-context
|
||||
- avoid-recursive-widget-calls
|
||||
- avoid-returning-widgets
|
||||
- avoid-shrink-wrap-in-lists
|
||||
- avoid-single-child-column-or-row
|
||||
- avoid-state-constructors
|
||||
- avoid-stateless-widget-initialized-fields
|
||||
- avoid-unnecessary-overrides-in-state
|
||||
- avoid-unnecessary-stateful-widgets
|
||||
- avoid-wrapping-in-padding
|
||||
- dispose-fields
|
||||
- prefer-align-over-container
|
||||
- prefer-const-border-radius
|
||||
- prefer-correct-callback-field-name: false
|
||||
- prefer-correct-edge-insets-constructor
|
||||
- prefer-dedicated-media-query-methods
|
||||
- prefer-define-hero-tag
|
||||
- prefer-extracting-callbacks
|
||||
- prefer-single-widget-per-file:
|
||||
ignore-private-widgets: true
|
||||
- prefer-for-loop-in-children
|
||||
- prefer-match-file-name: false
|
||||
- prefer-sliver-prefix
|
||||
- prefer-spacing
|
||||
- prefer-text-rich
|
||||
- prefer-transform-over-container
|
||||
- prefer-using-list-view
|
||||
- proper-super-calls
|
||||
- use-setstate-synchronously
|
||||
- prefer-widget-private-members:
|
||||
ignore-static: true
|
||||
- use-closest-build-context
|
||||
# riverpod
|
||||
- avoid-calling-notifier-members-inside-build
|
||||
- avoid-notifier-constructors
|
||||
- avoid-ref-read-inside-build
|
||||
- avoid-ref-watch-outside-build
|
||||
- avoid-unnecessary-consumer-widgets
|
||||
- dispose-provided-instances
|
||||
- use-ref-read-synchronously
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:analyzer/error/listener.dart';
|
||||
import 'package:analyzer/error/error.dart' show ErrorSeverity;
|
||||
import 'package:analyzer/error/listener.dart';
|
||||
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:glob/glob.dart';
|
||||
@@ -65,7 +65,8 @@ class ImportRule extends DartLintRule {
|
||||
) {
|
||||
if (_rootOffset == -1) {
|
||||
const project = "/immich/mobile/";
|
||||
_rootOffset = resolver.path.indexOf(project) + project.length;
|
||||
_rootOffset =
|
||||
resolver.path.toLowerCase().indexOf(project) + project.length;
|
||||
}
|
||||
final path = resolver.path.substring(_rootOffset);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
analyzer: ^7.0.0
|
||||
analyzer: ^6.0.0
|
||||
analyzer_plugin: ^0.11.3
|
||||
custom_lint_builder: ^0.6.4
|
||||
glob: ^2.1.2
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
|
||||
abstract interface class IAlbumRepository implements IDatabaseRepository {
|
||||
Future<Album> create(Album album);
|
||||
@@ -42,6 +43,16 @@ abstract interface class IAlbumRepository implements IDatabaseRepository {
|
||||
Future<Album> recalculateMetadata(Album album);
|
||||
|
||||
Future<List<Album>> search(String searchTerm, QuickFilterMode filterMode);
|
||||
|
||||
Stream<List<Album>> watchRemoteAlbums();
|
||||
|
||||
Stream<List<Album>> watchLocalAlbums();
|
||||
|
||||
Stream<Album?> watchAlbum(int id);
|
||||
|
||||
Stream<RenderList> getRenderListStream(Album album);
|
||||
|
||||
Future<void> clearTable();
|
||||
}
|
||||
|
||||
enum AlbumSort { remoteId, localId }
|
||||
|
||||
@@ -41,7 +41,7 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
|
||||
|
||||
Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state});
|
||||
|
||||
Future<void> deleteById(List<int> ids);
|
||||
Future<void> deleteByIds(List<int> ids);
|
||||
|
||||
Future<List<Asset>> getMatches({
|
||||
required List<Asset> assets,
|
||||
@@ -57,6 +57,12 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
|
||||
Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets);
|
||||
|
||||
Future<List<String>> getAllDuplicatedAssetIds();
|
||||
|
||||
Future<List<Asset>> getStackAssets(String stackId);
|
||||
|
||||
Future<void> clearTable();
|
||||
|
||||
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false});
|
||||
}
|
||||
|
||||
enum AssetSort { checksum, ownerIdChecksum }
|
||||
|
||||
@@ -11,4 +11,6 @@ abstract interface class IETagRepository implements IDatabaseRepository {
|
||||
Future<void> upsertAll(List<ETag> etags);
|
||||
|
||||
Future<void> deleteByIds(List<String> ids);
|
||||
|
||||
Future<void> clearTable();
|
||||
}
|
||||
|
||||
@@ -9,4 +9,6 @@ abstract interface class IExifInfoRepository implements IDatabaseRepository {
|
||||
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos);
|
||||
|
||||
Future<void> delete(int id);
|
||||
|
||||
Future<void> clearTable();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ abstract interface class IUserRepository implements IDatabaseRepository {
|
||||
Future<void> deleteById(List<int> ids);
|
||||
|
||||
Future<User> me();
|
||||
|
||||
Future<void> clearTable();
|
||||
}
|
||||
|
||||
enum UserSort { id }
|
||||
|
||||
@@ -8,43 +8,40 @@ import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final isRefreshingRemoteAlbumProvider = StateProvider<bool>((ref) => false);
|
||||
|
||||
class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
AlbumNotifier(this._albumService, this.db, this.ref) : super([]) {
|
||||
final query = db.albums.filter().remoteIdIsNotNull();
|
||||
query.findAll().then((value) {
|
||||
AlbumNotifier(this.albumService, this.ref) : super([]) {
|
||||
albumService.getAllRemoteAlbums().then((value) {
|
||||
if (mounted) {
|
||||
state = value;
|
||||
}
|
||||
});
|
||||
_streamSub = query.watch().listen((data) => state = data);
|
||||
|
||||
_streamSub =
|
||||
albumService.watchRemoteAlbums().listen((data) => state = data);
|
||||
}
|
||||
|
||||
final AlbumService _albumService;
|
||||
final Isar db;
|
||||
final AlbumService albumService;
|
||||
final Ref ref;
|
||||
late final StreamSubscription<List<Album>> _streamSub;
|
||||
|
||||
Future<void> refreshRemoteAlbums() async {
|
||||
ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true;
|
||||
await _albumService.refreshRemoteAlbums();
|
||||
await albumService.refreshRemoteAlbums();
|
||||
ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false;
|
||||
}
|
||||
|
||||
Future<void> refreshDeviceAlbums() => _albumService.refreshDeviceAlbums();
|
||||
Future<void> refreshDeviceAlbums() => albumService.refreshDeviceAlbums();
|
||||
|
||||
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
|
||||
Future<bool> deleteAlbum(Album album) => albumService.deleteAlbum(album);
|
||||
|
||||
Future<Album?> createAlbum(
|
||||
String albumTitle,
|
||||
Set<Asset> assets,
|
||||
) =>
|
||||
_albumService.createAlbum(albumTitle, assets, []);
|
||||
albumService.createAlbum(albumTitle, assets, []);
|
||||
|
||||
Future<Album?> getAlbumByName(
|
||||
String albumName, {
|
||||
@@ -52,7 +49,7 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
bool? shared,
|
||||
bool? owner,
|
||||
}) =>
|
||||
_albumService.getAlbumByName(
|
||||
albumService.getAlbumByName(
|
||||
albumName,
|
||||
remote: remote,
|
||||
shared: shared,
|
||||
@@ -74,7 +71,7 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
}
|
||||
|
||||
Future<bool> leaveAlbum(Album album) async {
|
||||
var res = await _albumService.leaveAlbum(album);
|
||||
var res = await albumService.leaveAlbum(album);
|
||||
|
||||
if (res) {
|
||||
await deleteAlbum(album);
|
||||
@@ -85,15 +82,15 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
}
|
||||
|
||||
void searchAlbums(String searchTerm, QuickFilterMode filterMode) async {
|
||||
state = await _albumService.search(searchTerm, filterMode);
|
||||
state = await albumService.search(searchTerm, filterMode);
|
||||
}
|
||||
|
||||
Future<void> addUsers(Album album, List<String> userIds) async {
|
||||
await _albumService.addUsers(album, userIds);
|
||||
await albumService.addUsers(album, userIds);
|
||||
}
|
||||
|
||||
Future<bool> removeUser(Album album, User user) async {
|
||||
final isRemoved = await _albumService.removeUser(album, user);
|
||||
final isRemoved = await albumService.removeUser(album, user);
|
||||
|
||||
if (isRemoved && album.sharedUsers.isEmpty) {
|
||||
state = state.where((element) => element.id != album.id).toList();
|
||||
@@ -103,25 +100,25 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
}
|
||||
|
||||
Future<void> addAssets(Album album, Iterable<Asset> assets) async {
|
||||
await _albumService.addAssets(album, assets);
|
||||
await albumService.addAssets(album, assets);
|
||||
}
|
||||
|
||||
Future<bool> removeAsset(Album album, Iterable<Asset> assets) async {
|
||||
return await _albumService.removeAsset(album, assets);
|
||||
return await albumService.removeAsset(album, assets);
|
||||
}
|
||||
|
||||
Future<bool> setActivitystatus(
|
||||
Album album,
|
||||
bool enabled,
|
||||
) {
|
||||
return _albumService.setActivityStatus(album, enabled);
|
||||
return albumService.setActivityStatus(album, enabled);
|
||||
}
|
||||
|
||||
Future<Album?> toggleSortOrder(Album album) {
|
||||
final order =
|
||||
album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc;
|
||||
|
||||
return _albumService.updateSortOrder(album, order);
|
||||
return albumService.updateSortOrder(album, order);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -135,57 +132,49 @@ final albumProvider =
|
||||
StateNotifierProvider.autoDispose<AlbumNotifier, List<Album>>((ref) {
|
||||
return AlbumNotifier(
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
||||
final albumWatcher =
|
||||
StreamProvider.autoDispose.family<Album, int>((ref, albumId) async* {
|
||||
final db = ref.watch(dbProvider);
|
||||
final a = await db.albums.get(albumId);
|
||||
if (a != null) yield a;
|
||||
await for (final a in db.albums.watchObject(albumId, fireImmediately: true)) {
|
||||
if (a != null) yield a;
|
||||
StreamProvider.autoDispose.family<Album, int>((ref, id) async* {
|
||||
final albumService = ref.watch(albumServiceProvider);
|
||||
|
||||
final album = await albumService.getAlbumById(id);
|
||||
if (album != null) {
|
||||
yield album;
|
||||
}
|
||||
|
||||
await for (final album in albumService.watchAlbum(id)) {
|
||||
if (album != null) {
|
||||
yield album;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final albumRenderlistProvider =
|
||||
StreamProvider.autoDispose.family<RenderList, int>((ref, albumId) {
|
||||
final album = ref.watch(albumWatcher(albumId)).value;
|
||||
StreamProvider.autoDispose.family<RenderList, int>((ref, id) {
|
||||
final album = ref.watch(albumWatcher(id)).value;
|
||||
|
||||
if (album != null) {
|
||||
final query = album.assets.filter().isTrashedEqualTo(false);
|
||||
if (album.sortOrder == SortOrder.asc) {
|
||||
return renderListGeneratorWithGroupBy(
|
||||
query.sortByFileCreatedAt(),
|
||||
GroupAssetsBy.none,
|
||||
);
|
||||
} else if (album.sortOrder == SortOrder.desc) {
|
||||
return renderListGeneratorWithGroupBy(
|
||||
query.sortByFileCreatedAtDesc(),
|
||||
GroupAssetsBy.none,
|
||||
);
|
||||
}
|
||||
return ref.watch(albumServiceProvider).getRenderListGenerator(album);
|
||||
}
|
||||
|
||||
return const Stream.empty();
|
||||
});
|
||||
|
||||
class LocalAlbumsNotifier extends StateNotifier<List<Album>> {
|
||||
LocalAlbumsNotifier(this.db) : super([]) {
|
||||
final query = db.albums.where().remoteIdIsNull();
|
||||
|
||||
query.findAll().then((value) {
|
||||
LocalAlbumsNotifier(this.albumService) : super([]) {
|
||||
albumService.getAllLocalAlbums().then((value) {
|
||||
if (mounted) {
|
||||
state = value;
|
||||
}
|
||||
});
|
||||
|
||||
_streamSub = query.watch().listen((data) => state = data);
|
||||
_streamSub = albumService.watchLocalAlbums().listen((data) => state = data);
|
||||
}
|
||||
|
||||
final Isar db;
|
||||
final AlbumService albumService;
|
||||
late final StreamSubscription<List<Album>> _streamSub;
|
||||
|
||||
@override
|
||||
@@ -197,5 +186,5 @@ class LocalAlbumsNotifier extends StateNotifier<List<Album>> {
|
||||
|
||||
final localAlbumsProvider =
|
||||
StateNotifierProvider.autoDispose<LocalAlbumsNotifier, List<Album>>((ref) {
|
||||
return LocalAlbumsNotifier(ref.watch(dbProvider));
|
||||
return LocalAlbumsNotifier(ref.watch(albumServiceProvider));
|
||||
});
|
||||
|
||||
@@ -2,28 +2,40 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
import 'package:immich_mobile/services/etag.service.dart';
|
||||
import 'package:immich_mobile/services/exif.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:immich_mobile/utils/db.dart';
|
||||
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
|
||||
return AssetNotifier(
|
||||
ref.watch(assetServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(etagServiceProvider),
|
||||
ref.watch(exifServiceProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
||||
class AssetNotifier extends StateNotifier<bool> {
|
||||
final AssetService _assetService;
|
||||
final AlbumService _albumService;
|
||||
final UserService _userService;
|
||||
final SyncService _syncService;
|
||||
final Isar _db;
|
||||
final ETagService _etagService;
|
||||
final ExifService _exifService;
|
||||
final StateNotifierProviderRef _ref;
|
||||
final log = Logger('AssetNotifier');
|
||||
bool _getAllAssetInProgress = false;
|
||||
@@ -34,7 +46,8 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
this._albumService,
|
||||
this._userService,
|
||||
this._syncService,
|
||||
this._db,
|
||||
this._etagService,
|
||||
this._exifService,
|
||||
this._ref,
|
||||
) : super(false);
|
||||
|
||||
@@ -48,7 +61,7 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
_getAllAssetInProgress = true;
|
||||
state = true;
|
||||
if (clear) {
|
||||
await clearAssetsAndAlbums(_db);
|
||||
await clearAllAssets();
|
||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||
}
|
||||
final bool changedUsers = await _userService.refreshUsers();
|
||||
@@ -68,8 +81,15 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearAllAsset() {
|
||||
return clearAssetsAndAlbums(_db);
|
||||
Future<void> clearAllAssets() async {
|
||||
await Store.delete(StoreKey.assetETag);
|
||||
await Future.wait([
|
||||
_assetService.clearTable(),
|
||||
_exifService.clearTable(),
|
||||
_albumService.clearTable(),
|
||||
_userService.clearTable(),
|
||||
_etagService.clearTable(),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> onNewAssetUploaded(Asset newAsset) async {
|
||||
@@ -78,102 +98,43 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
await _syncService.syncNewAssetToDb(newAsset);
|
||||
}
|
||||
|
||||
Future<bool> deleteLocalOnlyAssets(
|
||||
Iterable<Asset> deleteAssets, {
|
||||
bool onlyBackedUp = false,
|
||||
}) async {
|
||||
Future<bool> deleteLocalAssets(List<Asset> assets) async {
|
||||
_deleteInProgress = true;
|
||||
state = true;
|
||||
try {
|
||||
// Filter the assets based on the backed-up status
|
||||
final assets = onlyBackedUp
|
||||
? deleteAssets.where((e) => e.storage == AssetState.merged)
|
||||
: deleteAssets;
|
||||
|
||||
if (assets.isEmpty) {
|
||||
return false; // No assets to delete
|
||||
}
|
||||
|
||||
// Proceed with local deletion of the filtered assets
|
||||
final localDeleted = await _deleteLocalAssets(assets);
|
||||
|
||||
if (localDeleted.isNotEmpty) {
|
||||
final localOnlyIds = assets
|
||||
.where((e) => e.storage == AssetState.local)
|
||||
.map((e) => e.id)
|
||||
.toList();
|
||||
|
||||
// Update merged assets to remote-only
|
||||
final mergedAssets =
|
||||
assets.where((e) => e.storage == AssetState.merged).map((e) {
|
||||
e.localId = null;
|
||||
return e;
|
||||
}).toList();
|
||||
|
||||
// Update the local database
|
||||
await _db.writeTxn(() async {
|
||||
if (mergedAssets.isNotEmpty) {
|
||||
await _db.assets
|
||||
.putAll(mergedAssets); // Use the filtered merged assets
|
||||
}
|
||||
await _db.exifInfos.deleteAll(localOnlyIds);
|
||||
await _db.assets.deleteAll(localOnlyIds);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
await _assetService.deleteLocalAssets(assets);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.severe("Failed to delete local assets", error);
|
||||
return false;
|
||||
} finally {
|
||||
_deleteInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> deleteRemoteOnlyAssets(
|
||||
/// Delete remote asset only
|
||||
///
|
||||
/// Default behavior is trashing the asset
|
||||
Future<bool> deleteRemoteAssets(
|
||||
Iterable<Asset> deleteAssets, {
|
||||
bool force = false,
|
||||
bool shouldDeletePermanently = false,
|
||||
}) async {
|
||||
_deleteInProgress = true;
|
||||
state = true;
|
||||
try {
|
||||
final remoteDeleted = await _deleteRemoteAssets(deleteAssets, force);
|
||||
if (remoteDeleted.isNotEmpty) {
|
||||
final assetsToUpdate = force
|
||||
|
||||
/// If force, only update merged only assets and remove remote assets
|
||||
? remoteDeleted
|
||||
.where((e) => e.storage == AssetState.merged)
|
||||
.map((e) {
|
||||
e.remoteId = null;
|
||||
return e;
|
||||
})
|
||||
// If not force, trash everything
|
||||
: remoteDeleted.where((e) => e.isRemote).map((e) {
|
||||
e.isTrashed = true;
|
||||
return e;
|
||||
});
|
||||
|
||||
await _db.writeTxn(() async {
|
||||
if (assetsToUpdate.isNotEmpty) {
|
||||
await _db.assets.putAll(assetsToUpdate.toList());
|
||||
}
|
||||
if (force) {
|
||||
final remoteOnly = remoteDeleted
|
||||
.where((e) => e.storage == AssetState.remote)
|
||||
.map((e) => e.id)
|
||||
.toList();
|
||||
await _db.exifInfos.deleteAll(remoteOnly);
|
||||
await _db.assets.deleteAll(remoteOnly);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await _assetService.deleteRemoteAssets(
|
||||
deleteAssets,
|
||||
shouldDeletePermanently: shouldDeletePermanently,
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.severe("Failed to delete remote assets", error);
|
||||
return false;
|
||||
} finally {
|
||||
_deleteInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> deleteAssets(
|
||||
@@ -183,111 +144,18 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
_deleteInProgress = true;
|
||||
state = true;
|
||||
try {
|
||||
final hasLocal = deleteAssets.any((a) => a.storage != AssetState.remote);
|
||||
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
||||
final remoteDeleted = (hasLocal && localDeleted.isNotEmpty) || !hasLocal
|
||||
? await _deleteRemoteAssets(deleteAssets, force)
|
||||
: [];
|
||||
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
|
||||
final dbIds = <int>[];
|
||||
final dbUpdates = <Asset>[];
|
||||
|
||||
// Local assets are removed
|
||||
if (localDeleted.isNotEmpty) {
|
||||
// Permanently remove local only assets from isar
|
||||
dbIds.addAll(
|
||||
deleteAssets
|
||||
.where((a) => a.storage == AssetState.local)
|
||||
.map((e) => e.id),
|
||||
);
|
||||
|
||||
if (remoteDeleted.any((e) => e.isLocal)) {
|
||||
// Force delete: Add all local assets including merged assets
|
||||
if (force) {
|
||||
dbIds.addAll(remoteDeleted.map((e) => e.id));
|
||||
// Soft delete: Remove local Id from asset and trash it
|
||||
} else {
|
||||
dbUpdates.addAll(
|
||||
remoteDeleted.map((e) {
|
||||
e.localId = null;
|
||||
e.isTrashed = true;
|
||||
return e;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remote deletion
|
||||
if (remoteDeleted.isNotEmpty) {
|
||||
if (force) {
|
||||
// Remove remote only assets
|
||||
dbIds.addAll(
|
||||
deleteAssets
|
||||
.where((a) => a.storage == AssetState.remote)
|
||||
.map((e) => e.id),
|
||||
);
|
||||
// Local assets are not removed and there are merged assets
|
||||
final hasLocal = remoteDeleted.any((e) => e.isLocal);
|
||||
if (localDeleted.isEmpty && hasLocal) {
|
||||
// Remove remote Id from local assets
|
||||
dbUpdates.addAll(
|
||||
remoteDeleted.map((e) {
|
||||
e.remoteId = null;
|
||||
// Remove from trashed if remote asset is removed
|
||||
e.isTrashed = false;
|
||||
return e;
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
dbUpdates.addAll(
|
||||
remoteDeleted.map((e) {
|
||||
e.isTrashed = true;
|
||||
return e;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(dbUpdates);
|
||||
await _db.exifInfos.deleteAll(dbIds);
|
||||
await _db.assets.deleteAll(dbIds);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await _assetService.deleteAssets(
|
||||
deleteAssets,
|
||||
shouldDeletePermanently: force,
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.severe("Failed to delete assets", error);
|
||||
return false;
|
||||
} finally {
|
||||
_deleteInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<List<String>> _deleteLocalAssets(
|
||||
Iterable<Asset> assetsToDelete,
|
||||
) async {
|
||||
final List<String> local =
|
||||
assetsToDelete.where((a) => a.isLocal).map((a) => a.localId!).toList();
|
||||
// Delete asset from device
|
||||
if (local.isNotEmpty) {
|
||||
try {
|
||||
return await _ref.read(assetMediaRepositoryProvider).deleteAll(local);
|
||||
} catch (e, stack) {
|
||||
log.severe("Failed to delete asset from device", e, stack);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<List<Asset>> _deleteRemoteAssets(
|
||||
Iterable<Asset> assetsToDelete,
|
||||
bool? force,
|
||||
) async {
|
||||
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
|
||||
|
||||
final isSuccess = await _assetService.deleteAssets(remote, force: force);
|
||||
return isSuccess ? remote.toList() : [];
|
||||
}
|
||||
|
||||
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) {
|
||||
@@ -301,41 +169,40 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
|
||||
return AssetNotifier(
|
||||
ref.watch(assetServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
||||
final assetDetailProvider =
|
||||
StreamProvider.autoDispose.family<Asset, Asset>((ref, asset) async* {
|
||||
yield await ref.watch(assetServiceProvider).loadExif(asset);
|
||||
final db = ref.watch(dbProvider);
|
||||
await for (final a in db.assets.watchObject(asset.id)) {
|
||||
if (a != null) {
|
||||
yield await ref.watch(assetServiceProvider).loadExif(a);
|
||||
final assetService = ref.watch(assetServiceProvider);
|
||||
yield await assetService.loadExif(asset);
|
||||
|
||||
await for (final asset in assetService.watchAsset(asset.id)) {
|
||||
if (asset != null) {
|
||||
yield await ref.watch(assetServiceProvider).loadExif(asset);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final assetWatcher =
|
||||
StreamProvider.autoDispose.family<Asset?, Asset>((ref, asset) {
|
||||
final db = ref.watch(dbProvider);
|
||||
return db.assets.watchObject(asset.id, fireImmediately: true);
|
||||
final assetService = ref.watch(assetServiceProvider);
|
||||
return assetService.watchAsset(asset.id, fireImmediately: true);
|
||||
});
|
||||
|
||||
final assetsProvider = StreamProvider.family<RenderList, int?>(
|
||||
(ref, userId) {
|
||||
if (userId == null) return const Stream.empty();
|
||||
ref.watch(localeProvider);
|
||||
final query = _commonFilterAndSort(
|
||||
_assets(ref).where().ownerIdEqualToAnyChecksum(userId),
|
||||
);
|
||||
|
||||
final query = ref
|
||||
.watch(dbProvider)
|
||||
.assets
|
||||
.where()
|
||||
.ownerIdEqualToAnyChecksum(userId)
|
||||
.filter()
|
||||
.isArchivedEqualTo(false)
|
||||
.isTrashedEqualTo(false)
|
||||
.stackPrimaryAssetIdIsNull()
|
||||
.sortByFileCreatedAtDesc();
|
||||
|
||||
return renderListGenerator(query, ref);
|
||||
},
|
||||
dependencies: [localeProvider],
|
||||
@@ -345,11 +212,17 @@ final multiUserAssetsProvider = StreamProvider.family<RenderList, List<int>>(
|
||||
(ref, userIds) {
|
||||
if (userIds.isEmpty) return const Stream.empty();
|
||||
ref.watch(localeProvider);
|
||||
final query = _commonFilterAndSort(
|
||||
_assets(ref)
|
||||
.where()
|
||||
.anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u)),
|
||||
);
|
||||
final query = ref
|
||||
.watch(dbProvider)
|
||||
.assets
|
||||
.where()
|
||||
.anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u))
|
||||
.filter()
|
||||
.isArchivedEqualTo(false)
|
||||
.isTrashedEqualTo(false)
|
||||
.stackPrimaryAssetIdIsNull()
|
||||
.sortByFileCreatedAtDesc();
|
||||
|
||||
return renderListGenerator(query, ref);
|
||||
},
|
||||
dependencies: [localeProvider],
|
||||
@@ -371,17 +244,3 @@ QueryBuilder<Asset, Asset, QAfterSortBy>? getRemoteAssetQuery(WidgetRef ref) {
|
||||
.stackPrimaryAssetIdIsNull()
|
||||
.sortByFileCreatedAtDesc();
|
||||
}
|
||||
|
||||
IsarCollection<Asset> _assets(StreamProviderRef<RenderList> ref) =>
|
||||
ref.watch(dbProvider).assets;
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> _commonFilterAndSort(
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> query,
|
||||
) {
|
||||
return query
|
||||
.filter()
|
||||
.isArchivedEqualTo(false)
|
||||
.isTrashedEqualTo(false)
|
||||
.stackPrimaryAssetIdIsNull()
|
||||
.sortByFileCreatedAtDesc();
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'asset_stack.provider.g.dart';
|
||||
|
||||
class AssetStackNotifier extends StateNotifier<List<Asset>> {
|
||||
final AssetService assetService;
|
||||
final String _stackId;
|
||||
final Ref _ref;
|
||||
|
||||
AssetStackNotifier(this._stackId, this._ref) : super([]) {
|
||||
AssetStackNotifier(this.assetService, this._stackId) : super([]) {
|
||||
_fetchStack(_stackId);
|
||||
}
|
||||
|
||||
@@ -19,7 +18,7 @@ class AssetStackNotifier extends StateNotifier<List<Asset>> {
|
||||
return;
|
||||
}
|
||||
|
||||
final stack = await _ref.read(assetStackProvider(stackId).future);
|
||||
final stack = await assetService.getStackAssets(stackId);
|
||||
if (stack.isNotEmpty) {
|
||||
state = stack;
|
||||
}
|
||||
@@ -35,24 +34,10 @@ class AssetStackNotifier extends StateNotifier<List<Asset>> {
|
||||
|
||||
final assetStackStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<AssetStackNotifier, List<Asset>, String>(
|
||||
(ref, stackId) => AssetStackNotifier(stackId, ref),
|
||||
(ref, stackId) =>
|
||||
AssetStackNotifier(ref.watch(assetServiceProvider), stackId),
|
||||
);
|
||||
|
||||
final assetStackProvider =
|
||||
FutureProvider.autoDispose.family<List<Asset>, String>((ref, stackId) {
|
||||
return ref
|
||||
.watch(dbProvider)
|
||||
.assets
|
||||
.filter()
|
||||
.isArchivedEqualTo(false)
|
||||
.isTrashedEqualTo(false)
|
||||
.stackIdEqualTo(stackId)
|
||||
// orders primary asset first as its ID is null
|
||||
.sortByStackPrimaryAssetId()
|
||||
.thenByFileCreatedAtDesc()
|
||||
.findAll();
|
||||
});
|
||||
|
||||
@riverpod
|
||||
int assetStackIndex(AssetStackIndexRef ref, Asset asset) {
|
||||
return -1;
|
||||
|
||||
@@ -57,7 +57,7 @@ class TrashNotifier extends StateNotifier<bool> {
|
||||
|
||||
final isRemoved = await _ref
|
||||
.read(assetProvider.notifier)
|
||||
.deleteRemoteOnlyAssets(assetList, force: true);
|
||||
.deleteRemoteAssets(assetList, shouldDeletePermanently: true);
|
||||
|
||||
if (isRemoved) {
|
||||
final idsToRemove =
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
@@ -7,6 +8,7 @@ import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final albumRepositoryProvider =
|
||||
@@ -152,4 +154,44 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
||||
|
||||
return await query.findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearTable() async {
|
||||
await txn(() async {
|
||||
await db.albums.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<Album>> watchRemoteAlbums() {
|
||||
return db.albums.where().remoteIdIsNotNull().watch();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<Album>> watchLocalAlbums() {
|
||||
return db.albums.where().localIdIsNotNull().watch();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Album?> watchAlbum(int id) {
|
||||
return db.albums.watchObject(id, fireImmediately: true);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<RenderList> getRenderListStream(Album album) async* {
|
||||
final query = album.assets.filter().isTrashedEqualTo(false);
|
||||
final withSortedOption = switch (album.sortOrder) {
|
||||
SortOrder.asc => query.sortByFileCreatedAt(),
|
||||
SortOrder.desc => query.sortByFileCreatedAtDesc(),
|
||||
};
|
||||
|
||||
yield await RenderList.fromQuery(
|
||||
withSortedOption,
|
||||
GroupAssetsBy.none,
|
||||
);
|
||||
|
||||
await for (final _ in query.watchLazy()) {
|
||||
yield await RenderList.fromQuery(withSortedOption, GroupAssetsBy.none);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteById(List<int> ids) => txn(() async {
|
||||
Future<void> deleteByIds(List<int> ids) => txn(() async {
|
||||
await db.assets.deleteAll(ids);
|
||||
await db.exifInfos.deleteAll(ids);
|
||||
});
|
||||
@@ -197,6 +197,31 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
|
||||
@override
|
||||
Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state}) =>
|
||||
txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll());
|
||||
|
||||
@override
|
||||
Future<List<Asset>> getStackAssets(String stackId) {
|
||||
return db.assets
|
||||
.filter()
|
||||
.isArchivedEqualTo(false)
|
||||
.isTrashedEqualTo(false)
|
||||
.stackIdEqualTo(stackId)
|
||||
// orders primary asset first as its ID is null
|
||||
.sortByStackPrimaryAssetId()
|
||||
.thenByFileCreatedAtDesc()
|
||||
.findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearTable() async {
|
||||
await txn(() async {
|
||||
await db.assets.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false}) {
|
||||
return db.assets.watchObject(id, fireImmediately: fireImmediately);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>> _getMatchesImpl(
|
||||
|
||||
@@ -26,4 +26,11 @@ class ETagRepository extends DatabaseRepository implements IETagRepository {
|
||||
|
||||
@override
|
||||
Future<ETag?> getById(String id) => db.eTags.getById(id);
|
||||
|
||||
@override
|
||||
Future<void> clearTable() async {
|
||||
await txn(() async {
|
||||
await db.eTags.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,9 @@ class ExifInfoRepository extends DatabaseRepository
|
||||
await txn(() => db.exifInfos.putAll(exifInfos));
|
||||
return exifInfos;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearTable() {
|
||||
return txn(() => db.exifInfos.clear());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,4 +57,11 @@ class UserRepository extends DatabaseRepository implements IUserRepository {
|
||||
.or()
|
||||
.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.findAll();
|
||||
|
||||
@override
|
||||
Future<void> clearTable() async {
|
||||
await txn(() async {
|
||||
await db.users.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,6 +1060,7 @@ class NativeVideoViewerRoute extends PageRouteInfo<NativeVideoViewerRouteArgs> {
|
||||
required Asset asset,
|
||||
required Widget image,
|
||||
bool showControls = true,
|
||||
int playbackDelayFactor = 1,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
NativeVideoViewerRoute.name,
|
||||
@@ -1068,6 +1069,7 @@ class NativeVideoViewerRoute extends PageRouteInfo<NativeVideoViewerRouteArgs> {
|
||||
asset: asset,
|
||||
image: image,
|
||||
showControls: showControls,
|
||||
playbackDelayFactor: playbackDelayFactor,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
@@ -1083,6 +1085,7 @@ class NativeVideoViewerRoute extends PageRouteInfo<NativeVideoViewerRouteArgs> {
|
||||
asset: args.asset,
|
||||
image: args.image,
|
||||
showControls: args.showControls,
|
||||
playbackDelayFactor: args.playbackDelayFactor,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1094,6 +1097,7 @@ class NativeVideoViewerRouteArgs {
|
||||
required this.asset,
|
||||
required this.image,
|
||||
this.showControls = true,
|
||||
this.playbackDelayFactor = 1,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
@@ -1104,9 +1108,11 @@ class NativeVideoViewerRouteArgs {
|
||||
|
||||
final bool showControls;
|
||||
|
||||
final int playbackDelayFactor;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, image: $image, showControls: $showControls}';
|
||||
return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, image: $image, showControls: $showControls, playbackDelayFactor: $playbackDelayFactor}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final albumServiceProvider = Provider(
|
||||
@@ -309,7 +310,7 @@ class AlbumService {
|
||||
final List<int> idsToRemove =
|
||||
_syncService.sharedAssetsToRemove(foreignAssets, existing);
|
||||
if (idsToRemove.isNotEmpty) {
|
||||
await _assetRepository.deleteById(idsToRemove);
|
||||
await _assetRepository.deleteByIds(idsToRemove);
|
||||
}
|
||||
} else {
|
||||
await _albumRepository.delete(album.id);
|
||||
@@ -442,10 +443,35 @@ class AlbumService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Album>> getAll() async {
|
||||
Future<List<Album>> getAllRemoteAlbums() async {
|
||||
return _albumRepository.getAll(remote: true);
|
||||
}
|
||||
|
||||
Future<List<Album>> getAllLocalAlbums() async {
|
||||
return _albumRepository.getAll(remote: false);
|
||||
}
|
||||
|
||||
Stream<List<Album>> watchRemoteAlbums() {
|
||||
return _albumRepository.watchRemoteAlbums();
|
||||
}
|
||||
|
||||
Stream<List<Album>> watchLocalAlbums() {
|
||||
return _albumRepository.watchLocalAlbums();
|
||||
}
|
||||
|
||||
/// Get album by Isar ID
|
||||
Future<Album?> getAlbumById(int id) {
|
||||
return _albumRepository.get(id);
|
||||
}
|
||||
|
||||
Stream<Album?> watchAlbum(int id) {
|
||||
return _albumRepository.watchAlbum(id);
|
||||
}
|
||||
|
||||
Stream<RenderList> getRenderListGenerator(Album album) {
|
||||
return _albumRepository.getRenderListStream(album);
|
||||
}
|
||||
|
||||
Future<List<Album>> search(
|
||||
String searchTerm,
|
||||
QuickFilterMode filterMode,
|
||||
@@ -465,4 +491,8 @@ class AlbumService {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> clearTable() async {
|
||||
await _albumRepository.clearTable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class ApiService implements Authentication {
|
||||
late MapApi mapApi;
|
||||
late PartnersApi partnersApi;
|
||||
late PeopleApi peopleApi;
|
||||
late AuditApi auditApi;
|
||||
late SharedLinksApi sharedLinksApi;
|
||||
late SyncApi syncApi;
|
||||
late SystemConfigApi systemConfigApi;
|
||||
@@ -56,7 +55,6 @@ class ApiService implements Authentication {
|
||||
mapApi = MapApi(_apiClient);
|
||||
partnersApi = PartnersApi(_apiClient);
|
||||
peopleApi = PeopleApi(_apiClient);
|
||||
auditApi = AuditApi(_apiClient);
|
||||
sharedLinksApi = SharedLinksApi(_apiClient);
|
||||
syncApi = SyncApi(_apiClient);
|
||||
systemConfigApi = SystemConfigApi(_apiClient);
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||
@@ -17,6 +18,7 @@ import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/repositories/etag.repository.dart';
|
||||
import 'package:immich_mobile/repositories/exif_info.repository.dart';
|
||||
@@ -43,6 +45,7 @@ final assetServiceProvider = Provider(
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(assetMediaRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -58,6 +61,7 @@ class AssetService {
|
||||
final UserService _userService;
|
||||
final BackupService _backupService;
|
||||
final AlbumService _albumService;
|
||||
final IAssetMediaRepository _assetMediaRepository;
|
||||
final log = Logger('AssetService');
|
||||
|
||||
AssetService(
|
||||
@@ -72,6 +76,7 @@ class AssetService {
|
||||
this._userService,
|
||||
this._backupService,
|
||||
this._albumService,
|
||||
this._assetMediaRepository,
|
||||
);
|
||||
|
||||
/// Checks the server for updated assets and updates the local database if
|
||||
@@ -158,30 +163,6 @@ class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteAssets(
|
||||
Iterable<Asset> deleteAssets, {
|
||||
bool? force = false,
|
||||
}) async {
|
||||
try {
|
||||
final List<String> payload = [];
|
||||
|
||||
for (final asset in deleteAssets) {
|
||||
payload.add(asset.remoteId!);
|
||||
}
|
||||
|
||||
await _apiService.assetsApi.deleteAssets(
|
||||
AssetBulkDeleteDto(
|
||||
ids: payload,
|
||||
force: force,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} catch (error, stack) {
|
||||
log.severe("Error while deleting assets", error, stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Loads the exif information from the database. If there is none, loads
|
||||
/// the exif info from the server (remote assets only)
|
||||
Future<Asset> loadExif(Asset a) async {
|
||||
@@ -428,4 +409,109 @@ class AssetService {
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
Future<List<Asset>> getStackAssets(String stackId) {
|
||||
return _assetRepository.getStackAssets(stackId);
|
||||
}
|
||||
|
||||
Future<void> clearTable() {
|
||||
return _assetRepository.clearTable();
|
||||
}
|
||||
|
||||
/// Delete assets from local file system and unreference from the database
|
||||
Future<void> deleteLocalAssets(Iterable<Asset> assets) async {
|
||||
// Delete files from local gallery
|
||||
final candidates = assets.where((asset) => asset.isLocal);
|
||||
|
||||
final deletedIds = await _assetMediaRepository
|
||||
.deleteAll(candidates.map((asset) => asset.localId!).toList());
|
||||
|
||||
// Modify local database by removing the reference to the local assets
|
||||
if (deletedIds.isNotEmpty) {
|
||||
// Delete records from local database
|
||||
final isarIds = assets
|
||||
.where((asset) => asset.storage == AssetState.local)
|
||||
.map((asset) => asset.id)
|
||||
.toList();
|
||||
await _assetRepository.deleteByIds(isarIds);
|
||||
|
||||
// Modify Merged asset to be remote only
|
||||
final updatedAssets = assets
|
||||
.where((asset) => asset.storage == AssetState.merged)
|
||||
.map((asset) {
|
||||
asset.localId = null;
|
||||
return asset;
|
||||
}).toList();
|
||||
|
||||
await _assetRepository.updateAll(updatedAssets);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete assets from the server and unreference from the database
|
||||
Future<void> deleteRemoteAssets(
|
||||
Iterable<Asset> assets, {
|
||||
bool shouldDeletePermanently = false,
|
||||
}) async {
|
||||
final candidates = assets.where((a) => a.isRemote);
|
||||
if (candidates.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _apiService.assetsApi.deleteAssets(
|
||||
AssetBulkDeleteDto(
|
||||
ids: candidates.map((a) => a.remoteId!).toList(),
|
||||
force: shouldDeletePermanently,
|
||||
),
|
||||
);
|
||||
|
||||
/// Update asset info bassed on the deletion type.
|
||||
final payload = shouldDeletePermanently
|
||||
? assets
|
||||
.where((asset) => asset.storage == AssetState.merged)
|
||||
.map((asset) {
|
||||
asset.remoteId = null;
|
||||
return asset;
|
||||
})
|
||||
: assets.where((asset) => asset.isRemote).map((asset) {
|
||||
asset.isTrashed = true;
|
||||
return asset;
|
||||
});
|
||||
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(payload.toList());
|
||||
|
||||
if (shouldDeletePermanently) {
|
||||
final remoteAssetIds = assets
|
||||
.where((asset) => asset.storage == AssetState.remote)
|
||||
.map((asset) => asset.id)
|
||||
.toList();
|
||||
await _assetRepository.deleteByIds(remoteAssetIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Delete assets on both local file system and the server.
|
||||
/// Unreference from the database.
|
||||
Future<void> deleteAssets(
|
||||
Iterable<Asset> assets, {
|
||||
bool shouldDeletePermanently = false,
|
||||
}) async {
|
||||
final hasLocal = assets.any((asset) => asset.isLocal);
|
||||
final hasRemote = assets.any((asset) => asset.isRemote);
|
||||
|
||||
if (hasLocal) {
|
||||
await deleteLocalAssets(assets);
|
||||
}
|
||||
|
||||
if (hasRemote) {
|
||||
await deleteRemoteAssets(
|
||||
assets,
|
||||
shouldDeletePermanently: shouldDeletePermanently,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false}) {
|
||||
return _assetRepository.watchAsset(id, fireImmediately: fireImmediately);
|
||||
}
|
||||
}
|
||||
|
||||
16
mobile/lib/services/etag.service.dart
Normal file
16
mobile/lib/services/etag.service.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/repositories/etag.repository.dart';
|
||||
|
||||
final etagServiceProvider =
|
||||
Provider((ref) => ETagService(ref.watch(etagRepositoryProvider)));
|
||||
|
||||
class ETagService {
|
||||
final IETagRepository _eTagRepository;
|
||||
|
||||
ETagService(this._eTagRepository);
|
||||
|
||||
Future<void> clearTable() {
|
||||
return _eTagRepository.clearTable();
|
||||
}
|
||||
}
|
||||
16
mobile/lib/services/exif.service.dart
Normal file
16
mobile/lib/services/exif.service.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||
import 'package:immich_mobile/repositories/exif_info.repository.dart';
|
||||
|
||||
final exifServiceProvider =
|
||||
Provider((ref) => ExifService(ref.watch(exifInfoRepositoryProvider)));
|
||||
|
||||
class ExifService {
|
||||
final IExifInfoRepository _exifInfoRepository;
|
||||
|
||||
ExifService(this._exifInfoRepository);
|
||||
|
||||
Future<void> clearTable() {
|
||||
return _exifInfoRepository.clearTable();
|
||||
}
|
||||
}
|
||||
@@ -286,7 +286,7 @@ class SyncService {
|
||||
}
|
||||
final idsToDelete = toRemove.map((e) => e.id).toList();
|
||||
try {
|
||||
await _assetRepository.deleteById(idsToDelete);
|
||||
await _assetRepository.deleteByIds(idsToDelete);
|
||||
await upsertAssetsWithExif(toAdd + toUpdate);
|
||||
} catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
@@ -334,7 +334,7 @@ class SyncService {
|
||||
if (toDelete.isNotEmpty) {
|
||||
final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
|
||||
if (idsToRemove.isNotEmpty) {
|
||||
await _assetRepository.deleteById(idsToRemove);
|
||||
await _assetRepository.deleteByIds(idsToRemove);
|
||||
}
|
||||
} else {
|
||||
assert(toDelete.isEmpty);
|
||||
@@ -531,7 +531,7 @@ class SyncService {
|
||||
);
|
||||
if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteById(toDelete);
|
||||
await _assetRepository.deleteByIds(toDelete);
|
||||
await _assetRepository.updateAll(toUpdate);
|
||||
});
|
||||
_log.info(
|
||||
@@ -826,7 +826,7 @@ class SyncService {
|
||||
final (toDelete, toUpdate) =
|
||||
_handleAssetRemoval(assets, [], remote: false);
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteById(toDelete);
|
||||
await _assetRepository.deleteByIds(toDelete);
|
||||
await _assetRepository.updateAll(toUpdate);
|
||||
await _albumRepository.deleteAllLocal();
|
||||
});
|
||||
|
||||
@@ -103,4 +103,8 @@ class UserService {
|
||||
if (users == null) return false;
|
||||
return _syncService.syncUsersFromServer(users);
|
||||
}
|
||||
|
||||
Future<void> clearTable() {
|
||||
return _userRepository.clearTable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
Future<void> clearAssetsAndAlbums(Isar db) async {
|
||||
await Store.delete(StoreKey.assetETag);
|
||||
await db.writeTxn(() async {
|
||||
await db.assets.clear();
|
||||
await db.exifInfos.clear();
|
||||
await db.albums.clear();
|
||||
await db.eTags.clear();
|
||||
await db.users.clear();
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/utils/db.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
const int targetVersion = 8;
|
||||
@@ -14,6 +18,13 @@ Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
||||
}
|
||||
|
||||
Future<void> _migrateTo(Isar db, int version) async {
|
||||
await clearAssetsAndAlbums(db);
|
||||
await Store.delete(StoreKey.assetETag);
|
||||
await db.writeTxn(() async {
|
||||
await db.assets.clear();
|
||||
await db.exifInfos.clear();
|
||||
await db.albums.clear();
|
||||
await db.eTags.clear();
|
||||
await db.users.clear();
|
||||
});
|
||||
await Store.put(StoreKey.version, version);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
addDefault(value, 'sharedLinks', SharedLinksResponse().toJson());
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SharedLinkResponseDto':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'viewCount', 0);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ServerConfigDto':
|
||||
if (value is Map) {
|
||||
addDefault(
|
||||
@@ -26,11 +33,14 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'UserResponseDto':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||
}
|
||||
break;
|
||||
|
||||
case 'UserAdminResponseDto':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||
|
||||
@@ -200,24 +200,26 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void onDeleteLocal(bool onlyBackedUp) async {
|
||||
void onDeleteLocal(bool isMergedAsset) async {
|
||||
processing.value = true;
|
||||
try {
|
||||
// Select only the local assets from the selection
|
||||
final localIds = selection.value.where((a) => a.isLocal).toList();
|
||||
final localAssets = selection.value.where((a) => a.isLocal).toList();
|
||||
|
||||
final toDelete = isMergedAsset
|
||||
? localAssets.where((e) => e.storage == AssetState.merged)
|
||||
: localAssets;
|
||||
|
||||
if (toDelete.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete only the backed-up assets if 'onlyBackedUp' is true
|
||||
final isDeleted = await ref
|
||||
.read(assetProvider.notifier)
|
||||
.deleteLocalOnlyAssets(localIds, onlyBackedUp: onlyBackedUp);
|
||||
.deleteLocalAssets(toDelete.toList());
|
||||
|
||||
if (isDeleted) {
|
||||
// Show a toast with the correct number of deleted assets
|
||||
final deletedCount = localIds
|
||||
.where(
|
||||
(e) => !onlyBackedUp || e.isRemote,
|
||||
) // Only count backed-up assets
|
||||
.length;
|
||||
final deletedCount =
|
||||
localAssets.where((e) => !isMergedAsset || e.isRemote).length;
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
@@ -226,7 +228,6 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
|
||||
// Reset the selection
|
||||
selectionEnabledHook.value = false;
|
||||
}
|
||||
} finally {
|
||||
@@ -234,7 +235,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void onDeleteRemote([bool force = false]) async {
|
||||
void onDeleteRemote([bool shouldDeletePermanently = false]) async {
|
||||
processing.value = true;
|
||||
try {
|
||||
final toDelete = ownedRemoteSelection(
|
||||
@@ -242,13 +243,15 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
ownerErrorMessage: 'home_page_delete_err_partner'.tr(),
|
||||
).toList();
|
||||
|
||||
final isDeleted = await ref
|
||||
.read(assetProvider.notifier)
|
||||
.deleteRemoteOnlyAssets(toDelete, force: force);
|
||||
final isDeleted =
|
||||
await ref.read(assetProvider.notifier).deleteRemoteAssets(
|
||||
toDelete,
|
||||
shouldDeletePermanently: shouldDeletePermanently,
|
||||
);
|
||||
if (isDeleted) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: force
|
||||
msg: shouldDeletePermanently
|
||||
? 'assets_deleted_permanently_from_server'
|
||||
.tr(args: ["${toDelete.length}"])
|
||||
: 'assets_trashed_from_server'.tr(args: ["${toDelete.length}"]),
|
||||
|
||||
@@ -134,7 +134,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
ref.read(assetProvider.notifier).clearAllAsset();
|
||||
ref.read(assetProvider.notifier).clearAllAssets();
|
||||
ref.read(websocketProvider.notifier).disconnect();
|
||||
context.replaceRoute(const LoginRoute());
|
||||
},
|
||||
|
||||
@@ -85,7 +85,7 @@ class ChangePasswordForm extends HookConsumerWidget {
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
await ref
|
||||
.read(assetProvider.notifier)
|
||||
.clearAllAsset();
|
||||
.clearAllAssets();
|
||||
ref.read(websocketProvider.notifier).disconnect();
|
||||
|
||||
AutoRouter.of(context).back();
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/oauth.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
@@ -150,7 +149,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
final serverUrl = Store.tryGet(StoreKey.serverUrl);
|
||||
final serverUrl = getServerUrl();
|
||||
if (serverUrl != null) {
|
||||
serverEndpointController.text = serverUrl;
|
||||
}
|
||||
|
||||
3
mobile/openapi/README.md
generated
3
mobile/openapi/README.md
generated
@@ -109,7 +109,6 @@ Class | Method | HTTP request | Description
|
||||
*AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets |
|
||||
*AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets |
|
||||
*AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail |
|
||||
*AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
|
||||
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
||||
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
|
||||
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
|
||||
@@ -293,7 +292,6 @@ Class | Method | HTTP request | Description
|
||||
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
|
||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||
- [AudioCodec](doc//AudioCodec.md)
|
||||
- [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md)
|
||||
- [AvatarResponse](doc//AvatarResponse.md)
|
||||
- [AvatarUpdate](doc//AvatarUpdate.md)
|
||||
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
||||
@@ -317,7 +315,6 @@ Class | Method | HTTP request | Description
|
||||
- [DuplicateResponseDto](doc//DuplicateResponseDto.md)
|
||||
- [EmailNotificationsResponse](doc//EmailNotificationsResponse.md)
|
||||
- [EmailNotificationsUpdate](doc//EmailNotificationsUpdate.md)
|
||||
- [EntityType](doc//EntityType.md)
|
||||
- [ExifResponseDto](doc//ExifResponseDto.md)
|
||||
- [FaceDto](doc//FaceDto.md)
|
||||
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
|
||||
|
||||
3
mobile/openapi/lib/api.dart
generated
3
mobile/openapi/lib/api.dart
generated
@@ -34,7 +34,6 @@ part 'api/api_keys_api.dart';
|
||||
part 'api/activities_api.dart';
|
||||
part 'api/albums_api.dart';
|
||||
part 'api/assets_api.dart';
|
||||
part 'api/audit_api.dart';
|
||||
part 'api/authentication_api.dart';
|
||||
part 'api/deprecated_api.dart';
|
||||
part 'api/download_api.dart';
|
||||
@@ -106,7 +105,6 @@ part 'model/asset_stack_response_dto.dart';
|
||||
part 'model/asset_stats_response_dto.dart';
|
||||
part 'model/asset_type_enum.dart';
|
||||
part 'model/audio_codec.dart';
|
||||
part 'model/audit_deletes_response_dto.dart';
|
||||
part 'model/avatar_response.dart';
|
||||
part 'model/avatar_update.dart';
|
||||
part 'model/bulk_id_response_dto.dart';
|
||||
@@ -130,7 +128,6 @@ part 'model/duplicate_detection_config.dart';
|
||||
part 'model/duplicate_response_dto.dart';
|
||||
part 'model/email_notifications_response.dart';
|
||||
part 'model/email_notifications_update.dart';
|
||||
part 'model/entity_type.dart';
|
||||
part 'model/exif_response_dto.dart';
|
||||
part 'model/face_dto.dart';
|
||||
part 'model/facial_recognition_config.dart';
|
||||
|
||||
79
mobile/openapi/lib/api/audit_api.dart
generated
79
mobile/openapi/lib/api/audit_api.dart
generated
@@ -1,79 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class AuditApi {
|
||||
AuditApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'GET /audit/deletes' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] after (required):
|
||||
///
|
||||
/// * [EntityType] entityType (required):
|
||||
///
|
||||
/// * [String] userId:
|
||||
Future<Response> getAuditDeletesWithHttpInfo(DateTime after, EntityType entityType, { String? userId, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/audit/deletes';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
queryParams.addAll(_queryParams('', 'after', after));
|
||||
queryParams.addAll(_queryParams('', 'entityType', entityType));
|
||||
if (userId != null) {
|
||||
queryParams.addAll(_queryParams('', 'userId', userId));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] after (required):
|
||||
///
|
||||
/// * [EntityType] entityType (required):
|
||||
///
|
||||
/// * [String] userId:
|
||||
Future<AuditDeletesResponseDto?> getAuditDeletes(DateTime after, EntityType entityType, { String? userId, }) async {
|
||||
final response = await getAuditDeletesWithHttpInfo(after, entityType, userId: userId, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AuditDeletesResponseDto',) as AuditDeletesResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
@@ -266,8 +266,6 @@ class ApiClient {
|
||||
return AssetTypeEnumTypeTransformer().decode(value);
|
||||
case 'AudioCodec':
|
||||
return AudioCodecTypeTransformer().decode(value);
|
||||
case 'AuditDeletesResponseDto':
|
||||
return AuditDeletesResponseDto.fromJson(value);
|
||||
case 'AvatarResponse':
|
||||
return AvatarResponse.fromJson(value);
|
||||
case 'AvatarUpdate':
|
||||
@@ -314,8 +312,6 @@ class ApiClient {
|
||||
return EmailNotificationsResponse.fromJson(value);
|
||||
case 'EmailNotificationsUpdate':
|
||||
return EmailNotificationsUpdate.fromJson(value);
|
||||
case 'EntityType':
|
||||
return EntityTypeTypeTransformer().decode(value);
|
||||
case 'ExifResponseDto':
|
||||
return ExifResponseDto.fromJson(value);
|
||||
case 'FaceDto':
|
||||
|
||||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@@ -82,9 +82,6 @@ String parameterToString(dynamic value) {
|
||||
if (value is Colorspace) {
|
||||
return ColorspaceTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is EntityType) {
|
||||
return EntityTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is ImageFormat) {
|
||||
return ImageFormatTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
105
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
105
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@@ -20,7 +20,6 @@ class AssetBulkUpdateDto {
|
||||
this.isFavorite,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.orientation,
|
||||
this.rating,
|
||||
});
|
||||
|
||||
@@ -68,8 +67,6 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
num? longitude;
|
||||
|
||||
AssetBulkUpdateDtoOrientationEnum? orientation;
|
||||
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
///
|
||||
@@ -89,7 +86,6 @@ class AssetBulkUpdateDto {
|
||||
other.isFavorite == isFavorite &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude &&
|
||||
other.orientation == orientation &&
|
||||
other.rating == rating;
|
||||
|
||||
@override
|
||||
@@ -102,11 +98,10 @@ class AssetBulkUpdateDto {
|
||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||
(latitude == null ? 0 : latitude!.hashCode) +
|
||||
(longitude == null ? 0 : longitude!.hashCode) +
|
||||
(orientation == null ? 0 : orientation!.hashCode) +
|
||||
(rating == null ? 0 : rating!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, orientation=$orientation, rating=$rating]';
|
||||
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -141,11 +136,6 @@ class AssetBulkUpdateDto {
|
||||
} else {
|
||||
// json[r'longitude'] = null;
|
||||
}
|
||||
if (this.orientation != null) {
|
||||
json[r'orientation'] = this.orientation;
|
||||
} else {
|
||||
// json[r'orientation'] = null;
|
||||
}
|
||||
if (this.rating != null) {
|
||||
json[r'rating'] = this.rating;
|
||||
} else {
|
||||
@@ -172,7 +162,6 @@ class AssetBulkUpdateDto {
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
orientation: AssetBulkUpdateDtoOrientationEnum.fromJson(json[r'orientation']),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
);
|
||||
}
|
||||
@@ -225,95 +214,3 @@ class AssetBulkUpdateDto {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
class AssetBulkUpdateDtoOrientationEnum {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AssetBulkUpdateDtoOrientationEnum._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final int value;
|
||||
|
||||
@override
|
||||
String toString() => value.toString();
|
||||
|
||||
int toJson() => value;
|
||||
|
||||
static const number1 = AssetBulkUpdateDtoOrientationEnum._(1);
|
||||
static const number2 = AssetBulkUpdateDtoOrientationEnum._(2);
|
||||
static const number3 = AssetBulkUpdateDtoOrientationEnum._(3);
|
||||
static const number4 = AssetBulkUpdateDtoOrientationEnum._(4);
|
||||
static const number5 = AssetBulkUpdateDtoOrientationEnum._(5);
|
||||
static const number6 = AssetBulkUpdateDtoOrientationEnum._(6);
|
||||
static const number7 = AssetBulkUpdateDtoOrientationEnum._(7);
|
||||
static const number8 = AssetBulkUpdateDtoOrientationEnum._(8);
|
||||
|
||||
/// List of all possible values in this [enum][AssetBulkUpdateDtoOrientationEnum].
|
||||
static const values = <AssetBulkUpdateDtoOrientationEnum>[
|
||||
number1,
|
||||
number2,
|
||||
number3,
|
||||
number4,
|
||||
number5,
|
||||
number6,
|
||||
number7,
|
||||
number8,
|
||||
];
|
||||
|
||||
static AssetBulkUpdateDtoOrientationEnum? fromJson(dynamic value) => AssetBulkUpdateDtoOrientationEnumTypeTransformer().decode(value);
|
||||
|
||||
static List<AssetBulkUpdateDtoOrientationEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetBulkUpdateDtoOrientationEnum>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetBulkUpdateDtoOrientationEnum.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [AssetBulkUpdateDtoOrientationEnum] to int,
|
||||
/// and [decode] dynamic data back to [AssetBulkUpdateDtoOrientationEnum].
|
||||
class AssetBulkUpdateDtoOrientationEnumTypeTransformer {
|
||||
factory AssetBulkUpdateDtoOrientationEnumTypeTransformer() => _instance ??= const AssetBulkUpdateDtoOrientationEnumTypeTransformer._();
|
||||
|
||||
const AssetBulkUpdateDtoOrientationEnumTypeTransformer._();
|
||||
|
||||
int encode(AssetBulkUpdateDtoOrientationEnum data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a AssetBulkUpdateDtoOrientationEnum.
|
||||
///
|
||||
/// 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.
|
||||
AssetBulkUpdateDtoOrientationEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case 1: return AssetBulkUpdateDtoOrientationEnum.number1;
|
||||
case 2: return AssetBulkUpdateDtoOrientationEnum.number2;
|
||||
case 3: return AssetBulkUpdateDtoOrientationEnum.number3;
|
||||
case 4: return AssetBulkUpdateDtoOrientationEnum.number4;
|
||||
case 5: return AssetBulkUpdateDtoOrientationEnum.number5;
|
||||
case 6: return AssetBulkUpdateDtoOrientationEnum.number6;
|
||||
case 7: return AssetBulkUpdateDtoOrientationEnum.number7;
|
||||
case 8: return AssetBulkUpdateDtoOrientationEnum.number8;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [AssetBulkUpdateDtoOrientationEnumTypeTransformer] instance.
|
||||
static AssetBulkUpdateDtoOrientationEnumTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
109
mobile/openapi/lib/model/audit_deletes_response_dto.dart
generated
109
mobile/openapi/lib/model/audit_deletes_response_dto.dart
generated
@@ -1,109 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AuditDeletesResponseDto {
|
||||
/// Returns a new [AuditDeletesResponseDto] instance.
|
||||
AuditDeletesResponseDto({
|
||||
this.ids = const [],
|
||||
required this.needsFullSync,
|
||||
});
|
||||
|
||||
List<String> ids;
|
||||
|
||||
bool needsFullSync;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AuditDeletesResponseDto &&
|
||||
_deepEquality.equals(other.ids, ids) &&
|
||||
other.needsFullSync == needsFullSync;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(ids.hashCode) +
|
||||
(needsFullSync.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AuditDeletesResponseDto[ids=$ids, needsFullSync=$needsFullSync]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'ids'] = this.ids;
|
||||
json[r'needsFullSync'] = this.needsFullSync;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AuditDeletesResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AuditDeletesResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AuditDeletesResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AuditDeletesResponseDto(
|
||||
ids: json[r'ids'] is Iterable
|
||||
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
needsFullSync: mapValueOfType<bool>(json, r'needsFullSync')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AuditDeletesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AuditDeletesResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AuditDeletesResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AuditDeletesResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AuditDeletesResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AuditDeletesResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AuditDeletesResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AuditDeletesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AuditDeletesResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AuditDeletesResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'ids',
|
||||
'needsFullSync',
|
||||
};
|
||||
}
|
||||
|
||||
85
mobile/openapi/lib/model/entity_type.dart
generated
85
mobile/openapi/lib/model/entity_type.dart
generated
@@ -1,85 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class EntityType {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const EntityType._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const ASSET = EntityType._(r'ASSET');
|
||||
static const ALBUM = EntityType._(r'ALBUM');
|
||||
|
||||
/// List of all possible values in this [enum][EntityType].
|
||||
static const values = <EntityType>[
|
||||
ASSET,
|
||||
ALBUM,
|
||||
];
|
||||
|
||||
static EntityType? fromJson(dynamic value) => EntityTypeTypeTransformer().decode(value);
|
||||
|
||||
static List<EntityType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <EntityType>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = EntityType.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [EntityType] to String,
|
||||
/// and [decode] dynamic data back to [EntityType].
|
||||
class EntityTypeTypeTransformer {
|
||||
factory EntityTypeTypeTransformer() => _instance ??= const EntityTypeTypeTransformer._();
|
||||
|
||||
const EntityTypeTypeTransformer._();
|
||||
|
||||
String encode(EntityType data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a EntityType.
|
||||
///
|
||||
/// 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.
|
||||
EntityType? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'ASSET': return EntityType.ASSET;
|
||||
case r'ALBUM': return EntityType.ALBUM;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [EntityTypeTypeTransformer] instance.
|
||||
static EntityTypeTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class SharedLinkResponseDto {
|
||||
this.token,
|
||||
required this.type,
|
||||
required this.userId,
|
||||
required this.viewCount,
|
||||
});
|
||||
|
||||
///
|
||||
@@ -63,6 +64,8 @@ class SharedLinkResponseDto {
|
||||
|
||||
String userId;
|
||||
|
||||
num viewCount;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SharedLinkResponseDto &&
|
||||
other.album == album &&
|
||||
@@ -78,7 +81,8 @@ class SharedLinkResponseDto {
|
||||
other.showMetadata == showMetadata &&
|
||||
other.token == token &&
|
||||
other.type == type &&
|
||||
other.userId == userId;
|
||||
other.userId == userId &&
|
||||
other.viewCount == viewCount;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -96,10 +100,11 @@ class SharedLinkResponseDto {
|
||||
(showMetadata.hashCode) +
|
||||
(token == null ? 0 : token!.hashCode) +
|
||||
(type.hashCode) +
|
||||
(userId.hashCode);
|
||||
(userId.hashCode) +
|
||||
(viewCount.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, token=$token, type=$type, userId=$userId]';
|
||||
String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, token=$token, type=$type, userId=$userId, viewCount=$viewCount]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -137,6 +142,7 @@ class SharedLinkResponseDto {
|
||||
}
|
||||
json[r'type'] = this.type;
|
||||
json[r'userId'] = this.userId;
|
||||
json[r'viewCount'] = this.viewCount;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -163,6 +169,7 @@ class SharedLinkResponseDto {
|
||||
token: mapValueOfType<String>(json, r'token'),
|
||||
type: SharedLinkType.fromJson(json[r'type'])!,
|
||||
userId: mapValueOfType<String>(json, r'userId')!,
|
||||
viewCount: num.parse('${json[r'viewCount']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -222,6 +229,7 @@ class SharedLinkResponseDto {
|
||||
'showMetadata',
|
||||
'type',
|
||||
'userId',
|
||||
'viewCount',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
105
mobile/openapi/lib/model/update_asset_dto.dart
generated
105
mobile/openapi/lib/model/update_asset_dto.dart
generated
@@ -20,7 +20,6 @@ class UpdateAssetDto {
|
||||
this.latitude,
|
||||
this.livePhotoVideoId,
|
||||
this.longitude,
|
||||
this.orientation,
|
||||
this.rating,
|
||||
});
|
||||
|
||||
@@ -74,8 +73,6 @@ class UpdateAssetDto {
|
||||
///
|
||||
num? longitude;
|
||||
|
||||
UpdateAssetDtoOrientationEnum? orientation;
|
||||
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
///
|
||||
@@ -95,7 +92,6 @@ class UpdateAssetDto {
|
||||
other.latitude == latitude &&
|
||||
other.livePhotoVideoId == livePhotoVideoId &&
|
||||
other.longitude == longitude &&
|
||||
other.orientation == orientation &&
|
||||
other.rating == rating;
|
||||
|
||||
@override
|
||||
@@ -108,11 +104,10 @@ class UpdateAssetDto {
|
||||
(latitude == null ? 0 : latitude!.hashCode) +
|
||||
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||
(longitude == null ? 0 : longitude!.hashCode) +
|
||||
(orientation == null ? 0 : orientation!.hashCode) +
|
||||
(rating == null ? 0 : rating!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, orientation=$orientation, rating=$rating]';
|
||||
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, rating=$rating]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -151,11 +146,6 @@ class UpdateAssetDto {
|
||||
} else {
|
||||
// json[r'longitude'] = null;
|
||||
}
|
||||
if (this.orientation != null) {
|
||||
json[r'orientation'] = this.orientation;
|
||||
} else {
|
||||
// json[r'orientation'] = null;
|
||||
}
|
||||
if (this.rating != null) {
|
||||
json[r'rating'] = this.rating;
|
||||
} else {
|
||||
@@ -180,7 +170,6 @@ class UpdateAssetDto {
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
orientation: UpdateAssetDtoOrientationEnum.fromJson(json[r'orientation']),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
);
|
||||
}
|
||||
@@ -232,95 +221,3 @@ class UpdateAssetDto {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
class UpdateAssetDtoOrientationEnum {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const UpdateAssetDtoOrientationEnum._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final int value;
|
||||
|
||||
@override
|
||||
String toString() => value.toString();
|
||||
|
||||
int toJson() => value;
|
||||
|
||||
static const number1 = UpdateAssetDtoOrientationEnum._(1);
|
||||
static const number2 = UpdateAssetDtoOrientationEnum._(2);
|
||||
static const number3 = UpdateAssetDtoOrientationEnum._(3);
|
||||
static const number4 = UpdateAssetDtoOrientationEnum._(4);
|
||||
static const number5 = UpdateAssetDtoOrientationEnum._(5);
|
||||
static const number6 = UpdateAssetDtoOrientationEnum._(6);
|
||||
static const number7 = UpdateAssetDtoOrientationEnum._(7);
|
||||
static const number8 = UpdateAssetDtoOrientationEnum._(8);
|
||||
|
||||
/// List of all possible values in this [enum][UpdateAssetDtoOrientationEnum].
|
||||
static const values = <UpdateAssetDtoOrientationEnum>[
|
||||
number1,
|
||||
number2,
|
||||
number3,
|
||||
number4,
|
||||
number5,
|
||||
number6,
|
||||
number7,
|
||||
number8,
|
||||
];
|
||||
|
||||
static UpdateAssetDtoOrientationEnum? fromJson(dynamic value) => UpdateAssetDtoOrientationEnumTypeTransformer().decode(value);
|
||||
|
||||
static List<UpdateAssetDtoOrientationEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UpdateAssetDtoOrientationEnum>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UpdateAssetDtoOrientationEnum.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [UpdateAssetDtoOrientationEnum] to int,
|
||||
/// and [decode] dynamic data back to [UpdateAssetDtoOrientationEnum].
|
||||
class UpdateAssetDtoOrientationEnumTypeTransformer {
|
||||
factory UpdateAssetDtoOrientationEnumTypeTransformer() => _instance ??= const UpdateAssetDtoOrientationEnumTypeTransformer._();
|
||||
|
||||
const UpdateAssetDtoOrientationEnumTypeTransformer._();
|
||||
|
||||
int encode(UpdateAssetDtoOrientationEnum data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a UpdateAssetDtoOrientationEnum.
|
||||
///
|
||||
/// 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.
|
||||
UpdateAssetDtoOrientationEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case 1: return UpdateAssetDtoOrientationEnum.number1;
|
||||
case 2: return UpdateAssetDtoOrientationEnum.number2;
|
||||
case 3: return UpdateAssetDtoOrientationEnum.number3;
|
||||
case 4: return UpdateAssetDtoOrientationEnum.number4;
|
||||
case 5: return UpdateAssetDtoOrientationEnum.number5;
|
||||
case 6: return UpdateAssetDtoOrientationEnum.number6;
|
||||
case 7: return UpdateAssetDtoOrientationEnum.number7;
|
||||
case 8: return UpdateAssetDtoOrientationEnum.number8;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [UpdateAssetDtoOrientationEnumTypeTransformer] instance.
|
||||
static UpdateAssetDtoOrientationEnumTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ void main() {
|
||||
when(() => assetRepository.getAllByOwnerIdChecksum(any(), any()))
|
||||
.thenAnswer((_) async => [initialAssets[3], null, null]);
|
||||
when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []);
|
||||
when(() => assetRepository.deleteById(any())).thenAnswer((_) async {});
|
||||
when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {});
|
||||
when(() => exifInfoRepository.updateAll(any()))
|
||||
.thenAnswer((_) async => []);
|
||||
when(() => assetRepository.transaction<void>(any())).thenAnswer(
|
||||
|
||||
@@ -2079,65 +2079,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/audit/deletes": {
|
||||
"get": {
|
||||
"operationId": "getAuditDeletes",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "after",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "entityType",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EntityType"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AuditDeletesResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Audit"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/auth/admin-sign-up": {
|
||||
"post": {
|
||||
"operationId": "signUpAdmin",
|
||||
@@ -7963,21 +7904,6 @@
|
||||
"longitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"orientation": {
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8
|
||||
],
|
||||
"maximum": 8,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
@@ -8658,24 +8584,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AuditDeletesResponseDto": {
|
||||
"properties": {
|
||||
"ids": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"needsFullSync": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ids",
|
||||
"needsFullSync"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AvatarResponse": {
|
||||
"properties": {
|
||||
"color": {
|
||||
@@ -9090,13 +8998,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EntityType": {
|
||||
"enum": [
|
||||
"ASSET",
|
||||
"ALBUM"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ExifResponseDto": {
|
||||
"properties": {
|
||||
"city": {
|
||||
@@ -11514,6 +11415,9 @@
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewCount": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11528,7 +11432,8 @@
|
||||
"password",
|
||||
"showMetadata",
|
||||
"type",
|
||||
"userId"
|
||||
"userId",
|
||||
"viewCount"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -12895,21 +12800,6 @@
|
||||
"longitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"orientation": {
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8
|
||||
],
|
||||
"maximum": 8,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.13.1
|
||||
22.14.0
|
||||
|
||||
15
open-api/typescript-sdk/package-lock.json
generated
15
open-api/typescript-sdk/package-lock.json
generated
@@ -12,19 +12,20 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@oazapfts/runtime": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.3.tgz",
|
||||
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.4.tgz",
|
||||
"integrity": "sha512-7t6C2shug/6tZhQgkCa532oTYBLEnbASV/i1SG1rH2GB4h3aQQujYciYSPT92hvN4IwTe8S2hPkN/6iiOyTlCg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
|
||||
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
|
||||
"version": "22.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
|
||||
"integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"repository": {
|
||||
@@ -28,6 +28,6 @@
|
||||
"directory": "open-api/typescript-sdk"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.1"
|
||||
"node": "22.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +391,6 @@ export type AssetBulkUpdateDto = {
|
||||
isFavorite?: boolean;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
orientation?: Orientation;
|
||||
rating?: number;
|
||||
};
|
||||
export type AssetBulkUploadCheckItem = {
|
||||
@@ -440,7 +439,6 @@ export type UpdateAssetDto = {
|
||||
latitude?: number;
|
||||
livePhotoVideoId?: string | null;
|
||||
longitude?: number;
|
||||
orientation?: Orientation;
|
||||
rating?: number;
|
||||
};
|
||||
export type AssetMediaReplaceDto = {
|
||||
@@ -451,10 +449,6 @@ export type AssetMediaReplaceDto = {
|
||||
fileCreatedAt: string;
|
||||
fileModifiedAt: string;
|
||||
};
|
||||
export type AuditDeletesResponseDto = {
|
||||
ids: string[];
|
||||
needsFullSync: boolean;
|
||||
};
|
||||
export type SignUpDto = {
|
||||
email: string;
|
||||
name: string;
|
||||
@@ -1066,6 +1060,7 @@ export type SharedLinkResponseDto = {
|
||||
token?: string | null;
|
||||
"type": SharedLinkType;
|
||||
userId: string;
|
||||
viewCount: number;
|
||||
};
|
||||
export type SharedLinkCreateDto = {
|
||||
albumId?: string;
|
||||
@@ -1915,22 +1910,6 @@ export function playAssetVideo({ id, key }: {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getAuditDeletes({ after, entityType, userId }: {
|
||||
after: string;
|
||||
entityType: EntityType;
|
||||
userId?: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: AuditDeletesResponseDto;
|
||||
}>(`/audit/deletes${QS.query(QS.explode({
|
||||
after,
|
||||
entityType,
|
||||
userId
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function signUpAdmin({ signUpDto }: {
|
||||
signUpDto: SignUpDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@@ -3483,16 +3462,6 @@ export enum AssetMediaStatus {
|
||||
Replaced = "replaced",
|
||||
Duplicate = "duplicate"
|
||||
}
|
||||
export enum Orientation {
|
||||
$1 = 1,
|
||||
$2 = 2,
|
||||
$3 = 3,
|
||||
$4 = 4,
|
||||
$5 = 5,
|
||||
$6 = 6,
|
||||
$7 = 7,
|
||||
$8 = 8
|
||||
}
|
||||
export enum Action {
|
||||
Accept = "accept",
|
||||
Reject = "reject"
|
||||
@@ -3511,10 +3480,6 @@ export enum AssetMediaSize {
|
||||
Preview = "preview",
|
||||
Thumbnail = "thumbnail"
|
||||
}
|
||||
export enum EntityType {
|
||||
Asset = "ASSET",
|
||||
Album = "ALBUM"
|
||||
}
|
||||
export enum ManualJobName {
|
||||
PersonCleanup = "person-cleanup",
|
||||
TagCleanup = "tag-cleanup",
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.13.1
|
||||
22.14.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:20250211@sha256:6ae577a6518e1ccca973db16955f4d79b01cac3ae122759ccb1c17bf6c330ba9 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:20250218@sha256:04df131dafca34538685453e4a00387ffe14288edff43cc68cf44feb76c8f4c0 AS dev
|
||||
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
@@ -42,7 +42,7 @@ RUN npm run build
|
||||
|
||||
|
||||
# prod build
|
||||
FROM ghcr.io/immich-app/base-server-prod:20250211@sha256:879cfe3d2afd4b7bdb211f694f99b6c0679a8bbd3e96964ad6c878f8471d63ea
|
||||
FROM ghcr.io/immich-app/base-server-prod:20250218@sha256:c5980c358dc94b62cac2086edaa1fd1ae855340a0d8e386fd92bc527d8aaadaa
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Immich server project
|
||||
|
||||
This project uses the [NestJS](https://nestjs.com/) web framework. Please refer to [the NestJS docs](https://docs.nestjs.com/) for information on getting started as a contributor to this project.
|
||||
896
server/package-lock.json
generated
896
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@
|
||||
"@opentelemetry/context-async-hooks": "^1.24.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.57.0",
|
||||
"@opentelemetry/sdk-node": "^0.57.0",
|
||||
"@react-email/components": "^0.0.32",
|
||||
"@react-email/components": "^0.0.33",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"archiver": "^7.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
@@ -112,7 +112,7 @@
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.2",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
@@ -145,6 +145,6 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.1"
|
||||
"node": "22.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuditDeletesDto, AuditDeletesResponseDto } from 'src/dtos/audit.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { AuditService } from 'src/services/audit.service';
|
||||
|
||||
@ApiTags('Audit')
|
||||
@Controller('audit')
|
||||
export class AuditController {
|
||||
constructor(private service: AuditService) {}
|
||||
|
||||
@Get('deletes')
|
||||
@Authenticated()
|
||||
getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
|
||||
return this.service.getDeletes(auth, dto);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { APIKeyController } from 'src/controllers/api-key.controller';
|
||||
import { AppController } from 'src/controllers/app.controller';
|
||||
import { AssetMediaController } from 'src/controllers/asset-media.controller';
|
||||
import { AssetController } from 'src/controllers/asset.controller';
|
||||
import { AuditController } from 'src/controllers/audit.controller';
|
||||
import { AuthController } from 'src/controllers/auth.controller';
|
||||
import { DownloadController } from 'src/controllers/download.controller';
|
||||
import { DuplicateController } from 'src/controllers/duplicate.controller';
|
||||
@@ -40,7 +39,6 @@ export const controllers = [
|
||||
AppController,
|
||||
AssetController,
|
||||
AssetMediaController,
|
||||
AuditController,
|
||||
AuthController,
|
||||
DownloadController,
|
||||
DuplicateController,
|
||||
|
||||
1
server/src/db.d.ts
vendored
1
server/src/db.d.ts
vendored
@@ -312,6 +312,7 @@ export interface SharedLinks {
|
||||
showExif: Generated<boolean>;
|
||||
type: string;
|
||||
userId: string;
|
||||
viewCount: Generated<number>;
|
||||
}
|
||||
|
||||
export interface SmartSearch {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetType, ExifOrientation } from 'src/enum';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { AssetStats } from 'src/repositories/asset.repository';
|
||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||
|
||||
@@ -54,12 +54,6 @@ export class UpdateAssetBase {
|
||||
@Max(5)
|
||||
@Min(-1)
|
||||
rating?: number;
|
||||
|
||||
@Optional()
|
||||
@Min(1)
|
||||
@Max(8)
|
||||
@ApiProperty({ type: 'integer' })
|
||||
orientation?: ExifOrientation;
|
||||
}
|
||||
|
||||
export class AssetBulkUpdateDto extends UpdateAssetBase {
|
||||
|
||||
@@ -94,6 +94,7 @@ export class SharedLinkResponseDto {
|
||||
type!: SharedLinkType;
|
||||
createdAt!: Date;
|
||||
expiresAt!: Date | null;
|
||||
viewCount!: number;
|
||||
assets!: AssetResponseDto[];
|
||||
album?: AlbumResponseDto;
|
||||
allowUpload!: boolean;
|
||||
@@ -102,7 +103,7 @@ export class SharedLinkResponseDto {
|
||||
showMetadata!: boolean;
|
||||
}
|
||||
|
||||
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||
const map = (sharedLink: SharedLinkEntity, { stripMetadata }: { stripMetadata: boolean }) => {
|
||||
const linkAssets = sharedLink.assets || [];
|
||||
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
||||
|
||||
@@ -115,35 +116,16 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD
|
||||
userId: sharedLink.userId,
|
||||
key: sharedLink.key.toString('base64url'),
|
||||
type: sharedLink.type,
|
||||
viewCount: sharedLink.viewCount,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map((asset) => mapAsset(asset)),
|
||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showMetadata: sharedLink.showExif,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||
const linkAssets = sharedLink.assets || [];
|
||||
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
||||
|
||||
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
|
||||
|
||||
return {
|
||||
id: sharedLink.id,
|
||||
description: sharedLink.description,
|
||||
password: sharedLink.password,
|
||||
userId: sharedLink.userId,
|
||||
key: sharedLink.key.toString('base64url'),
|
||||
type: sharedLink.type,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })) as AssetResponseDto[],
|
||||
assets: assets.map((asset) => mapAsset(asset, { stripMetadata })),
|
||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showMetadata: sharedLink.showExif,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const mapSharedLink = (sharedLink: SharedLinkEntity) => map(sharedLink, { stripMetadata: false });
|
||||
export const mapSharedLinkWithoutMetadata = (sharedLink: SharedLinkEntity) => map(sharedLink, { stripMetadata: true });
|
||||
|
||||
@@ -50,7 +50,7 @@ export const ImmichLayout = ({ children, preview }: ImmichLayoutProps) => (
|
||||
<Section className="flex justify-center mb-12">
|
||||
<Img
|
||||
src="https://immich.app/img/immich-logo-inline-light.png"
|
||||
className="h-12 antialiased rounded-none"
|
||||
className="h-12 antialiased rounded-none w-full"
|
||||
alt="Immich"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
@@ -62,4 +62,7 @@ export class SharedLinkEntity {
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
albumId!: string | null;
|
||||
|
||||
@Column({ default: 0 })
|
||||
viewCount!: number;
|
||||
}
|
||||
|
||||
14
server/src/migrations/1739916305466-AddViewCount.ts
Normal file
14
server/src/migrations/1739916305466-AddViewCount.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddViewCount1739916305466 implements MigrationInterface {
|
||||
name = 'AddViewCount1739916305466'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" ADD "viewCount" integer NOT NULL DEFAULT '0'`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "viewCount"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -169,7 +169,6 @@ from
|
||||
and "asset_faces"."personId" = $1
|
||||
and "assets"."isArchived" = $2
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."livePhotoVideoId" is null
|
||||
|
||||
-- PersonRepository.getNumberOfPeople
|
||||
select
|
||||
|
||||
@@ -194,3 +194,12 @@ where
|
||||
"shared_links"."type" = $2
|
||||
or "albums"."id" is not null
|
||||
)
|
||||
|
||||
-- SharedLinkRepository.incrementViewCount
|
||||
update "shared_links"
|
||||
set
|
||||
"viewCount" = viewCount + 1
|
||||
where
|
||||
"shared_links"."id" = $1
|
||||
returning
|
||||
*
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
|
||||
import geotz from 'geo-tz';
|
||||
import { LogLevel } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
interface ExifDuration {
|
||||
@@ -102,9 +101,6 @@ export class MetadataRepository {
|
||||
}
|
||||
|
||||
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
||||
if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) {
|
||||
this.logger.verbose(`Writing tags ${JSON.stringify(tags)} to ${path}`);
|
||||
}
|
||||
try {
|
||||
await this.exiftool.write(path, tags);
|
||||
} catch (error) {
|
||||
|
||||
@@ -314,8 +314,7 @@ export class PersonRepository {
|
||||
.onRef('assets.id', '=', 'asset_faces.assetId')
|
||||
.on('asset_faces.personId', '=', personId)
|
||||
.on('assets.isArchived', '=', false)
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.livePhotoVideoId', 'is', null),
|
||||
.on('assets.deletedAt', 'is', null),
|
||||
)
|
||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
|
||||
.executeTakeFirst();
|
||||
|
||||
@@ -216,6 +216,15 @@ export class SharedLinkRepository {
|
||||
await this.db.deleteFrom('shared_links').where('shared_links.id', '=', entity.id).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async incrementViewCount(id: string) {
|
||||
await this.db
|
||||
.updateTable('shared_links')
|
||||
.set('viewCount', sql<number>`"viewCount" + 1`)
|
||||
.where('shared_links.id', '=', id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
private getSharedLinks(id: string) {
|
||||
return this.db
|
||||
.selectFrom('shared_links')
|
||||
|
||||
@@ -100,7 +100,7 @@ export class AssetService extends BaseService {
|
||||
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||
|
||||
const { description, dateTimeOriginal, latitude, longitude, rating, orientation, ...rest } = dto;
|
||||
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
||||
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
||||
|
||||
let previousMotion: AssetEntity | null = null;
|
||||
@@ -113,7 +113,7 @@ export class AssetService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating, orientation });
|
||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
|
||||
|
||||
const asset = await this.assetRepository.update({ id, ...rest });
|
||||
|
||||
@@ -129,12 +129,11 @@ export class AssetService extends BaseService {
|
||||
}
|
||||
|
||||
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||
const { ids, dateTimeOriginal, latitude, longitude, orientation, ...options } = dto;
|
||||
const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids });
|
||||
|
||||
// TODO rewrite this to support batching
|
||||
for (const id of ids) {
|
||||
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude, orientation });
|
||||
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -285,14 +284,11 @@ export class AssetService extends BaseService {
|
||||
}
|
||||
|
||||
private async updateMetadata(dto: ISidecarWriteJob) {
|
||||
const { id, description, dateTimeOriginal, latitude, longitude, rating, orientation } = dto;
|
||||
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating, orientation }, _.isUndefined);
|
||||
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
|
||||
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined);
|
||||
if (Object.keys(writes).length > 0) {
|
||||
await this.assetRepository.upsertExif({ assetId: id, ...writes });
|
||||
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } });
|
||||
if (orientation !== undefined) {
|
||||
await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAILS, data: { id, notify: true } });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user