forked from Cutlery/immich
Merge branch 'main' into feat/offline-files-job
This commit is contained in:
commit
482645e22d
@ -9,8 +9,8 @@ The database is saved to your Immich upload folder in the `database-backup` subd
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Borg needs to be installed on your server as well as the remote machine. You can find instructions to install Borg [here](https://borgbackup.readthedocs.io/en/latest/installation.html).
|
- Borg needs to be installed on your server as well as the remote machine. You can find instructions to install Borg [here](https://borgbackup.readthedocs.io/en/latest/installation.html).
|
||||||
- To run this sript as a non-root user, you should [add your username to the docker group](https://docs.docker.com/engine/install/linux-postinstall/).
|
- (Optional) To run this sript as a non-root user, you should [add your username to the docker group](https://docs.docker.com/engine/install/linux-postinstall/).
|
||||||
- To run this script non-interactively, set up [passwordless ssh](https://www.redhat.com/sysadmin/passwordless-ssh) to your remote machine from your server.
|
- To run this script non-interactively, set up [passwordless ssh](https://www.redhat.com/sysadmin/passwordless-ssh) to your remote machine from your server. If you skipped the previous step, make sure this step is done from your root account.
|
||||||
|
|
||||||
To initialize the borg repository, run the following commands once.
|
To initialize the borg repository, run the following commands once.
|
||||||
|
|
||||||
@ -19,16 +19,13 @@ UPLOAD_LOCATION="/path/to/immich/directory" # Immich database location, as
|
|||||||
BACKUP_PATH="/path/to/local/backup/directory"
|
BACKUP_PATH="/path/to/local/backup/directory"
|
||||||
|
|
||||||
mkdir "$UPLOAD_LOCATION/database-backup"
|
mkdir "$UPLOAD_LOCATION/database-backup"
|
||||||
mkdir "$BACKUP_PATH/immich-borg"
|
|
||||||
|
|
||||||
borg init --encryption=none "$BACKUP_PATH/immich-borg"
|
borg init --encryption=none "$BACKUP_PATH/immich-borg"
|
||||||
|
|
||||||
## Remote set up
|
## Remote set up
|
||||||
REMOTE_HOST="remote_host@IP"
|
REMOTE_HOST="remote_host@IP"
|
||||||
REMOTE_BACKUP_PATH="/path/to/remote/backup/directory"
|
REMOTE_BACKUP_PATH="/path/to/remote/backup/directory"
|
||||||
|
|
||||||
ssh "$REMOTE_HOST" "mkdir \"$REMOTE_BACKUP_PATH\"/immich-borg"
|
borg init --encryption=none "$REMOTE_HOST:$REMOTE_BACKUP_PATH/immich-borg"
|
||||||
ssh "$REMOTE_HOST" "borg init --encryption=none \"$REMOTE_BACKUP_PATH\"/immich-borg"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit the following script as necessary and add it to your crontab. Note that this script assumes there are no `:`, `@`, or `"` characters in your paths. If these characters exist, you will need to escape and/or rename the paths.
|
Edit the following script as necessary and add it to your crontab. Note that this script assumes there are no `:`, `@`, or `"` characters in your paths. If these characters exist, you will need to escape and/or rename the paths.
|
||||||
|
56
mobile/assets/immich-logo-inline-dark.svg
Normal file
56
mobile/assets/immich-logo-inline-dark.svg
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Router_Medium_x5F_Black_00000159464448132936669960000002337362428709113490_"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 792 266.25"
|
||||||
|
style="enable-background:new 0 0 792 266.25;" xml:space="preserve">
|
||||||
|
|
||||||
|
<g>
|
||||||
|
<path class="st0" style="fill:#ACCBFA;" d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||||
|
C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||||
|
c5.84,0,10.52,4.67,10.52,10.68c0,2.84-0.83,7.68-0.83,10.68v38.73c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||||
|
c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||||
|
<path class="st0" style="fill:#ACCBFA;" d="M394.28,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||||
|
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||||
|
c0-2.84,0.83-7.68,0.83-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||||
|
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||||
|
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||||
|
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||||
|
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C399.12,182.55,394.28,177.88,394.28,171.87z"/>
|
||||||
|
<path class="st0" style="fill:#ACCBFA;" d="M528.5,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||||
|
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||||
|
c0-2.84,0.84-7.68,0.84-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||||
|
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||||
|
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||||
|
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||||
|
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C533.35,182.55,528.5,177.88,528.5,171.87z"/>
|
||||||
|
<path class="st0" style="fill:#ACCBFA;" d="M576.92,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||||
|
C565.23,68.36,570.57,63.18,576.92,63.18z M567.07,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||||
|
s10.52,4.67,10.52,10.68c0,2.84-0.84,7.68-0.84,10.68v38.73c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||||
|
s-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||||
|
<path class="st0" style="fill:#ACCBFA;" d="M601.79,141.31c0-23.54,14.69-42.57,39.07-42.57c12.86,0,24.71,5.84,30.05,14.53c2,3.17,2.34,5.01,2.34,6.51
|
||||||
|
c0,5.18-4.01,9.52-9.85,9.52c-3.84,0-7.34-2.17-8.85-6.01c-2.34-5.18-6.85-8.18-13.69-8.18c-12.86,0-20.03,11.52-20.03,26.04
|
||||||
|
c0,14.69,7.51,26.04,20.53,26.04c7.01,0,12.02-2.5,14.36-7.68c1.67-3.51,4.84-6.51,9.18-6.51c6.01,0,9.68,4.17,9.68,9.35
|
||||||
|
c0,2.5-1,5.51-3.17,8.35c-5.51,7.35-15.86,13.19-30.05,13.19C616.32,183.89,601.79,165.19,601.79,141.31z"/>
|
||||||
|
<path class="st0" style="fill:#ACCBFA;" d="M737.69,171.87c0-2.84,0.67-7.68,0.67-10.68v-28.55c0-10.18-5.68-17.2-15.36-17.2
|
||||||
|
c-6.68,0-12.35,3.17-16.03,8.35v37.4c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.67,10.68-10.52,10.68s-10.52-4.67-10.52-10.68
|
||||||
|
c0-2.84,0.84-7.68,0.84-10.68v-80.8c0-3.01-0.84-7.85-0.84-10.68c0-6.01,4.84-10.68,10.52-10.68c5.84,0,10.52,4.67,10.52,10.68
|
||||||
|
c0,2.84-0.67,7.68-0.67,10.68v27.21c5.01-5.51,12.19-8.85,21.37-8.85c17.2,0,29.55,12.86,29.55,31.22v31.22
|
||||||
|
c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68C742.36,182.55,737.69,177.88,737.69,171.87z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" style="fill:#FA2921;" d="M114.82,96.21c11.92,10.55,21.52,21.86,27.7,32.52c10.62-18.99,17.71-41.55,17.8-55.92c0-0.1,0-0.19,0-0.28
|
||||||
|
c0-21.26-21.21-29.54-39.48-29.54s-39.48,8.28-39.48,29.54c0,0.29,0,0.68,0,1.15C91.54,78.2,103.61,86.29,114.82,96.21z"/>
|
||||||
|
<path class="st2" style="fill:#ED79B5;" d="M49.8,154.19c7.45-8.29,18.88-17.27,31.77-24.86c13.72-8.07,27.44-13.71,39.49-16.3
|
||||||
|
c-14.78-15.96-34.04-29.68-47.68-34.21c-0.1-0.03-0.18-0.06-0.27-0.09c-20.22-6.57-34.65,11.05-40.3,28.42s-4.33,40.11,15.89,46.68
|
||||||
|
C48.99,153.93,49.35,154.05,49.8,154.19z"/>
|
||||||
|
<path class="st3" style="fill:#FFB400;" d="M209.07,106.86c-5.65-17.38-20.07-34.99-40.3-28.42c-0.28,0.09-0.65,0.21-1.09,0.35
|
||||||
|
c-1.16,11.08-5.12,25.07-11.09,38.79c-6.35,14.6-14.14,27.23-22.36,36.39c21.34,4.23,44.99,4,58.68-0.35
|
||||||
|
c0.1-0.03,0.19-0.06,0.27-0.09C213.4,146.97,214.71,124.24,209.07,106.86z"/>
|
||||||
|
<path class="st4" style="fill:#1E83F7;" d="M102.8,171.18c-3.44-15.54-4.56-30.34-3.3-42.59c-19.75,9.12-38.75,23.2-47.27,34.78
|
||||||
|
c-0.06,0.08-0.11,0.16-0.16,0.23c-12.5,17.2-0.2,36.37,14.58,47.11s36.81,16.51,49.31-0.69c0.17-0.24,0.4-0.55,0.68-0.93
|
||||||
|
C111.05,199.44,106.04,185.79,102.8,171.18z"/>
|
||||||
|
<path class="st5" style="fill:#18C249;" d="M189.48,162.49c-10.9,2.33-25.42,2.88-40.32,1.44c-15.84-1.53-30.26-5.03-41.52-10.02
|
||||||
|
c2.57,21.6,10.09,44.02,18.47,55.7c0.06,0.08,0.11,0.16,0.16,0.23c12.5,17.2,34.52,11.43,49.31,0.69
|
||||||
|
c14.78-10.74,27.08-29.9,14.58-47.11C189.99,163.18,189.76,162.86,189.48,162.49z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.6 KiB |
54
mobile/assets/immich-logo-inline-light.svg
Normal file
54
mobile/assets/immich-logo-inline-light.svg
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Router_Medium_x5F_White" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px" y="0px" viewBox="0 0 792 266.25" style="enable-background:new 0 0 792 266.25;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path class="st0" style="fill:#4251B0;" d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||||
|
C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||||
|
c5.84,0,10.52,4.67,10.52,10.68c0,2.84-0.83,7.68-0.83,10.68v38.73c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||||
|
c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||||
|
<path class="st0" style="fill:#4251B0;" d="M394.28,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||||
|
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||||
|
c0-2.84,0.83-7.68,0.83-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||||
|
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||||
|
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||||
|
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||||
|
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C399.12,182.55,394.28,177.88,394.28,171.87z"/>
|
||||||
|
<path class="st0" style="fill:#4251B0;" d="M528.5,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||||
|
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||||
|
c0-2.84,0.84-7.68,0.84-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||||
|
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||||
|
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||||
|
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||||
|
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C533.35,182.55,528.5,177.88,528.5,171.87z"/>
|
||||||
|
<path class="st0" style="fill:#4251B0;" d="M576.92,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||||
|
C565.23,68.36,570.57,63.18,576.92,63.18z M567.07,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||||
|
s10.52,4.67,10.52,10.68c0,2.84-0.84,7.68-0.84,10.68v38.73c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||||
|
s-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||||
|
<path class="st0" style="fill:#4251B0;" d="M601.79,141.31c0-23.54,14.69-42.57,39.07-42.57c12.86,0,24.71,5.84,30.05,14.53c2,3.17,2.34,5.01,2.34,6.51
|
||||||
|
c0,5.18-4.01,9.52-9.85,9.52c-3.84,0-7.34-2.17-8.85-6.01c-2.34-5.18-6.85-8.18-13.69-8.18c-12.86,0-20.03,11.52-20.03,26.04
|
||||||
|
c0,14.69,7.51,26.04,20.53,26.04c7.01,0,12.02-2.5,14.36-7.68c1.67-3.51,4.84-6.51,9.18-6.51c6.01,0,9.68,4.17,9.68,9.35
|
||||||
|
c0,2.5-1,5.51-3.17,8.35c-5.51,7.35-15.86,13.19-30.05,13.19C616.32,183.89,601.79,165.19,601.79,141.31z"/>
|
||||||
|
<path class="st0" style="fill:#4251B0;" d="M737.69,171.87c0-2.84,0.67-7.68,0.67-10.68v-28.55c0-10.18-5.68-17.2-15.36-17.2
|
||||||
|
c-6.68,0-12.35,3.17-16.03,8.35v37.4c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.67,10.68-10.52,10.68s-10.52-4.67-10.52-10.68
|
||||||
|
c0-2.84,0.84-7.68,0.84-10.68v-80.8c0-3.01-0.84-7.85-0.84-10.68c0-6.01,4.84-10.68,10.52-10.68c5.84,0,10.52,4.67,10.52,10.68
|
||||||
|
c0,2.84-0.67,7.68-0.67,10.68v27.21c5.01-5.51,12.19-8.85,21.37-8.85c17.2,0,29.55,12.86,29.55,31.22v31.22
|
||||||
|
c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68C742.36,182.55,737.69,177.88,737.69,171.87z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" style="fill:#FA2921;" d="M114.82,96.21c11.92,10.55,21.52,21.86,27.7,32.52c10.62-18.99,17.71-41.55,17.8-55.92c0-0.1,0-0.19,0-0.28
|
||||||
|
c0-21.26-21.21-29.54-39.48-29.54s-39.48,8.28-39.48,29.54c0,0.29,0,0.68,0,1.15C91.54,78.2,103.61,86.29,114.82,96.21z"/>
|
||||||
|
<path class="st2" style="fill:#ED79B5;" d="M49.8,154.19c7.45-8.29,18.88-17.27,31.77-24.86c13.72-8.07,27.44-13.71,39.49-16.3
|
||||||
|
c-14.78-15.96-34.04-29.68-47.68-34.21c-0.1-0.03-0.18-0.06-0.27-0.09c-20.22-6.57-34.65,11.05-40.3,28.42s-4.33,40.11,15.89,46.68
|
||||||
|
C48.99,153.93,49.35,154.05,49.8,154.19z"/>
|
||||||
|
<path class="st3" style="fill:#FFB400;" d="M209.07,106.86c-5.65-17.38-20.07-34.99-40.3-28.42c-0.28,0.09-0.65,0.21-1.09,0.35
|
||||||
|
c-1.16,11.08-5.12,25.07-11.09,38.79c-6.35,14.6-14.14,27.23-22.36,36.39c21.34,4.23,44.99,4,58.68-0.35
|
||||||
|
c0.1-0.03,0.19-0.06,0.27-0.09C213.4,146.97,214.71,124.24,209.07,106.86z"/>
|
||||||
|
<path class="st4" style="fill:#1E83F7;" d="M102.8,171.18c-3.44-15.54-4.56-30.34-3.3-42.59c-19.75,9.12-38.75,23.2-47.27,34.78
|
||||||
|
c-0.06,0.08-0.11,0.16-0.16,0.23c-12.5,17.2-0.2,36.37,14.58,47.11s36.81,16.51,49.31-0.69c0.17-0.24,0.4-0.55,0.68-0.93
|
||||||
|
C111.05,199.44,106.04,185.79,102.8,171.18z"/>
|
||||||
|
<path class="st5" style="fill:#18C249;" d="M189.48,162.49c-10.9,2.33-25.42,2.88-40.32,1.44c-15.84-1.53-30.26-5.03-41.52-10.02
|
||||||
|
c2.57,21.6,10.09,44.02,18.47,55.7c0.06,0.08,0.11,0.16,0.16,0.23c12.5,17.2,34.52,11.43,49.31,0.69
|
||||||
|
c14.78-10.74,27.08-29.9,14.58-47.11C189.99,163.18,189.76,162.86,189.48,162.49z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.5 KiB |
@ -180,4 +180,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
COCOAPODS: 1.12.1
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
@ -169,11 +170,11 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 3.0),
|
padding: const EdgeInsets.only(top: 3.0),
|
||||||
child: Image.asset(
|
child: SvgPicture.asset(
|
||||||
height: 30,
|
|
||||||
context.isDarkTheme
|
context.isDarkTheme
|
||||||
? 'assets/immich-logo-inline-dark.png'
|
? 'assets/immich-logo-inline-dark.svg'
|
||||||
: 'assets/immich-logo-inline-light.png',
|
: 'assets/immich-logo-inline-light.svg',
|
||||||
|
height: 40,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -18,6 +18,7 @@ class ImmichLogo extends StatelessWidget {
|
|||||||
image: const AssetImage('assets/immich-logo.png'),
|
image: const AssetImage('assets/immich-logo.png'),
|
||||||
width: size,
|
width: size,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
|
isAntiAlias: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@ -161,6 +161,7 @@ Class | Method | HTTP request | Description
|
|||||||
*PersonApi* | [**reassignFaces**](doc//PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
|
*PersonApi* | [**reassignFaces**](doc//PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
|
||||||
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
|
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
|
||||||
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
|
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
|
||||||
|
*SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities |
|
||||||
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
|
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
|
||||||
*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
|
*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
|
||||||
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
|
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
|
||||||
|
52
mobile/openapi/doc/SearchApi.md
generated
52
mobile/openapi/doc/SearchApi.md
generated
@ -9,6 +9,7 @@ All URIs are relative to */api*
|
|||||||
|
|
||||||
Method | HTTP request | Description
|
Method | HTTP request | Description
|
||||||
------------- | ------------- | -------------
|
------------- | ------------- | -------------
|
||||||
|
[**getAssetsByCity**](SearchApi.md#getassetsbycity) | **GET** /search/cities |
|
||||||
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
|
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
|
||||||
[**getSearchSuggestions**](SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
|
[**getSearchSuggestions**](SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
|
||||||
[**search**](SearchApi.md#search) | **GET** /search |
|
[**search**](SearchApi.md#search) | **GET** /search |
|
||||||
@ -18,6 +19,57 @@ Method | HTTP request | Description
|
|||||||
[**searchSmart**](SearchApi.md#searchsmart) | **POST** /search/smart |
|
[**searchSmart**](SearchApi.md#searchsmart) | **POST** /search/smart |
|
||||||
|
|
||||||
|
|
||||||
|
# **getAssetsByCity**
|
||||||
|
> List<AssetResponseDto> getAssetsByCity()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure API key authorization: cookie
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure API key authorization: api_key
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = SearchApi();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.getAssetsByCity();
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling SearchApi->getAssetsByCity: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
This endpoint does not need any parameter.
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**List<AssetResponseDto>**](AssetResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **getExploreData**
|
# **getExploreData**
|
||||||
> List<SearchExploreResponseDto> getExploreData()
|
> List<SearchExploreResponseDto> getExploreData()
|
||||||
|
|
||||||
|
44
mobile/openapi/lib/api/search_api.dart
generated
44
mobile/openapi/lib/api/search_api.dart
generated
@ -16,6 +16,50 @@ class SearchApi {
|
|||||||
|
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /search/cities' operation and returns the [Response].
|
||||||
|
Future<Response> getAssetsByCityWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/search/cities';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<AssetResponseDto>?> getAssetsByCity() async {
|
||||||
|
final response = await getAssetsByCityWithHttpInfo();
|
||||||
|
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) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
|
||||||
|
.cast<AssetResponseDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /search/explore' operation and returns the [Response].
|
/// Performs an HTTP 'GET /search/explore' operation and returns the [Response].
|
||||||
Future<Response> getExploreDataWithHttpInfo() async {
|
Future<Response> getExploreDataWithHttpInfo() async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
|
5
mobile/openapi/test/search_api_test.dart
generated
5
mobile/openapi/test/search_api_test.dart
generated
@ -17,6 +17,11 @@ void main() {
|
|||||||
// final instance = SearchApi();
|
// final instance = SearchApi();
|
||||||
|
|
||||||
group('tests for SearchApi', () {
|
group('tests for SearchApi', () {
|
||||||
|
//Future<List<AssetResponseDto>> getAssetsByCity() async
|
||||||
|
test('test getAssetsByCity', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
//Future<List<SearchExploreResponseDto>> getExploreData() async
|
//Future<List<SearchExploreResponseDto>> getExploreData() async
|
||||||
test('test getExploreData', () async {
|
test('test getExploreData', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -560,6 +560,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.9"
|
version: "2.4.9"
|
||||||
|
flutter_svg:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_svg
|
||||||
|
sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.9"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -1006,6 +1014,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.3"
|
version: "1.8.3"
|
||||||
|
path_parsing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1587,6 +1603,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
vector_graphics:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics
|
||||||
|
sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.10+1"
|
||||||
|
vector_graphics_codec:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_codec
|
||||||
|
sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.10+1"
|
||||||
|
vector_graphics_compiler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_compiler
|
||||||
|
sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.10+1"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -34,6 +34,7 @@ dependencies:
|
|||||||
ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
|
ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
|
||||||
geolocator: ^11.0.0 # used to move to current location in map view
|
geolocator: ^11.0.0 # used to move to current location in map view
|
||||||
flutter_udid: ^3.0.0
|
flutter_udid: ^3.0.0
|
||||||
|
flutter_svg: ^2.0.9
|
||||||
package_info_plus: ^5.0.1
|
package_info_plus: ^5.0.1
|
||||||
url_launcher: ^6.2.4
|
url_launcher: ^6.2.4
|
||||||
http: 0.13.5
|
http: 0.13.5
|
||||||
|
@ -4597,6 +4597,41 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/search/cities": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getAssetsByCity",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AssetResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Search"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/search/explore": {
|
"/search/explore": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getExploreData",
|
"operationId": "getExploreData",
|
||||||
|
@ -2204,6 +2204,14 @@ export function search({ clip, motion, page, q, query, recent, size, smart, $typ
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
export function getAssetsByCity(opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: AssetResponseDto[];
|
||||||
|
}>("/search/cities", {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
export function getExploreData(opts?: Oazapfts.RequestOpts) {
|
export function getExploreData(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
|
@ -67,11 +67,11 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
originalFileName: 'silver_fir',
|
originalFileName: 'silver_fir.jpg',
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
originalFileName: 'tanners_ridge',
|
originalFileName: 'tanners_ridge.jpg',
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
@ -103,10 +103,10 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
expect(assets).toEqual(
|
expect(assets).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
originalFileName: 'el_torcal_rocks',
|
originalFileName: 'el_torcal_rocks.jpg',
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
originalFileName: 'silver_fir',
|
originalFileName: 'silver_fir.jpg',
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
@ -143,7 +143,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
|
|
||||||
expect(assets[0]).toEqual(
|
expect(assets[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
originalFileName: 'el_torcal_rocks',
|
originalFileName: 'el_torcal_rocks.jpg',
|
||||||
exifInfo: expect.objectContaining({
|
exifInfo: expect.objectContaining({
|
||||||
dateTimeOriginal: '2023-09-25T08:33:30.880Z',
|
dateTimeOriginal: '2023-09-25T08:33:30.880Z',
|
||||||
exifImageHeight: 534,
|
exifImageHeight: 534,
|
||||||
@ -190,7 +190,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
|
|
||||||
expect(assets[0]).toEqual(
|
expect(assets[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
originalFileName: 'el_torcal_rocks',
|
originalFileName: 'el_torcal_rocks.jpg',
|
||||||
exifInfo: expect.objectContaining({
|
exifInfo: expect.objectContaining({
|
||||||
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
|
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
|
||||||
}),
|
}),
|
||||||
@ -230,7 +230,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
|||||||
|
|
||||||
expect(assets[0]).toEqual(
|
expect(assets[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
originalFileName: 'el_torcal_rocks',
|
originalFileName: 'el_torcal_rocks.jpg',
|
||||||
exifInfo: expect.objectContaining({
|
exifInfo: expect.objectContaining({
|
||||||
exifImageHeight: 534,
|
exifImageHeight: 534,
|
||||||
exifImageWidth: 800,
|
exifImageWidth: 800,
|
||||||
|
@ -548,19 +548,19 @@ describe(AssetService.name, () => {
|
|||||||
await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(
|
await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
);
|
);
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the asset', async () => {
|
it('should update the asset', async () => {
|
||||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
assetMock.save.mockResolvedValue(assetStub.image);
|
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||||
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
|
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
|
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the exif description', async () => {
|
it('should update the exif description', async () => {
|
||||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
assetMock.save.mockResolvedValue(assetStub.image);
|
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||||
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
|
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
|
||||||
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
|
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
|
||||||
});
|
});
|
||||||
|
@ -324,7 +324,19 @@ export class AssetService {
|
|||||||
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
|
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
|
||||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
|
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
|
||||||
|
|
||||||
const asset = await this.assetRepository.save({ id, ...rest });
|
await this.assetRepository.update({ id, ...rest });
|
||||||
|
const asset = await this.assetRepository.getById(id, {
|
||||||
|
exifInfo: true,
|
||||||
|
owner: true,
|
||||||
|
smartInfo: true,
|
||||||
|
tags: true,
|
||||||
|
faces: {
|
||||||
|
person: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!asset) {
|
||||||
|
throw new BadRequestException('Asset not found');
|
||||||
|
}
|
||||||
return mapAsset(asset, { auth });
|
return mapAsset(asset, { auth });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,27 +93,27 @@ export class AuditService {
|
|||||||
|
|
||||||
switch (pathType) {
|
switch (pathType) {
|
||||||
case AssetPathType.ENCODED_VIDEO: {
|
case AssetPathType.ENCODED_VIDEO: {
|
||||||
await this.assetRepository.save({ id, encodedVideoPath: pathValue });
|
await this.assetRepository.update({ id, encodedVideoPath: pathValue });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case AssetPathType.JPEG_THUMBNAIL: {
|
case AssetPathType.JPEG_THUMBNAIL: {
|
||||||
await this.assetRepository.save({ id, resizePath: pathValue });
|
await this.assetRepository.update({ id, resizePath: pathValue });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case AssetPathType.WEBP_THUMBNAIL: {
|
case AssetPathType.WEBP_THUMBNAIL: {
|
||||||
await this.assetRepository.save({ id, webpPath: pathValue });
|
await this.assetRepository.update({ id, webpPath: pathValue });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case AssetPathType.ORIGINAL: {
|
case AssetPathType.ORIGINAL: {
|
||||||
await this.assetRepository.save({ id, originalPath: pathValue });
|
await this.assetRepository.update({ id, originalPath: pathValue });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case AssetPathType.SIDECAR: {
|
case AssetPathType.SIDECAR: {
|
||||||
await this.assetRepository.save({ id, sidecarPath: pathValue });
|
await this.assetRepository.update({ id, sidecarPath: pathValue });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,7 +384,7 @@ describe(LibraryService.name, () => {
|
|||||||
fileModifiedAt: expect.any(Date),
|
fileModifiedAt: expect.any(Date),
|
||||||
localDateTime: expect.any(Date),
|
localDateTime: expect.any(Date),
|
||||||
type: AssetType.IMAGE,
|
type: AssetType.IMAGE,
|
||||||
originalFileName: 'photo',
|
originalFileName: 'photo.jpg',
|
||||||
sidecarPath: null,
|
sidecarPath: null,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
@ -432,7 +432,7 @@ describe(LibraryService.name, () => {
|
|||||||
fileModifiedAt: expect.any(Date),
|
fileModifiedAt: expect.any(Date),
|
||||||
localDateTime: expect.any(Date),
|
localDateTime: expect.any(Date),
|
||||||
type: AssetType.IMAGE,
|
type: AssetType.IMAGE,
|
||||||
originalFileName: 'photo',
|
originalFileName: 'photo.jpg',
|
||||||
sidecarPath: '/data/user1/photo.jpg.xmp',
|
sidecarPath: '/data/user1/photo.jpg.xmp',
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
@ -479,7 +479,7 @@ describe(LibraryService.name, () => {
|
|||||||
fileModifiedAt: expect.any(Date),
|
fileModifiedAt: expect.any(Date),
|
||||||
localDateTime: expect.any(Date),
|
localDateTime: expect.any(Date),
|
||||||
type: AssetType.VIDEO,
|
type: AssetType.VIDEO,
|
||||||
originalFileName: 'video',
|
originalFileName: 'video.mp4',
|
||||||
sidecarPath: null,
|
sidecarPath: null,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
@ -591,7 +591,7 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true });
|
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true });
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -609,7 +609,7 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false });
|
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false });
|
||||||
|
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.METADATA_EXTRACTION,
|
name: JobName.METADATA_EXTRACTION,
|
||||||
@ -638,7 +638,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
});
|
});
|
||||||
@ -1264,7 +1264,7 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
await sut.watchAll();
|
await sut.watchAll();
|
||||||
|
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
|
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle an error event', async () => {
|
it('should handle an error event', async () => {
|
||||||
|
@ -172,7 +172,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`);
|
this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`);
|
||||||
const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path);
|
const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path);
|
||||||
if (asset && matcher(path)) {
|
if (asset && matcher(path)) {
|
||||||
await this.assetRepository.save({ id: asset.id, isOffline: true });
|
await this.assetRepository.update({ id: asset.id, isOffline: true });
|
||||||
}
|
}
|
||||||
this.emit(StorageEventType.UNLINK, path);
|
this.emit(StorageEventType.UNLINK, path);
|
||||||
};
|
};
|
||||||
@ -421,7 +421,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
// Mark asset as offline
|
// Mark asset as offline
|
||||||
this.logger.debug(`Marking asset as offline: ${assetPath}`);
|
this.logger.debug(`Marking asset as offline: ${assetPath}`);
|
||||||
|
|
||||||
await this.assetRepository.save({ id: existingAssetEntity.id, isOffline: true });
|
await this.assetRepository.update({ id: existingAssetEntity.id, isOffline: true });
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
} else {
|
} else {
|
||||||
// File can't be accessed and does not already exist in db
|
// File can't be accessed and does not already exist in db
|
||||||
@ -454,7 +454,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
if (stats && existingAssetEntity?.isOffline) {
|
if (stats && existingAssetEntity?.isOffline) {
|
||||||
// File was previously offline but is now online
|
// File was previously offline but is now online
|
||||||
this.logger.debug(`Marking previously-offline asset as online: ${assetPath}`);
|
this.logger.debug(`Marking previously-offline asset as online: ${assetPath}`);
|
||||||
await this.assetRepository.save({ id: existingAssetEntity.id, isOffline: false });
|
await this.assetRepository.update({ id: existingAssetEntity.id, isOffline: false });
|
||||||
doRefresh = true;
|
doRefresh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -503,7 +503,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
fileModifiedAt: stats.mtime,
|
fileModifiedAt: stats.mtime,
|
||||||
localDateTime: stats.mtime,
|
localDateTime: stats.mtime,
|
||||||
type: assetType,
|
type: assetType,
|
||||||
originalFileName: parse(assetPath).name,
|
originalFileName: parse(assetPath).base,
|
||||||
sidecarPath,
|
sidecarPath,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
|
@ -205,7 +205,7 @@ describe(MediaService.name, () => {
|
|||||||
assetMock.getByIds.mockResolvedValue([]);
|
assetMock.getByIds.mockResolvedValue([]);
|
||||||
await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id });
|
await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id });
|
||||||
expect(mediaMock.resize).not.toHaveBeenCalled();
|
expect(mediaMock.resize).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalledWith();
|
expect(assetMock.update).not.toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip video thumbnail generation if no video stream', async () => {
|
it('should skip video thumbnail generation if no video stream', async () => {
|
||||||
@ -213,7 +213,7 @@ describe(MediaService.name, () => {
|
|||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id });
|
await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id });
|
||||||
expect(mediaMock.resize).not.toHaveBeenCalled();
|
expect(mediaMock.resize).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalledWith();
|
expect(assetMock.update).not.toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate a thumbnail for an image', async () => {
|
it('should generate a thumbnail for an image', async () => {
|
||||||
@ -227,7 +227,7 @@ describe(MediaService.name, () => {
|
|||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.SRGB,
|
colorspace: Colorspace.SRGB,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
||||||
});
|
});
|
||||||
@ -246,7 +246,7 @@ describe(MediaService.name, () => {
|
|||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
||||||
});
|
});
|
||||||
@ -271,7 +271,7 @@ describe(MediaService.name, () => {
|
|||||||
twoPass: false,
|
twoPass: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
||||||
});
|
});
|
||||||
@ -296,7 +296,7 @@ describe(MediaService.name, () => {
|
|||||||
twoPass: false,
|
twoPass: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
||||||
});
|
});
|
||||||
@ -337,7 +337,7 @@ describe(MediaService.name, () => {
|
|||||||
assetMock.getByIds.mockResolvedValue([]);
|
assetMock.getByIds.mockResolvedValue([]);
|
||||||
await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id });
|
await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id });
|
||||||
expect(mediaMock.resize).not.toHaveBeenCalled();
|
expect(mediaMock.resize).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalledWith();
|
expect(assetMock.update).not.toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate a thumbnail', async () => {
|
it('should generate a thumbnail', async () => {
|
||||||
@ -350,7 +350,7 @@ describe(MediaService.name, () => {
|
|||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.SRGB,
|
colorspace: Colorspace.SRGB,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp',
|
webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp',
|
||||||
});
|
});
|
||||||
@ -370,7 +370,7 @@ describe(MediaService.name, () => {
|
|||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp',
|
webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp',
|
||||||
});
|
});
|
||||||
@ -397,7 +397,7 @@ describe(MediaService.name, () => {
|
|||||||
await sut.handleGenerateThumbhashThumbnail({ id: assetStub.image.id });
|
await sut.handleGenerateThumbhashThumbnail({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(mediaMock.generateThumbhash).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg');
|
expect(mediaMock.generateThumbhash).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg');
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });
|
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -172,7 +172,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resizePath = await this.generateThumbnail(asset, 'jpeg');
|
const resizePath = await this.generateThumbnail(asset, 'jpeg');
|
||||||
await this.assetRepository.save({ id: asset.id, resizePath });
|
await this.assetRepository.update({ id: asset.id, resizePath });
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const webpPath = await this.generateThumbnail(asset, 'webp');
|
const webpPath = await this.generateThumbnail(asset, 'webp');
|
||||||
await this.assetRepository.save({ id: asset.id, webpPath });
|
await this.assetRepository.update({ id: asset.id, webpPath });
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,7 +233,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const thumbhash = await this.mediaRepository.generateThumbhash(asset.resizePath);
|
const thumbhash = await this.mediaRepository.generateThumbhash(asset.resizePath);
|
||||||
await this.assetRepository.save({ id: asset.id, thumbhash });
|
await this.assetRepository.update({ id: asset.id, thumbhash });
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
@ -286,7 +286,7 @@ export class MediaService {
|
|||||||
if (asset.encodedVideoPath) {
|
if (asset.encodedVideoPath) {
|
||||||
this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
|
this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
|
||||||
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } });
|
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } });
|
||||||
await this.assetRepository.save({ id: asset.id, encodedVideoPath: null });
|
await this.assetRepository.update({ id: asset.id, encodedVideoPath: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@ -321,7 +321,7 @@ export class MediaService {
|
|||||||
|
|
||||||
this.logger.log(`Successfully encoded ${asset.id}`);
|
this.logger.log(`Successfully encoded ${asset.id}`);
|
||||||
|
|
||||||
await this.assetRepository.save({ id: asset.id, encodedVideoPath: output });
|
await this.assetRepository.update({ id: asset.id, encodedVideoPath: output });
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ describe(MetadataService.name, () => {
|
|||||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||||
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
expect(albumMock.removeAsset).not.toHaveBeenCalled();
|
expect(albumMock.removeAsset).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ describe(MetadataService.name, () => {
|
|||||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||||
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
expect(albumMock.removeAsset).not.toHaveBeenCalled();
|
expect(albumMock.removeAsset).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ describe(MetadataService.name, () => {
|
|||||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED);
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED);
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||||
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
expect(albumMock.removeAsset).not.toHaveBeenCalled();
|
expect(albumMock.removeAsset).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -159,7 +159,7 @@ describe(MetadataService.name, () => {
|
|||||||
otherAssetId: assetStub.livePhotoMotionAsset.id,
|
otherAssetId: assetStub.livePhotoMotionAsset.id,
|
||||||
type: AssetType.IMAGE,
|
type: AssetType.IMAGE,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
expect(albumMock.removeAsset).not.toHaveBeenCalled();
|
expect(albumMock.removeAsset).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -182,11 +182,11 @@ describe(MetadataService.name, () => {
|
|||||||
otherAssetId: assetStub.livePhotoStillAsset.id,
|
otherAssetId: assetStub.livePhotoStillAsset.id,
|
||||||
type: AssetType.VIDEO,
|
type: AssetType.VIDEO,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
|
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
|
||||||
expect(albumMock.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
expect(albumMock.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -248,7 +248,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
expect(assetMock.upsertExif).not.toHaveBeenCalled();
|
expect(assetMock.upsertExif).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a date in a sidecar file', async () => {
|
it('should handle a date in a sidecar file', async () => {
|
||||||
@ -267,7 +267,7 @@ describe(MetadataService.name, () => {
|
|||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id]);
|
||||||
expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }));
|
expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }));
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: sidecarDate,
|
fileCreatedAt: sidecarDate,
|
||||||
@ -282,7 +282,7 @@ describe(MetadataService.name, () => {
|
|||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }));
|
expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }));
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: assetStub.image.createdAt,
|
fileCreatedAt: assetStub.image.createdAt,
|
||||||
@ -304,7 +304,7 @@ describe(MetadataService.name, () => {
|
|||||||
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }),
|
expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }),
|
||||||
);
|
);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.withLocation.id,
|
id: assetStub.withLocation.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: assetStub.withLocation.createdAt,
|
fileCreatedAt: assetStub.withLocation.createdAt,
|
||||||
@ -333,7 +333,7 @@ describe(MetadataService.name, () => {
|
|||||||
expect(storageMock.writeFile).not.toHaveBeenCalled();
|
expect(storageMock.writeFile).not.toHaveBeenCalled();
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalledWith(
|
expect(assetMock.update).not.toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ assetType: AssetType.VIDEO, isVisible: false }),
|
expect.objectContaining({ assetType: AssetType.VIDEO, isVisible: false }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -376,7 +376,7 @@ describe(MetadataService.name, () => {
|
|||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
||||||
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||||
expect(assetMock.save).toHaveBeenNthCalledWith(1, {
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
||||||
});
|
});
|
||||||
@ -404,7 +404,7 @@ describe(MetadataService.name, () => {
|
|||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
||||||
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||||
expect(assetMock.save).toHaveBeenNthCalledWith(1, {
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
||||||
});
|
});
|
||||||
@ -430,7 +430,7 @@ describe(MetadataService.name, () => {
|
|||||||
expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object));
|
expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object));
|
||||||
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||||
expect(assetMock.save).toHaveBeenNthCalledWith(1, {
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
||||||
});
|
});
|
||||||
@ -470,7 +470,7 @@ describe(MetadataService.name, () => {
|
|||||||
expect(assetMock.create).toHaveBeenCalledTimes(0);
|
expect(assetMock.create).toHaveBeenCalledTimes(0);
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledTimes(0);
|
expect(storageMock.writeFile).toHaveBeenCalledTimes(0);
|
||||||
// The still asset gets saved by handleMetadataExtraction, but not the video
|
// The still asset gets saved by handleMetadataExtraction, but not the video
|
||||||
expect(assetMock.save).toHaveBeenCalledTimes(1);
|
expect(assetMock.update).toHaveBeenCalledTimes(1);
|
||||||
expect(jobMock.queue).toHaveBeenCalledTimes(0);
|
expect(jobMock.queue).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -529,7 +529,7 @@ describe(MetadataService.name, () => {
|
|||||||
projectionType: 'EQUIRECTANGULAR',
|
projectionType: 'EQUIRECTANGULAR',
|
||||||
timeZone: tags.tz,
|
timeZone: tags.tz,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: new Date('1970-01-01'),
|
fileCreatedAt: new Date('1970-01-01'),
|
||||||
@ -545,7 +545,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
expect(assetMock.upsertExif).toHaveBeenCalled();
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
||||||
expect(assetMock.save).toHaveBeenCalledWith(
|
expect(assetMock.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
duration: '00:00:06.210',
|
duration: '00:00:06.210',
|
||||||
@ -561,7 +561,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
expect(assetMock.upsertExif).toHaveBeenCalled();
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
||||||
expect(assetMock.save).toHaveBeenCalledWith(
|
expect(assetMock.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
duration: '00:00:08.410',
|
duration: '00:00:08.410',
|
||||||
@ -577,7 +577,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
expect(assetMock.upsertExif).toHaveBeenCalled();
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
||||||
expect(assetMock.save).toHaveBeenCalledWith(
|
expect(assetMock.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
duration: '00:00:06.200',
|
duration: '00:00:06.200',
|
||||||
@ -593,7 +593,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
expect(assetMock.upsertExif).toHaveBeenCalled();
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
||||||
expect(assetMock.save).toHaveBeenCalledWith(
|
expect(assetMock.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
duration: '00:00:06.207',
|
duration: '00:00:06.207',
|
||||||
@ -638,13 +638,13 @@ describe(MetadataService.name, () => {
|
|||||||
it('should do nothing if asset could not be found', async () => {
|
it('should do nothing if asset could not be found', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([]);
|
assetMock.getByIds.mockResolvedValue([]);
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing if asset has no sidecar path', async () => {
|
it('should do nothing if asset has no sidecar path', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set sidecar path if exists (sidecar named photo.ext.xmp)', async () => {
|
it('should set sidecar path if exists (sidecar named photo.ext.xmp)', async () => {
|
||||||
@ -653,7 +653,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK);
|
expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.sidecar.id,
|
id: assetStub.sidecar.id,
|
||||||
sidecarPath: assetStub.sidecar.sidecarPath,
|
sidecarPath: assetStub.sidecar.sidecarPath,
|
||||||
});
|
});
|
||||||
@ -670,7 +670,7 @@ describe(MetadataService.name, () => {
|
|||||||
assetStub.sidecarWithoutExt.sidecarPath,
|
assetStub.sidecarWithoutExt.sidecarPath,
|
||||||
constants.R_OK,
|
constants.R_OK,
|
||||||
);
|
);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.sidecarWithoutExt.id,
|
id: assetStub.sidecarWithoutExt.id,
|
||||||
sidecarPath: assetStub.sidecarWithoutExt.sidecarPath,
|
sidecarPath: assetStub.sidecarWithoutExt.sidecarPath,
|
||||||
});
|
});
|
||||||
@ -688,7 +688,7 @@ describe(MetadataService.name, () => {
|
|||||||
assetStub.sidecarWithoutExt.sidecarPath,
|
assetStub.sidecarWithoutExt.sidecarPath,
|
||||||
constants.R_OK,
|
constants.R_OK,
|
||||||
);
|
);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.sidecar.id,
|
id: assetStub.sidecar.id,
|
||||||
sidecarPath: assetStub.sidecar.sidecarPath,
|
sidecarPath: assetStub.sidecar.sidecarPath,
|
||||||
});
|
});
|
||||||
@ -700,7 +700,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK);
|
expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.sidecar.id,
|
id: assetStub.sidecar.id,
|
||||||
sidecarPath: null,
|
sidecarPath: null,
|
||||||
});
|
});
|
||||||
@ -724,16 +724,15 @@ describe(MetadataService.name, () => {
|
|||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
storageMock.checkFileExists.mockResolvedValue(false);
|
storageMock.checkFileExists.mockResolvedValue(false);
|
||||||
await sut.handleSidecarDiscovery({ id: assetStub.image.id });
|
await sut.handleSidecarDiscovery({ id: assetStub.image.id });
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update a image asset when a sidecar is found', async () => {
|
it('should update a image asset when a sidecar is found', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
assetMock.save.mockResolvedValue(assetStub.image);
|
|
||||||
storageMock.checkFileExists.mockResolvedValue(true);
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
await sut.handleSidecarDiscovery({ id: assetStub.image.id });
|
await sut.handleSidecarDiscovery({ id: assetStub.image.id });
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledWith('/original/path.jpg.xmp', constants.R_OK);
|
expect(storageMock.checkFileExists).toHaveBeenCalledWith('/original/path.jpg.xmp', constants.R_OK);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
sidecarPath: '/original/path.jpg.xmp',
|
sidecarPath: '/original/path.jpg.xmp',
|
||||||
});
|
});
|
||||||
@ -741,11 +740,10 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should update a video asset when a sidecar is found', async () => {
|
it('should update a video asset when a sidecar is found', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
assetMock.save.mockResolvedValue(assetStub.video);
|
|
||||||
storageMock.checkFileExists.mockResolvedValue(true);
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
await sut.handleSidecarDiscovery({ id: assetStub.video.id });
|
await sut.handleSidecarDiscovery({ id: assetStub.video.id });
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledWith('/original/path.ext.xmp', constants.R_OK);
|
expect(storageMock.checkFileExists).toHaveBeenCalledWith('/original/path.ext.xmp', constants.R_OK);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
sidecarPath: '/original/path.ext.xmp',
|
sidecarPath: '/original/path.ext.xmp',
|
||||||
});
|
});
|
||||||
|
@ -177,8 +177,8 @@ export class MetadataService {
|
|||||||
|
|
||||||
const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset];
|
const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset];
|
||||||
|
|
||||||
await this.assetRepository.save({ id: photoAsset.id, livePhotoVideoId: motionAsset.id });
|
await this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id });
|
||||||
await this.assetRepository.save({ id: motionAsset.id, isVisible: false });
|
await this.assetRepository.update({ id: motionAsset.id, isVisible: false });
|
||||||
await this.albumRepository.removeAsset(motionAsset.id);
|
await this.albumRepository.removeAsset(motionAsset.id);
|
||||||
|
|
||||||
// Notify clients to hide the linked live photo asset
|
// Notify clients to hide the linked live photo asset
|
||||||
@ -249,7 +249,7 @@ export class MetadataService {
|
|||||||
if (dateTimeOriginal && timeZoneOffset) {
|
if (dateTimeOriginal && timeZoneOffset) {
|
||||||
localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60_000);
|
localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60_000);
|
||||||
}
|
}
|
||||||
await this.assetRepository.save({
|
await this.assetRepository.update({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
duration: tags.Duration ? this.getDuration(tags.Duration) : null,
|
duration: tags.Duration ? this.getDuration(tags.Duration) : null,
|
||||||
localDateTime,
|
localDateTime,
|
||||||
@ -317,7 +317,7 @@ export class MetadataService {
|
|||||||
await this.repository.writeTags(sidecarPath, exif);
|
await this.repository.writeTags(sidecarPath, exif);
|
||||||
|
|
||||||
if (!asset.sidecarPath) {
|
if (!asset.sidecarPath) {
|
||||||
await this.assetRepository.save({ id, sidecarPath });
|
await this.assetRepository.update({ id, sidecarPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
@ -435,7 +435,7 @@ export class MetadataService {
|
|||||||
this.storageCore.ensureFolders(motionPath);
|
this.storageCore.ensureFolders(motionPath);
|
||||||
await this.storageRepository.writeFile(motionAsset.originalPath, video);
|
await this.storageRepository.writeFile(motionAsset.originalPath, video);
|
||||||
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: motionAsset.id } });
|
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: motionAsset.id } });
|
||||||
await this.assetRepository.save({ id: asset.id, livePhotoVideoId: motionAsset.id });
|
await this.assetRepository.update({ id: asset.id, livePhotoVideoId: motionAsset.id });
|
||||||
|
|
||||||
// If the asset already had an associated livePhotoVideo, delete it, because
|
// If the asset already had an associated livePhotoVideo, delete it, because
|
||||||
// its checksum doesn't match the checksum of the motionAsset we just extracted
|
// its checksum doesn't match the checksum of the motionAsset we just extracted
|
||||||
@ -587,7 +587,7 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sidecarPath) {
|
if (sidecarPath) {
|
||||||
await this.assetRepository.save({ id: asset.id, sidecarPath });
|
await this.assetRepository.update({ id: asset.id, sidecarPath });
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,7 +598,7 @@ export class MetadataService {
|
|||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Sidecar file was not found. Checked paths '${sidecarPathWithExt}' and '${sidecarPathWithoutExt}'. Removing sidecarPath for asset ${asset.id}`,
|
`Sidecar file was not found. Checked paths '${sidecarPathWithExt}' and '${sidecarPathWithoutExt}'. Removing sidecarPath for asset ${asset.id}`,
|
||||||
);
|
);
|
||||||
await this.assetRepository.save({ id: asset.id, sidecarPath: null });
|
await this.assetRepository.update({ id: asset.id, sidecarPath: null });
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
@ -91,6 +91,25 @@ export type AssetCreate = Pick<
|
|||||||
> &
|
> &
|
||||||
Partial<AssetEntity>;
|
Partial<AssetEntity>;
|
||||||
|
|
||||||
|
export type AssetWithoutRelations = Omit<
|
||||||
|
AssetEntity,
|
||||||
|
| 'livePhotoVideo'
|
||||||
|
| 'stack'
|
||||||
|
| 'albums'
|
||||||
|
| 'faces'
|
||||||
|
| 'owner'
|
||||||
|
| 'library'
|
||||||
|
| 'exifInfo'
|
||||||
|
| 'sharedLinks'
|
||||||
|
| 'smartInfo'
|
||||||
|
| 'smartSearch'
|
||||||
|
| 'tags'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type AssetUpdateOptions = Pick<AssetWithoutRelations, 'id'> & Partial<AssetWithoutRelations>;
|
||||||
|
|
||||||
|
export type AssetUpdateAllOptions = Omit<Partial<AssetWithoutRelations>, 'id'>;
|
||||||
|
|
||||||
export interface MonthDay {
|
export interface MonthDay {
|
||||||
day: number;
|
day: number;
|
||||||
month: number;
|
month: number;
|
||||||
@ -139,8 +158,8 @@ export interface IAssetRepository {
|
|||||||
deleteAll(ownerId: string): Promise<void>;
|
deleteAll(ownerId: string): Promise<void>;
|
||||||
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||||
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
||||||
updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void>;
|
updateAll(ids: string[], options: Partial<AssetUpdateAllOptions>): Promise<void>;
|
||||||
save(asset: Pick<AssetEntity, 'id'> & Partial<AssetEntity>): Promise<AssetEntity>;
|
update(asset: AssetUpdateOptions): Promise<void>;
|
||||||
remove(asset: AssetEntity): Promise<void>;
|
remove(asset: AssetEntity): Promise<void>;
|
||||||
softDeleteAll(ids: string[]): Promise<void>;
|
softDeleteAll(ids: string[]): Promise<void>;
|
||||||
restoreAll(ids: string[]): Promise<void>;
|
restoreAll(ids: string[]): Promise<void>;
|
||||||
|
@ -187,5 +187,6 @@ export interface ISearchRepository {
|
|||||||
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
||||||
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
|
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
|
||||||
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
||||||
|
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]>;
|
||||||
deleteAllSearchEmbeddings(): Promise<void>;
|
deleteAllSearchEmbeddings(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -115,6 +115,32 @@ export class SearchService {
|
|||||||
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null);
|
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAssetsByCity(auth: AuthDto): Promise<AssetResponseDto[]> {
|
||||||
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
|
const assets = await this.searchRepository.getAssetsByCity(userIds);
|
||||||
|
return assets.map((asset) => mapAsset(asset));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise<string[]> {
|
||||||
|
switch (dto.type) {
|
||||||
|
case SearchSuggestionType.COUNTRY: {
|
||||||
|
return this.metadataRepository.getCountries(auth.user.id);
|
||||||
|
}
|
||||||
|
case SearchSuggestionType.STATE: {
|
||||||
|
return this.metadataRepository.getStates(auth.user.id, dto.country);
|
||||||
|
}
|
||||||
|
case SearchSuggestionType.CITY: {
|
||||||
|
return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state);
|
||||||
|
}
|
||||||
|
case SearchSuggestionType.CAMERA_MAKE: {
|
||||||
|
return this.metadataRepository.getCameraMakes(auth.user.id, dto.model);
|
||||||
|
}
|
||||||
|
case SearchSuggestionType.CAMERA_MODEL: {
|
||||||
|
return this.metadataRepository.getCameraModels(auth.user.id, dto.make);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove after implementing new search filters
|
// TODO: remove after implementing new search filters
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> {
|
async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> {
|
||||||
@ -191,24 +217,4 @@ export class SearchService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise<string[]> {
|
|
||||||
switch (dto.type) {
|
|
||||||
case SearchSuggestionType.COUNTRY: {
|
|
||||||
return this.metadataRepository.getCountries(auth.user.id);
|
|
||||||
}
|
|
||||||
case SearchSuggestionType.STATE: {
|
|
||||||
return this.metadataRepository.getStates(auth.user.id, dto.country);
|
|
||||||
}
|
|
||||||
case SearchSuggestionType.CITY: {
|
|
||||||
return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state);
|
|
||||||
}
|
|
||||||
case SearchSuggestionType.CAMERA_MAKE: {
|
|
||||||
return this.metadataRepository.getCameraMakes(auth.user.id, dto.model);
|
|
||||||
}
|
|
||||||
case SearchSuggestionType.CAMERA_MODEL: {
|
|
||||||
return this.metadataRepository.getCameraModels(auth.user.id, dto.make);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
expect(storageMock.checkFileExists).not.toHaveBeenCalled();
|
expect(storageMock.checkFileExists).not.toHaveBeenCalled();
|
||||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||||
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
expect(moveMock.create).not.toHaveBeenCalled();
|
expect(moveMock.create).not.toHaveBeenCalled();
|
||||||
expect(moveMock.update).not.toHaveBeenCalled();
|
expect(moveMock.update).not.toHaveBeenCalled();
|
||||||
expect(storageMock.stat).not.toHaveBeenCalled();
|
expect(storageMock.stat).not.toHaveBeenCalled();
|
||||||
@ -122,14 +122,6 @@ describe(StorageTemplateService.name, () => {
|
|||||||
const newMotionPicturePath = `upload/library/${userStub.user1.id}/2022/2022-06-19/${assetStub.livePhotoStillAsset.id}.mp4`;
|
const newMotionPicturePath = `upload/library/${userStub.user1.id}/2022/2022-06-19/${assetStub.livePhotoStillAsset.id}.mp4`;
|
||||||
const newStillPicturePath = `upload/library/${userStub.user1.id}/2022/2022-06-19/${assetStub.livePhotoStillAsset.id}.jpeg`;
|
const newStillPicturePath = `upload/library/${userStub.user1.id}/2022/2022-06-19/${assetStub.livePhotoStillAsset.id}.jpeg`;
|
||||||
|
|
||||||
when(assetMock.save)
|
|
||||||
.calledWith({ id: assetStub.livePhotoStillAsset.id, originalPath: newStillPicturePath })
|
|
||||||
.mockResolvedValue(assetStub.livePhotoStillAsset);
|
|
||||||
|
|
||||||
when(assetMock.save)
|
|
||||||
.calledWith({ id: assetStub.livePhotoMotionAsset.id, originalPath: newMotionPicturePath })
|
|
||||||
.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
||||||
|
|
||||||
when(assetMock.getByIds)
|
when(assetMock.getByIds)
|
||||||
.calledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true })
|
.calledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true })
|
||||||
.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
||||||
@ -175,11 +167,11 @@ describe(StorageTemplateService.name, () => {
|
|||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true });
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(2);
|
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(2);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
originalPath: newStillPicturePath,
|
originalPath: newStillPicturePath,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.livePhotoMotionAsset.id,
|
id: assetStub.livePhotoMotionAsset.id,
|
||||||
originalPath: newMotionPicturePath,
|
originalPath: newMotionPicturePath,
|
||||||
});
|
});
|
||||||
@ -200,10 +192,6 @@ describe(StorageTemplateService.name, () => {
|
|||||||
newPath: previousFailedNewPath,
|
newPath: previousFailedNewPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
when(assetMock.save)
|
|
||||||
.calledWith({ id: assetStub.image.id, originalPath: newPath })
|
|
||||||
.mockResolvedValue(assetStub.image);
|
|
||||||
|
|
||||||
when(assetMock.getByIds)
|
when(assetMock.getByIds)
|
||||||
.calledWith([assetStub.image.id], { exifInfo: true })
|
.calledWith([assetStub.image.id], { exifInfo: true })
|
||||||
.mockResolvedValue([assetStub.image]);
|
.mockResolvedValue([assetStub.image]);
|
||||||
@ -232,7 +220,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
oldPath: assetStub.image.originalPath,
|
oldPath: assetStub.image.originalPath,
|
||||||
newPath,
|
newPath,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
originalPath: newPath,
|
originalPath: newPath,
|
||||||
});
|
});
|
||||||
@ -257,10 +245,6 @@ describe(StorageTemplateService.name, () => {
|
|||||||
newPath: previousFailedNewPath,
|
newPath: previousFailedNewPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
when(assetMock.save)
|
|
||||||
.calledWith({ id: assetStub.image.id, originalPath: newPath })
|
|
||||||
.mockResolvedValue(assetStub.image);
|
|
||||||
|
|
||||||
when(assetMock.getByIds)
|
when(assetMock.getByIds)
|
||||||
.calledWith([assetStub.image.id], { exifInfo: true })
|
.calledWith([assetStub.image.id], { exifInfo: true })
|
||||||
.mockResolvedValue([assetStub.image]);
|
.mockResolvedValue([assetStub.image]);
|
||||||
@ -291,7 +275,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
oldPath: previousFailedNewPath,
|
oldPath: previousFailedNewPath,
|
||||||
newPath,
|
newPath,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
originalPath: newPath,
|
originalPath: newPath,
|
||||||
});
|
});
|
||||||
@ -307,10 +291,6 @@ describe(StorageTemplateService.name, () => {
|
|||||||
.mockResolvedValue({ size: 5000 } as Stats);
|
.mockResolvedValue({ size: 5000 } as Stats);
|
||||||
when(cryptoMock.hashFile).calledWith(newPath).mockResolvedValue(Buffer.from('different-hash', 'utf8'));
|
when(cryptoMock.hashFile).calledWith(newPath).mockResolvedValue(Buffer.from('different-hash', 'utf8'));
|
||||||
|
|
||||||
when(assetMock.save)
|
|
||||||
.calledWith({ id: assetStub.image.id, originalPath: newPath })
|
|
||||||
.mockResolvedValue(assetStub.image);
|
|
||||||
|
|
||||||
when(assetMock.getByIds)
|
when(assetMock.getByIds)
|
||||||
.calledWith([assetStub.image.id], { exifInfo: true })
|
.calledWith([assetStub.image.id], { exifInfo: true })
|
||||||
.mockResolvedValue([assetStub.image]);
|
.mockResolvedValue([assetStub.image]);
|
||||||
@ -345,7 +325,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
expect(storageMock.copyFile).toHaveBeenCalledWith(assetStub.image.originalPath, newPath);
|
expect(storageMock.copyFile).toHaveBeenCalledWith(assetStub.image.originalPath, newPath);
|
||||||
expect(storageMock.unlink).toHaveBeenCalledWith(newPath);
|
expect(storageMock.unlink).toHaveBeenCalledWith(newPath);
|
||||||
expect(storageMock.unlink).toHaveBeenCalledTimes(1);
|
expect(storageMock.unlink).toHaveBeenCalledTimes(1);
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
@ -374,10 +354,6 @@ describe(StorageTemplateService.name, () => {
|
|||||||
newPath: previousFailedNewPath,
|
newPath: previousFailedNewPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
when(assetMock.save)
|
|
||||||
.calledWith({ id: assetStub.image.id, originalPath: newPath })
|
|
||||||
.mockResolvedValue(assetStub.image);
|
|
||||||
|
|
||||||
when(assetMock.getByIds)
|
when(assetMock.getByIds)
|
||||||
.calledWith([assetStub.image.id], { exifInfo: true })
|
.calledWith([assetStub.image.id], { exifInfo: true })
|
||||||
.mockResolvedValue([assetStub.image]);
|
.mockResolvedValue([assetStub.image]);
|
||||||
@ -404,7 +380,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||||
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
||||||
expect(moveMock.update).not.toHaveBeenCalled();
|
expect(moveMock.update).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -427,7 +403,6 @@ describe(StorageTemplateService.name, () => {
|
|||||||
items: [assetStub.image],
|
items: [assetStub.image],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
assetMock.save.mockResolvedValue(assetStub.image);
|
|
||||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||||
moveMock.create.mockResolvedValue({
|
moveMock.create.mockResolvedValue({
|
||||||
id: '123',
|
id: '123',
|
||||||
@ -449,7 +424,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
|
|
||||||
expect(assetMock.getAll).toHaveBeenCalled();
|
expect(assetMock.getAll).toHaveBeenCalled();
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(2);
|
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(2);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg',
|
originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg',
|
||||||
});
|
});
|
||||||
@ -474,7 +449,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||||
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
||||||
expect(storageMock.checkFileExists).not.toHaveBeenCalledTimes(2);
|
expect(storageMock.checkFileExists).not.toHaveBeenCalledTimes(2);
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip when an asset is probably a duplicate', async () => {
|
it('should skip when an asset is probably a duplicate', async () => {
|
||||||
@ -495,7 +470,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||||
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
||||||
expect(storageMock.checkFileExists).not.toHaveBeenCalledTimes(2);
|
expect(storageMock.checkFileExists).not.toHaveBeenCalledTimes(2);
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move an asset', async () => {
|
it('should move an asset', async () => {
|
||||||
@ -503,7 +478,6 @@ describe(StorageTemplateService.name, () => {
|
|||||||
items: [assetStub.image],
|
items: [assetStub.image],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
assetMock.save.mockResolvedValue(assetStub.image);
|
|
||||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||||
moveMock.create.mockResolvedValue({
|
moveMock.create.mockResolvedValue({
|
||||||
id: '123',
|
id: '123',
|
||||||
@ -520,7 +494,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
'/original/path.jpg',
|
'/original/path.jpg',
|
||||||
'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||||
);
|
);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||||
});
|
});
|
||||||
@ -531,7 +505,6 @@ describe(StorageTemplateService.name, () => {
|
|||||||
items: [assetStub.image],
|
items: [assetStub.image],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
assetMock.save.mockResolvedValue(assetStub.image);
|
|
||||||
userMock.getList.mockResolvedValue([userStub.storageLabel]);
|
userMock.getList.mockResolvedValue([userStub.storageLabel]);
|
||||||
moveMock.create.mockResolvedValue({
|
moveMock.create.mockResolvedValue({
|
||||||
id: '123',
|
id: '123',
|
||||||
@ -548,7 +521,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
'/original/path.jpg',
|
'/original/path.jpg',
|
||||||
'upload/library/label-1/2023/2023-02-23/asset-id.jpg',
|
'upload/library/label-1/2023/2023-02-23/asset-id.jpg',
|
||||||
);
|
);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
originalPath: 'upload/library/label-1/2023/2023-02-23/asset-id.jpg',
|
originalPath: 'upload/library/label-1/2023/2023-02-23/asset-id.jpg',
|
||||||
});
|
});
|
||||||
@ -592,7 +565,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
expect(storageMock.utimes).toHaveBeenCalledWith(newPath, expect.any(Date), expect.any(Date));
|
expect(storageMock.utimes).toHaveBeenCalledWith(newPath, expect.any(Date), expect.any(Date));
|
||||||
expect(storageMock.unlink).toHaveBeenCalledWith(assetStub.image.originalPath);
|
expect(storageMock.unlink).toHaveBeenCalledWith(assetStub.image.originalPath);
|
||||||
expect(storageMock.unlink).toHaveBeenCalledTimes(1);
|
expect(storageMock.unlink).toHaveBeenCalledTimes(1);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
originalPath: newPath,
|
originalPath: newPath,
|
||||||
});
|
});
|
||||||
@ -630,7 +603,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||||
);
|
);
|
||||||
expect(storageMock.stat).toHaveBeenCalledWith('upload/library/user-id/2023/2023-02-23/asset-id.jpg');
|
expect(storageMock.stat).toHaveBeenCalledWith('upload/library/user-id/2023/2023-02-23/asset-id.jpg');
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not update the database if the move fails', async () => {
|
it('should not update the database if the move fails', async () => {
|
||||||
@ -656,7 +629,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
'/original/path.jpg',
|
'/original/path.jpg',
|
||||||
'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||||
);
|
);
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not move read-only asset', async () => {
|
it('should not move read-only asset', async () => {
|
||||||
@ -670,7 +643,6 @@ describe(StorageTemplateService.name, () => {
|
|||||||
],
|
],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
assetMock.save.mockResolvedValue(assetStub.image);
|
|
||||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||||
|
|
||||||
await sut.handleMigration();
|
await sut.handleMigration();
|
||||||
@ -678,7 +650,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
expect(assetMock.getAll).toHaveBeenCalled();
|
expect(assetMock.getAll).toHaveBeenCalled();
|
||||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||||
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -286,19 +286,19 @@ export class StorageCore {
|
|||||||
private savePath(pathType: PathType, id: string, newPath: string) {
|
private savePath(pathType: PathType, id: string, newPath: string) {
|
||||||
switch (pathType) {
|
switch (pathType) {
|
||||||
case AssetPathType.ORIGINAL: {
|
case AssetPathType.ORIGINAL: {
|
||||||
return this.assetRepository.save({ id, originalPath: newPath });
|
return this.assetRepository.update({ id, originalPath: newPath });
|
||||||
}
|
}
|
||||||
case AssetPathType.JPEG_THUMBNAIL: {
|
case AssetPathType.JPEG_THUMBNAIL: {
|
||||||
return this.assetRepository.save({ id, resizePath: newPath });
|
return this.assetRepository.update({ id, resizePath: newPath });
|
||||||
}
|
}
|
||||||
case AssetPathType.WEBP_THUMBNAIL: {
|
case AssetPathType.WEBP_THUMBNAIL: {
|
||||||
return this.assetRepository.save({ id, webpPath: newPath });
|
return this.assetRepository.update({ id, webpPath: newPath });
|
||||||
}
|
}
|
||||||
case AssetPathType.ENCODED_VIDEO: {
|
case AssetPathType.ENCODED_VIDEO: {
|
||||||
return this.assetRepository.save({ id, encodedVideoPath: newPath });
|
return this.assetRepository.update({ id, encodedVideoPath: newPath });
|
||||||
}
|
}
|
||||||
case AssetPathType.SIDECAR: {
|
case AssetPathType.SIDECAR: {
|
||||||
return this.assetRepository.save({ id, sidecarPath: newPath });
|
return this.assetRepository.update({ id, sidecarPath: newPath });
|
||||||
}
|
}
|
||||||
case PersonPathType.FACE: {
|
case PersonPathType.FACE: {
|
||||||
return this.personRepository.update({ id, thumbnailPath: newPath });
|
return this.personRepository.update({ id, thumbnailPath: newPath });
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
AssetResponseDto,
|
||||||
AuthDto,
|
AuthDto,
|
||||||
MetadataSearchDto,
|
MetadataSearchDto,
|
||||||
PersonResponseDto,
|
PersonResponseDto,
|
||||||
@ -55,6 +56,11 @@ export class SearchController {
|
|||||||
return this.service.searchPlaces(dto);
|
return this.service.searchPlaces(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('cities')
|
||||||
|
getAssetsByCity(@Auth() auth: AuthDto): Promise<AssetResponseDto[]> {
|
||||||
|
return this.service.getAssetsByCity(auth);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('suggestions')
|
@Get('suggestions')
|
||||||
getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise<string[]> {
|
getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise<string[]> {
|
||||||
return this.service.getSearchSuggestions(auth, dto);
|
return this.service.getSearchSuggestions(auth, dto);
|
||||||
|
@ -6,6 +6,8 @@ import {
|
|||||||
AssetSearchOptions,
|
AssetSearchOptions,
|
||||||
AssetStats,
|
AssetStats,
|
||||||
AssetStatsOptions,
|
AssetStatsOptions,
|
||||||
|
AssetUpdateAllOptions,
|
||||||
|
AssetUpdateOptions,
|
||||||
IAssetRepository,
|
IAssetRepository,
|
||||||
LivePhotoSearchOptions,
|
LivePhotoSearchOptions,
|
||||||
MapMarker,
|
MapMarker,
|
||||||
@ -275,7 +277,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] })
|
@GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] })
|
||||||
@Chunked()
|
@Chunked()
|
||||||
async updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void> {
|
async updateAll(ids: string[], options: AssetUpdateAllOptions): Promise<void> {
|
||||||
await this.repository.update({ id: In(ids) }, options);
|
await this.repository.update({ id: In(ids) }, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,21 +291,8 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
await this.repository.restore({ id: In(ids) });
|
await this.repository.restore({ id: In(ids) });
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(asset: Partial<AssetEntity>): Promise<AssetEntity> {
|
async update(asset: AssetUpdateOptions): Promise<void> {
|
||||||
const { id } = await this.repository.save(asset);
|
await this.repository.update(asset.id, asset);
|
||||||
return this.repository.findOneOrFail({
|
|
||||||
where: { id },
|
|
||||||
relations: {
|
|
||||||
exifInfo: true,
|
|
||||||
owner: true,
|
|
||||||
smartInfo: true,
|
|
||||||
tags: true,
|
|
||||||
faces: {
|
|
||||||
person: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
withDeleted: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(asset: AssetEntity): Promise<void> {
|
async remove(asset: AssetEntity): Promise<void> {
|
||||||
|
@ -15,6 +15,7 @@ import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant';
|
|||||||
import {
|
import {
|
||||||
AssetEntity,
|
AssetEntity,
|
||||||
AssetFaceEntity,
|
AssetFaceEntity,
|
||||||
|
AssetType,
|
||||||
GeodataPlacesEntity,
|
GeodataPlacesEntity,
|
||||||
SmartInfoEntity,
|
SmartInfoEntity,
|
||||||
SmartSearchEntity,
|
SmartSearchEntity,
|
||||||
@ -33,6 +34,7 @@ import { Instrumentation } from '../instrumentation';
|
|||||||
export class SearchRepository implements ISearchRepository {
|
export class SearchRepository implements ISearchRepository {
|
||||||
private logger = new ImmichLogger(SearchRepository.name);
|
private logger = new ImmichLogger(SearchRepository.name);
|
||||||
private faceColumns: string[];
|
private faceColumns: string[];
|
||||||
|
private assetsByCityQuery: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SmartInfoEntity) private repository: Repository<SmartInfoEntity>,
|
@InjectRepository(SmartInfoEntity) private repository: Repository<SmartInfoEntity>,
|
||||||
@ -45,6 +47,14 @@ export class SearchRepository implements ISearchRepository {
|
|||||||
.getMetadata(AssetFaceEntity)
|
.getMetadata(AssetFaceEntity)
|
||||||
.ownColumns.map((column) => column.propertyName)
|
.ownColumns.map((column) => column.propertyName)
|
||||||
.filter((propertyName) => propertyName !== 'embedding');
|
.filter((propertyName) => propertyName !== 'embedding');
|
||||||
|
this.assetsByCityQuery =
|
||||||
|
assetsByCityCte +
|
||||||
|
this.assetRepository
|
||||||
|
.createQueryBuilder('asset')
|
||||||
|
.innerJoinAndSelect('asset.exifInfo', 'exif')
|
||||||
|
.withDeleted()
|
||||||
|
.getQuery() +
|
||||||
|
' INNER JOIN cte ON asset.id = cte."assetId"';
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(modelName: string): Promise<void> {
|
async init(modelName: string): Promise<void> {
|
||||||
@ -220,6 +230,27 @@ export class SearchRepository implements ISearchRepository {
|
|||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
|
async getAssetsByCity(userIds: string[]): Promise<AssetEntity[]> {
|
||||||
|
const parameters = [userIds.join(', '), true, false, AssetType.IMAGE];
|
||||||
|
const rawRes = await this.repository.query(this.assetsByCityQuery, parameters);
|
||||||
|
|
||||||
|
const items: AssetEntity[] = [];
|
||||||
|
for (const res of rawRes) {
|
||||||
|
const item = { exifInfo: {} as Record<string, any> } as Record<string, any>;
|
||||||
|
for (const [key, value] of Object.entries(res)) {
|
||||||
|
if (key.startsWith('exif_')) {
|
||||||
|
item.exifInfo[key.replace('exif_', '')] = value;
|
||||||
|
} else {
|
||||||
|
item[key.replace('asset_', '')] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.push(item as AssetEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
async upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void> {
|
async upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void> {
|
||||||
await this.repository.upsert(smartInfo, { conflictPaths: ['assetId'] });
|
await this.repository.upsert(smartInfo, { conflictPaths: ['assetId'] });
|
||||||
if (!smartInfo.assetId || !embedding) {
|
if (!smartInfo.assetId || !embedding) {
|
||||||
@ -290,3 +321,30 @@ export class SearchRepository implements ISearchRepository {
|
|||||||
return runtimeConfig;
|
return runtimeConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the performance difference between this and the normal way is too huge to ignore, e.g. 3s vs 4ms
|
||||||
|
const assetsByCityCte = `
|
||||||
|
WITH RECURSIVE cte AS (
|
||||||
|
(
|
||||||
|
SELECT city, "assetId"
|
||||||
|
FROM exif
|
||||||
|
INNER JOIN assets ON exif."assetId" = assets.id
|
||||||
|
WHERE "ownerId" IN ($1) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4
|
||||||
|
ORDER BY city
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT l.city, l."assetId"
|
||||||
|
FROM cte c
|
||||||
|
, LATERAL (
|
||||||
|
SELECT city, "assetId"
|
||||||
|
FROM exif
|
||||||
|
INNER JOIN assets ON exif."assetId" = assets.id
|
||||||
|
WHERE city > c.city AND "ownerId" IN ($1) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4
|
||||||
|
ORDER BY city
|
||||||
|
LIMIT 1
|
||||||
|
) l
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
@ -266,3 +266,111 @@ ORDER BY
|
|||||||
) ASC
|
) ASC
|
||||||
LIMIT
|
LIMIT
|
||||||
20
|
20
|
||||||
|
|
||||||
|
-- SearchRepository.getAssetsByCity
|
||||||
|
WITH RECURSIVE
|
||||||
|
cte AS (
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
city,
|
||||||
|
"assetId"
|
||||||
|
FROM
|
||||||
|
exif
|
||||||
|
INNER JOIN assets ON exif."assetId" = assets.id
|
||||||
|
WHERE
|
||||||
|
"ownerId" IN ($1)
|
||||||
|
AND "isVisible" = $2
|
||||||
|
AND "isArchived" = $3
|
||||||
|
AND type = $4
|
||||||
|
ORDER BY
|
||||||
|
city
|
||||||
|
LIMIT
|
||||||
|
1
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
l.city,
|
||||||
|
l."assetId"
|
||||||
|
FROM
|
||||||
|
cte c,
|
||||||
|
LATERAL (
|
||||||
|
SELECT
|
||||||
|
city,
|
||||||
|
"assetId"
|
||||||
|
FROM
|
||||||
|
exif
|
||||||
|
INNER JOIN assets ON exif."assetId" = assets.id
|
||||||
|
WHERE
|
||||||
|
city > c.city
|
||||||
|
AND "ownerId" IN ($1)
|
||||||
|
AND "isVisible" = $2
|
||||||
|
AND "isArchived" = $3
|
||||||
|
AND type = $4
|
||||||
|
ORDER BY
|
||||||
|
city
|
||||||
|
LIMIT
|
||||||
|
1
|
||||||
|
) l
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
"asset"."id" AS "asset_id",
|
||||||
|
"asset"."deviceAssetId" AS "asset_deviceAssetId",
|
||||||
|
"asset"."ownerId" AS "asset_ownerId",
|
||||||
|
"asset"."libraryId" AS "asset_libraryId",
|
||||||
|
"asset"."deviceId" AS "asset_deviceId",
|
||||||
|
"asset"."type" AS "asset_type",
|
||||||
|
"asset"."originalPath" AS "asset_originalPath",
|
||||||
|
"asset"."resizePath" AS "asset_resizePath",
|
||||||
|
"asset"."webpPath" AS "asset_webpPath",
|
||||||
|
"asset"."thumbhash" AS "asset_thumbhash",
|
||||||
|
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||||
|
"asset"."createdAt" AS "asset_createdAt",
|
||||||
|
"asset"."updatedAt" AS "asset_updatedAt",
|
||||||
|
"asset"."deletedAt" AS "asset_deletedAt",
|
||||||
|
"asset"."fileCreatedAt" AS "asset_fileCreatedAt",
|
||||||
|
"asset"."localDateTime" AS "asset_localDateTime",
|
||||||
|
"asset"."fileModifiedAt" AS "asset_fileModifiedAt",
|
||||||
|
"asset"."isFavorite" AS "asset_isFavorite",
|
||||||
|
"asset"."isArchived" AS "asset_isArchived",
|
||||||
|
"asset"."isExternal" AS "asset_isExternal",
|
||||||
|
"asset"."isReadOnly" AS "asset_isReadOnly",
|
||||||
|
"asset"."isOffline" AS "asset_isOffline",
|
||||||
|
"asset"."checksum" AS "asset_checksum",
|
||||||
|
"asset"."duration" AS "asset_duration",
|
||||||
|
"asset"."isVisible" AS "asset_isVisible",
|
||||||
|
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
|
||||||
|
"asset"."originalFileName" AS "asset_originalFileName",
|
||||||
|
"asset"."sidecarPath" AS "asset_sidecarPath",
|
||||||
|
"asset"."stackId" AS "asset_stackId",
|
||||||
|
"exif"."assetId" AS "exif_assetId",
|
||||||
|
"exif"."description" AS "exif_description",
|
||||||
|
"exif"."exifImageWidth" AS "exif_exifImageWidth",
|
||||||
|
"exif"."exifImageHeight" AS "exif_exifImageHeight",
|
||||||
|
"exif"."fileSizeInByte" AS "exif_fileSizeInByte",
|
||||||
|
"exif"."orientation" AS "exif_orientation",
|
||||||
|
"exif"."dateTimeOriginal" AS "exif_dateTimeOriginal",
|
||||||
|
"exif"."modifyDate" AS "exif_modifyDate",
|
||||||
|
"exif"."timeZone" AS "exif_timeZone",
|
||||||
|
"exif"."latitude" AS "exif_latitude",
|
||||||
|
"exif"."longitude" AS "exif_longitude",
|
||||||
|
"exif"."projectionType" AS "exif_projectionType",
|
||||||
|
"exif"."city" AS "exif_city",
|
||||||
|
"exif"."livePhotoCID" AS "exif_livePhotoCID",
|
||||||
|
"exif"."autoStackId" AS "exif_autoStackId",
|
||||||
|
"exif"."state" AS "exif_state",
|
||||||
|
"exif"."country" AS "exif_country",
|
||||||
|
"exif"."make" AS "exif_make",
|
||||||
|
"exif"."model" AS "exif_model",
|
||||||
|
"exif"."lensModel" AS "exif_lensModel",
|
||||||
|
"exif"."fNumber" AS "exif_fNumber",
|
||||||
|
"exif"."focalLength" AS "exif_focalLength",
|
||||||
|
"exif"."iso" AS "exif_iso",
|
||||||
|
"exif"."exposureTime" AS "exif_exposureTime",
|
||||||
|
"exif"."profileDescription" AS "exif_profileDescription",
|
||||||
|
"exif"."colorspace" AS "exif_colorspace",
|
||||||
|
"exif"."bitsPerSample" AS "exif_bitsPerSample",
|
||||||
|
"exif"."fps" AS "exif_fps"
|
||||||
|
FROM
|
||||||
|
"assets" "asset"
|
||||||
|
INNER JOIN "exif" "exif" ON "exif"."assetId" = "asset"."id"
|
||||||
|
INNER JOIN cte ON asset.id = cte."assetId"
|
||||||
|
@ -24,7 +24,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
|
|||||||
getLibraryAssetPaths: jest.fn(),
|
getLibraryAssetPaths: jest.fn(),
|
||||||
getByLibraryIdAndOriginalPath: jest.fn(),
|
getByLibraryIdAndOriginalPath: jest.fn(),
|
||||||
deleteAll: jest.fn(),
|
deleteAll: jest.fn(),
|
||||||
save: jest.fn(),
|
update: jest.fn(),
|
||||||
remove: jest.fn(),
|
remove: jest.fn(),
|
||||||
findLivePhotoMatch: jest.fn(),
|
findLivePhotoMatch: jest.fn(),
|
||||||
getMapMarkers: jest.fn(),
|
getMapMarkers: jest.fn(),
|
||||||
|
@ -8,6 +8,7 @@ export const newSearchRepositoryMock = (): jest.Mocked<ISearchRepository> => {
|
|||||||
searchFaces: jest.fn(),
|
searchFaces: jest.fn(),
|
||||||
upsert: jest.fn(),
|
upsert: jest.fn(),
|
||||||
searchPlaces: jest.fn(),
|
searchPlaces: jest.fn(),
|
||||||
|
getAssetsByCity: jest.fn(),
|
||||||
deleteAllSearchEmbeddings: jest.fn(),
|
deleteAllSearchEmbeddings: jest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
18
web/src/lib/__mocks__/sdk.mock.ts
Normal file
18
web/src/lib/__mocks__/sdk.mock.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import sdk from '@immich/sdk';
|
||||||
|
import type { Mock, MockedObject } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('@immich/sdk', async (originalImport) => {
|
||||||
|
const module = await originalImport<typeof import('@immich/sdk')>();
|
||||||
|
|
||||||
|
const mocks: Record<string, Mock> = {};
|
||||||
|
for (const [key, value] of Object.entries(module)) {
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
mocks[key] = vi.fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = { ...module, ...mocks };
|
||||||
|
return { ...mock, default: mock };
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sdkMock = sdk as MockedObject<typeof sdk>;
|
@ -1,18 +1,11 @@
|
|||||||
import { createObjectURLMock } from '$lib/__mocks__/jsdom-url.mock';
|
import { createObjectURLMock } from '$lib/__mocks__/jsdom-url.mock';
|
||||||
import sdk, { ThumbnailFormat } from '@immich/sdk';
|
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||||
|
import { ThumbnailFormat } from '@immich/sdk';
|
||||||
import { albumFactory } from '@test-data';
|
import { albumFactory } from '@test-data';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte';
|
import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte';
|
||||||
import type { MockedObject } from 'vitest';
|
|
||||||
import AlbumCard from '../album-card.svelte';
|
import AlbumCard from '../album-card.svelte';
|
||||||
|
|
||||||
vi.mock('@immich/sdk', async (originalImport) => {
|
|
||||||
const module = await originalImport<typeof import('@immich/sdk')>();
|
|
||||||
const mock = { ...module, getAssetThumbnail: vi.fn() };
|
|
||||||
return { ...mock, default: mock };
|
|
||||||
});
|
|
||||||
|
|
||||||
const sdkMock: MockedObject<typeof sdk> = sdk as MockedObject<typeof sdk>;
|
|
||||||
const onShowContextMenu = vi.fn();
|
const onShowContextMenu = vi.fn();
|
||||||
|
|
||||||
describe('AlbumCard component', () => {
|
describe('AlbumCard component', () => {
|
||||||
|
@ -6,7 +6,9 @@
|
|||||||
let dragStartTarget: EventTarget | null = null;
|
let dragStartTarget: EventTarget | null = null;
|
||||||
|
|
||||||
const handleDragEnter = (e: DragEvent) => {
|
const handleDragEnter = (e: DragEvent) => {
|
||||||
|
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||||
dragStartTarget = e.target;
|
dragStartTarget = e.target;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
357
web/src/lib/stores/asset.store.spec.ts
Normal file
357
web/src/lib/stores/asset.store.spec.ts
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||||
|
import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk';
|
||||||
|
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||||
|
import { AssetStore, BucketPosition } from './assets.store';
|
||||||
|
|
||||||
|
describe('AssetStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('init', () => {
|
||||||
|
let assetStore: AssetStore;
|
||||||
|
const bucketAssets: Record<string, AssetResponseDto[]> = {
|
||||||
|
'2024-03-01T00:00:00.000Z': assetFactory.buildList(1),
|
||||||
|
'2024-02-01T00:00:00.000Z': assetFactory.buildList(100),
|
||||||
|
'2024-01-01T00:00:00.000Z': assetFactory.buildList(3),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
assetStore = new AssetStore({});
|
||||||
|
sdkMock.getTimeBuckets.mockResolvedValue([
|
||||||
|
{ count: 1, timeBucket: '2024-03-01T00:00:00.000Z' },
|
||||||
|
{ count: 100, timeBucket: '2024-02-01T00:00:00.000Z' },
|
||||||
|
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
||||||
|
]);
|
||||||
|
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket]));
|
||||||
|
|
||||||
|
await assetStore.init({ width: 1588, height: 1000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load buckets in viewport', () => {
|
||||||
|
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
|
||||||
|
expect(sdkMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.Month });
|
||||||
|
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calculates bucket height', () => {
|
||||||
|
expect(assetStore.buckets).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 235 }),
|
||||||
|
expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 3760 }),
|
||||||
|
expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 235 }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calculates timeline height', () => {
|
||||||
|
expect(assetStore.timelineHeight).toBe(4230);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadBucket', () => {
|
||||||
|
let assetStore: AssetStore;
|
||||||
|
const bucketAssets: Record<string, AssetResponseDto[]> = {
|
||||||
|
'2024-01-03T00:00:00.000Z': assetFactory.buildList(1),
|
||||||
|
'2024-01-01T00:00:00.000Z': assetFactory.buildList(3),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
assetStore = new AssetStore({});
|
||||||
|
sdkMock.getTimeBuckets.mockResolvedValue([
|
||||||
|
{ count: 1, timeBucket: '2024-01-03T00:00:00.000Z' },
|
||||||
|
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
||||||
|
]);
|
||||||
|
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket]));
|
||||||
|
await assetStore.init({ width: 0, height: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads a bucket', async () => {
|
||||||
|
expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.assets.length).toEqual(0);
|
||||||
|
await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||||
|
expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.assets.length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores invalid buckets', async () => {
|
||||||
|
await assetStore.loadBucket('2023-01-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
expect(sdkMock.getTimeBucket).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only updates the position of loaded buckets', async () => {
|
||||||
|
await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown);
|
||||||
|
expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.position).toEqual(BucketPosition.Unknown);
|
||||||
|
|
||||||
|
await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.position).toEqual(BucketPosition.Visible);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancels bucket loading', async () => {
|
||||||
|
const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
|
||||||
|
const loadPromise = assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown);
|
||||||
|
|
||||||
|
const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z');
|
||||||
|
expect(bucket).not.toBeNull();
|
||||||
|
|
||||||
|
assetStore.cancelBucket(bucket!);
|
||||||
|
expect(abortSpy).toBeCalledTimes(1);
|
||||||
|
await loadPromise;
|
||||||
|
expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.assets.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addAssets', () => {
|
||||||
|
let assetStore: AssetStore;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
assetStore = new AssetStore({});
|
||||||
|
sdkMock.getTimeBuckets.mockResolvedValue([]);
|
||||||
|
await assetStore.init({ width: 1588, height: 1000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is empty initially', () => {
|
||||||
|
expect(assetStore.buckets.length).toEqual(0);
|
||||||
|
expect(assetStore.assets.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds assets to new bucket', () => {
|
||||||
|
const asset = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' });
|
||||||
|
assetStore.addAssets([asset]);
|
||||||
|
|
||||||
|
expect(assetStore.buckets.length).toEqual(1);
|
||||||
|
expect(assetStore.assets.length).toEqual(1);
|
||||||
|
expect(assetStore.buckets[0].assets.length).toEqual(1);
|
||||||
|
expect(assetStore.buckets[0].bucketDate).toEqual('2024-01-01T00:00:00.000Z');
|
||||||
|
expect(assetStore.assets[0].id).toEqual(asset.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds assets to existing bucket', () => {
|
||||||
|
const [assetOne, assetTwo] = assetFactory.buildList(2, { fileCreatedAt: '2024-01-20T12:00:00.000Z' });
|
||||||
|
assetStore.addAssets([assetOne]);
|
||||||
|
assetStore.addAssets([assetTwo]);
|
||||||
|
|
||||||
|
expect(assetStore.buckets.length).toEqual(1);
|
||||||
|
expect(assetStore.assets.length).toEqual(2);
|
||||||
|
expect(assetStore.buckets[0].assets.length).toEqual(2);
|
||||||
|
expect(assetStore.buckets[0].bucketDate).toEqual('2024-01-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('orders assets in buckets by descending date', () => {
|
||||||
|
const assetOne = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' });
|
||||||
|
const assetTwo = assetFactory.build({ fileCreatedAt: '2024-01-15T12:00:00.000Z' });
|
||||||
|
const assetThree = assetFactory.build({ fileCreatedAt: '2024-01-16T12:00:00.000Z' });
|
||||||
|
assetStore.addAssets([assetOne, assetTwo, assetThree]);
|
||||||
|
|
||||||
|
const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z');
|
||||||
|
expect(bucket).not.toBeNull();
|
||||||
|
expect(bucket?.assets.length).toEqual(3);
|
||||||
|
expect(bucket?.assets[0].id).toEqual(assetOne.id);
|
||||||
|
expect(bucket?.assets[1].id).toEqual(assetThree.id);
|
||||||
|
expect(bucket?.assets[2].id).toEqual(assetTwo.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('orders buckets by descending date', () => {
|
||||||
|
const assetOne = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' });
|
||||||
|
const assetTwo = assetFactory.build({ fileCreatedAt: '2024-04-20T12:00:00.000Z' });
|
||||||
|
const assetThree = assetFactory.build({ fileCreatedAt: '2023-01-20T12:00:00.000Z' });
|
||||||
|
assetStore.addAssets([assetOne, assetTwo, assetThree]);
|
||||||
|
|
||||||
|
expect(assetStore.buckets.length).toEqual(3);
|
||||||
|
expect(assetStore.buckets[0].bucketDate).toEqual('2024-04-01T00:00:00.000Z');
|
||||||
|
expect(assetStore.buckets[1].bucketDate).toEqual('2024-01-01T00:00:00.000Z');
|
||||||
|
expect(assetStore.buckets[2].bucketDate).toEqual('2023-01-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates existing asset', () => {
|
||||||
|
const updateAssetsSpy = vi.spyOn(assetStore, 'updateAssets');
|
||||||
|
const asset = assetFactory.build();
|
||||||
|
assetStore.addAssets([asset]);
|
||||||
|
|
||||||
|
assetStore.addAssets([asset]);
|
||||||
|
expect(updateAssetsSpy).toBeCalledWith([asset]);
|
||||||
|
expect(assetStore.assets.length).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateAssets', () => {
|
||||||
|
let assetStore: AssetStore;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
assetStore = new AssetStore({});
|
||||||
|
sdkMock.getTimeBuckets.mockResolvedValue([]);
|
||||||
|
await assetStore.init({ width: 1588, height: 1000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores non-existing assets', () => {
|
||||||
|
assetStore.updateAssets([assetFactory.build()]);
|
||||||
|
|
||||||
|
expect(assetStore.buckets.length).toEqual(0);
|
||||||
|
expect(assetStore.assets.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates an asset', () => {
|
||||||
|
const asset = assetFactory.build({ isFavorite: false });
|
||||||
|
const updatedAsset = { ...asset, isFavorite: true };
|
||||||
|
|
||||||
|
assetStore.addAssets([asset]);
|
||||||
|
expect(assetStore.assets.length).toEqual(1);
|
||||||
|
expect(assetStore.assets[0].isFavorite).toEqual(false);
|
||||||
|
|
||||||
|
assetStore.updateAssets([updatedAsset]);
|
||||||
|
expect(assetStore.assets.length).toEqual(1);
|
||||||
|
expect(assetStore.assets[0].isFavorite).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces bucket date when asset date changes', () => {
|
||||||
|
const asset = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' });
|
||||||
|
const updatedAsset = { ...asset, fileCreatedAt: '2024-03-20T12:00:00.000Z' };
|
||||||
|
|
||||||
|
assetStore.addAssets([asset]);
|
||||||
|
expect(assetStore.buckets.length).toEqual(1);
|
||||||
|
expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')).not.toBeNull();
|
||||||
|
|
||||||
|
assetStore.updateAssets([updatedAsset]);
|
||||||
|
expect(assetStore.buckets.length).toEqual(1);
|
||||||
|
expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')).toBeNull();
|
||||||
|
expect(assetStore.getBucketByDate('2024-03-01T00:00:00.000Z')).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeAssets', () => {
|
||||||
|
let assetStore: AssetStore;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
assetStore = new AssetStore({});
|
||||||
|
sdkMock.getTimeBuckets.mockResolvedValue([]);
|
||||||
|
await assetStore.init({ width: 1588, height: 1000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores invalid IDs', () => {
|
||||||
|
assetStore.addAssets(assetFactory.buildList(2, { fileCreatedAt: '2024-01-20T12:00:00.000Z' }));
|
||||||
|
assetStore.removeAssets(['', 'invalid', '4c7d9acc']);
|
||||||
|
|
||||||
|
expect(assetStore.assets.length).toEqual(2);
|
||||||
|
expect(assetStore.buckets.length).toEqual(1);
|
||||||
|
expect(assetStore.buckets[0].assets.length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes asset from bucket', () => {
|
||||||
|
const [assetOne, assetTwo] = assetFactory.buildList(2, { fileCreatedAt: '2024-01-20T12:00:00.000Z' });
|
||||||
|
assetStore.addAssets([assetOne, assetTwo]);
|
||||||
|
assetStore.removeAssets([assetOne.id]);
|
||||||
|
|
||||||
|
expect(assetStore.assets.length).toEqual(1);
|
||||||
|
expect(assetStore.buckets.length).toEqual(1);
|
||||||
|
expect(assetStore.buckets[0].assets.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes bucket when empty', () => {
|
||||||
|
const assets = assetFactory.buildList(2, { fileCreatedAt: '2024-01-20T12:00:00.000Z' });
|
||||||
|
assetStore.addAssets(assets);
|
||||||
|
assetStore.removeAssets(assets.map((asset) => asset.id));
|
||||||
|
|
||||||
|
expect(assetStore.assets.length).toEqual(0);
|
||||||
|
expect(assetStore.buckets.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPreviousAssetId', () => {
|
||||||
|
let assetStore: AssetStore;
|
||||||
|
const bucketAssets: Record<string, AssetResponseDto[]> = {
|
||||||
|
'2024-03-01T00:00:00.000Z': assetFactory.buildList(1),
|
||||||
|
'2024-02-01T00:00:00.000Z': assetFactory.buildList(6),
|
||||||
|
'2024-01-01T00:00:00.000Z': assetFactory.buildList(3),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
assetStore = new AssetStore({});
|
||||||
|
sdkMock.getTimeBuckets.mockResolvedValue([
|
||||||
|
{ count: 1, timeBucket: '2024-03-01T00:00:00.000Z' },
|
||||||
|
{ count: 6, timeBucket: '2024-02-01T00:00:00.000Z' },
|
||||||
|
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
||||||
|
]);
|
||||||
|
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket]));
|
||||||
|
|
||||||
|
await assetStore.init({ width: 0, height: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for invalid assetId', async () => {
|
||||||
|
expect(() => assetStore.getPreviousAssetId('invalid')).not.toThrow();
|
||||||
|
expect(await assetStore.getPreviousAssetId('invalid')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns previous assetId', async () => {
|
||||||
|
await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z');
|
||||||
|
|
||||||
|
expect(await assetStore.getPreviousAssetId(bucket!.assets[1].id)).toEqual(bucket!.assets[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns previous assetId spanning multiple buckets', async () => {
|
||||||
|
await assetStore.loadBucket('2024-02-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
await assetStore.loadBucket('2024-03-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
|
||||||
|
const bucket = assetStore.getBucketByDate('2024-02-01T00:00:00.000Z');
|
||||||
|
const previousBucket = assetStore.getBucketByDate('2024-03-01T00:00:00.000Z');
|
||||||
|
expect(await assetStore.getPreviousAssetId(bucket!.assets[0].id)).toEqual(previousBucket!.assets[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads previous bucket', async () => {
|
||||||
|
await assetStore.loadBucket('2024-02-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
|
||||||
|
const loadBucketSpy = vi.spyOn(assetStore, 'loadBucket');
|
||||||
|
const bucket = assetStore.getBucketByDate('2024-02-01T00:00:00.000Z');
|
||||||
|
const previousBucket = assetStore.getBucketByDate('2024-03-01T00:00:00.000Z');
|
||||||
|
expect(await assetStore.getPreviousAssetId(bucket!.assets[0].id)).toEqual(previousBucket!.assets[0].id);
|
||||||
|
expect(loadBucketSpy).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips removed assets', async () => {
|
||||||
|
await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
await assetStore.loadBucket('2024-02-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
await assetStore.loadBucket('2024-03-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
|
||||||
|
const [assetOne, assetTwo, assetThree] = assetStore.assets;
|
||||||
|
assetStore.removeAssets([assetTwo.id]);
|
||||||
|
expect(await assetStore.getPreviousAssetId(assetThree.id)).toEqual(assetOne.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when no more assets', async () => {
|
||||||
|
await assetStore.loadBucket('2024-03-01T00:00:00.000Z', BucketPosition.Visible);
|
||||||
|
expect(await assetStore.getPreviousAssetId(assetStore.assets[0].id)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBucketIndexByAssetId', () => {
|
||||||
|
let assetStore: AssetStore;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
assetStore = new AssetStore({});
|
||||||
|
sdkMock.getTimeBuckets.mockResolvedValue([]);
|
||||||
|
await assetStore.init({ width: 0, height: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for invalid buckets', () => {
|
||||||
|
expect(assetStore.getBucketByDate('invalid')).toBeNull();
|
||||||
|
expect(assetStore.getBucketByDate('2024-03-01T00:00:00.000Z')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the bucket index', () => {
|
||||||
|
const assetOne = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' });
|
||||||
|
const assetTwo = assetFactory.build({ fileCreatedAt: '2024-02-15T12:00:00.000Z' });
|
||||||
|
assetStore.addAssets([assetOne, assetTwo]);
|
||||||
|
|
||||||
|
expect(assetStore.getBucketIndexByAssetId(assetTwo.id)).toEqual(0);
|
||||||
|
expect(assetStore.getBucketIndexByAssetId(assetOne.id)).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores removed buckets', () => {
|
||||||
|
const assetOne = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' });
|
||||||
|
const assetTwo = assetFactory.build({ fileCreatedAt: '2024-02-15T12:00:00.000Z' });
|
||||||
|
assetStore.addAssets([assetOne, assetTwo]);
|
||||||
|
|
||||||
|
assetStore.removeAssets([assetTwo.id]);
|
||||||
|
expect(assetStore.getBucketIndexByAssetId(assetOne.id)).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -3,20 +3,20 @@
|
|||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import type { SearchExploreResponseDto } from '@immich/sdk';
|
|
||||||
import { mdiMapMarkerOff } from '@mdi/js';
|
import { mdiMapMarkerOff } from '@mdi/js';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||||
|
import type { AssetResponseDto } from '@immich/sdk';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
const CITY_FIELD = 'exifInfo.city';
|
type AssetWithCity = AssetResponseDto & {
|
||||||
const getFieldItems = (items: SearchExploreResponseDto[]) => {
|
exifInfo: {
|
||||||
const targetField = items.find((item) => item.fieldName === CITY_FIELD);
|
city: string;
|
||||||
return targetField?.items || [];
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
$: places = getFieldItems(data.items);
|
$: places = data.items.filter((item): item is AssetWithCity => !!item.exifInfo?.city);
|
||||||
$: hasPlaces = places.length > 0;
|
$: hasPlaces = places.length > 0;
|
||||||
|
|
||||||
let innerHeight: number;
|
let innerHeight: number;
|
||||||
@ -27,17 +27,18 @@
|
|||||||
<UserPageLayout title="Places">
|
<UserPageLayout title="Places">
|
||||||
{#if hasPlaces}
|
{#if hasPlaces}
|
||||||
<div class="flex flex-row flex-wrap gap-4">
|
<div class="flex flex-row flex-wrap gap-4">
|
||||||
{#each places as item (item.data.id)}
|
{#each places as item (item.id)}
|
||||||
<a class="relative" href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city: item.value })}" draggable="false">
|
{@const city = item.exifInfo.city}
|
||||||
|
<a class="relative" href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city })}" draggable="false">
|
||||||
<div
|
<div
|
||||||
class="flex w-[calc((100vw-(72px+5rem))/2)] max-w-[156px] justify-center overflow-hidden rounded-xl brightness-75 filter"
|
class="flex w-[calc((100vw-(72px+5rem))/2)] max-w-[156px] justify-center overflow-hidden rounded-xl brightness-75 filter"
|
||||||
>
|
>
|
||||||
<Thumbnail thumbnailSize={156} asset={item.data} readonly />
|
<Thumbnail thumbnailSize={156} asset={item} readonly />
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center text-sm font-medium capitalize text-white backdrop-blur-[1px] hover:cursor-pointer"
|
class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center text-sm font-medium capitalize text-white backdrop-blur-[1px] hover:cursor-pointer"
|
||||||
>
|
>
|
||||||
{item.value}
|
{city}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getExploreData } from '@immich/sdk';
|
import { getAssetsByCity } from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
await authenticate();
|
await authenticate();
|
||||||
const items = await getExploreData();
|
const items = await getAssetsByCity();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
|
30
web/src/test-data/factories/asset-factory.ts
Normal file
30
web/src/test-data/factories/asset-factory.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
||||||
|
import { Sync } from 'factory.ts';
|
||||||
|
|
||||||
|
export const assetFactory = Sync.makeFactory<AssetResponseDto>({
|
||||||
|
id: Sync.each(() => faker.string.uuid()),
|
||||||
|
deviceAssetId: Sync.each(() => faker.string.uuid()),
|
||||||
|
ownerId: Sync.each(() => faker.string.uuid()),
|
||||||
|
deviceId: '',
|
||||||
|
libraryId: Sync.each(() => faker.string.uuid()),
|
||||||
|
type: Sync.each(() => faker.helpers.enumValue(AssetTypeEnum)),
|
||||||
|
originalPath: Sync.each(() => faker.system.filePath()),
|
||||||
|
originalFileName: Sync.each(() => faker.system.fileName()),
|
||||||
|
resized: true,
|
||||||
|
thumbhash: Sync.each(() => faker.string.alphanumeric(28)),
|
||||||
|
fileCreatedAt: Sync.each(() => faker.date.past().toISOString()),
|
||||||
|
fileModifiedAt: Sync.each(() => faker.date.past().toISOString()),
|
||||||
|
localDateTime: Sync.each(() => faker.date.past().toISOString()),
|
||||||
|
updatedAt: Sync.each(() => faker.date.past().toISOString()),
|
||||||
|
isFavorite: Sync.each(() => faker.datatype.boolean()),
|
||||||
|
isArchived: Sync.each(() => faker.datatype.boolean()),
|
||||||
|
isTrashed: Sync.each(() => faker.datatype.boolean()),
|
||||||
|
duration: '0:00:00.00000',
|
||||||
|
checksum: Sync.each(() => faker.string.alphanumeric(28)),
|
||||||
|
isExternal: Sync.each(() => faker.datatype.boolean()),
|
||||||
|
isOffline: Sync.each(() => faker.datatype.boolean()),
|
||||||
|
isReadOnly: Sync.each(() => faker.datatype.boolean()),
|
||||||
|
hasMetadata: Sync.each(() => faker.datatype.boolean()),
|
||||||
|
stackCount: null,
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user