mirror of
https://github.com/immich-app/immich.git
synced 2026-05-24 16:42:30 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e03c0cdbba |
@@ -210,7 +210,7 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0
|
||||
uses: ruby/setup-ruby@c515ec17f69368147deb311832da000dd229d338 # v1.297.0
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
needs: [get_body, should_run]
|
||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||
container:
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:557cca601891b8b7d78b940071d35aaf7aaeb9b327d19b22cf282118edbc5272
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:df7188ba88abb0800d73cc97d3633280f0c0c3d4c441d678225067bf154150fb
|
||||
outputs:
|
||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||
steps:
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -83,6 +83,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
suffix: ['']
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0
|
||||
- uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
message-id: 'preview-status'
|
||||
@@ -48,14 +48,14 @@ jobs:
|
||||
name: 'preview'
|
||||
})
|
||||
|
||||
- uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0
|
||||
- uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
|
||||
if: ${{ github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
message-id: 'preview-status'
|
||||
message: 'PRs from forks cannot have preview environments.'
|
||||
|
||||
- uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0
|
||||
- uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.2",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
|
||||
@@ -26,7 +26,7 @@ You can search the following types of content:
|
||||
| Time frame | Start and end date of a specific time bucket |
|
||||
| Media type | Image or video or both |
|
||||
| Display options | In Archive, in Favorites or Not in any album |
|
||||
| Star rating | User-assigned star rating |
|
||||
| Start rating | User-assigned start rating |
|
||||
|
||||
<img src={require('./img/advanced-search-filters.webp').default} width="70%" title='Advanced search filters' />
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ Immich requires [**Docker**](https://docs.docker.com/get-started/get-docker/) wi
|
||||
The Compose plugin will be installed by both Docker Engine and Desktop by following the linked installation guides; it can also be [separately installed](https://docs.docker.com/compose/install/).
|
||||
|
||||
:::note
|
||||
Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/retired/#docker-compose-v1-replaced-by-compose-v2) and is no longer supported by Immich.
|
||||
Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer supported by Immich.
|
||||
:::
|
||||
|
||||
### Special requirements for Windows users
|
||||
|
||||
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "v2.7.3",
|
||||
"url": "https://docs.v2.7.3.archive.immich.app"
|
||||
"label": "v2.7.2",
|
||||
"url": "https://docs.v2.7.2.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v2.6.3",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-i18n",
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"format": "prettier --cache --check .",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "immich-ml"
|
||||
version = "2.7.3"
|
||||
version = "2.7.2"
|
||||
description = ""
|
||||
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
|
||||
requires-python = ">=3.11,<4.0"
|
||||
|
||||
Generated
+1
-1
@@ -898,7 +898,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "immich-ml"
|
||||
version = "2.7.3"
|
||||
version = "2.7.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3044,
|
||||
"android.injected.version.name" => "2.7.3",
|
||||
"android.injected.version.code" => 3043,
|
||||
"android.injected.version.name" => "2.7.2",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.7.3</string>
|
||||
<string>2.7.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -45,17 +45,14 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
||||
final syncManager = ref.read(backgroundSyncProvider);
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||
|
||||
await backupNotifier.getBackupStatus(currentUser.id);
|
||||
|
||||
backupNotifier.updateSyncing(true);
|
||||
syncSuccess = await syncManager.syncRemote();
|
||||
backupNotifier.updateSyncing(false);
|
||||
ref.read(driftBackupProvider.notifier).updateSyncing(true);
|
||||
syncSuccess = await ref.read(backgroundSyncProvider).syncRemote();
|
||||
ref.read(driftBackupProvider.notifier).updateSyncing(false);
|
||||
|
||||
if (mounted) {
|
||||
await backupNotifier.getBackupStatus(currentUser.id);
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -85,9 +82,9 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
}
|
||||
|
||||
if (syncSuccess == null) {
|
||||
backupNotifier.updateSyncing(true);
|
||||
ref.read(driftBackupProvider.notifier).updateSyncing(true);
|
||||
syncSuccess = await backupSyncManager.syncRemote();
|
||||
backupNotifier.updateSyncing(false);
|
||||
ref.read(driftBackupProvider.notifier).updateSyncing(false);
|
||||
}
|
||||
|
||||
await backupNotifier.getBackupStatus(currentUser.id);
|
||||
|
||||
@@ -19,7 +19,6 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
||||
static final _log = Logger('CancellableImageProviderMixin');
|
||||
|
||||
bool isCancelled = false;
|
||||
bool isFinished = false;
|
||||
ImageRequest? request;
|
||||
CancelableOperation<ImageInfo?>? cachedOperation;
|
||||
|
||||
@@ -54,15 +53,13 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
||||
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode, {bool evictOnError = true}) async* {
|
||||
if (isCancelled) {
|
||||
this.request = null;
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final image = await request.load(decode);
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
if (image == null && evictOnError) {
|
||||
if ((image == null && evictOnError) || isCancelled) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return;
|
||||
} else if (image == null) {
|
||||
@@ -70,9 +67,6 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
||||
}
|
||||
yield image;
|
||||
} catch (e, stack) {
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
if (evictOnError) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
rethrow;
|
||||
@@ -86,24 +80,20 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
||||
Future<ui.Codec?> loadCodecRequest(ImageRequest request) async {
|
||||
if (isCancelled) {
|
||||
this.request = null;
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final codec = await request.loadCodec();
|
||||
if (isCancelled) {
|
||||
if (codec == null || isCancelled) {
|
||||
codec?.dispose();
|
||||
return null;
|
||||
}
|
||||
if (codec == null) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return null;
|
||||
}
|
||||
return codec;
|
||||
} catch (e) {
|
||||
if (!isCancelled) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
}
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
rethrow;
|
||||
} finally {
|
||||
this.request = null;
|
||||
@@ -131,8 +121,6 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
||||
@override
|
||||
void cancel() {
|
||||
isCancelled = true;
|
||||
final hasActiveWork = !isFinished;
|
||||
|
||||
final request = this.request;
|
||||
if (request != null) {
|
||||
this.request = null;
|
||||
@@ -144,10 +132,6 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
||||
cachedOperation = null;
|
||||
operation.cancel();
|
||||
}
|
||||
|
||||
if (hasActiveWork) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
||||
yield* initialImageStream();
|
||||
|
||||
if (isCancelled) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,24 +113,24 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
||||
yield* loadRequest(request, decode);
|
||||
|
||||
if (!Store.get(StoreKey.loadOriginal, false)) {
|
||||
isFinished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCancelled) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return;
|
||||
}
|
||||
|
||||
request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero);
|
||||
|
||||
yield* loadRequest(request, decode);
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
Stream<Object> _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||
yield* initialImageStream();
|
||||
|
||||
if (isCancelled) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,6 +143,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
||||
yield* loadRequest(previewRequest, decode);
|
||||
|
||||
if (isCancelled) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -149,11 +151,9 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
||||
final originalRequest = request = LocalImageRequest(localId: key.id, size: Size.zero, assetType: key.assetType);
|
||||
final codec = await loadCodecRequest(originalRequest);
|
||||
if (codec == null) {
|
||||
if (isCancelled) return;
|
||||
throw StateError('Failed to load animated codec for local asset ${key.id}');
|
||||
}
|
||||
yield codec;
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -105,6 +105,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
yield* initialImageStream();
|
||||
|
||||
if (isCancelled) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,23 +116,23 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
yield* loadRequest(previewRequest, decode, evictOnError: !loadOriginal);
|
||||
|
||||
if (!loadOriginal) {
|
||||
isFinished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCancelled) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return;
|
||||
}
|
||||
|
||||
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId));
|
||||
yield* loadRequest(originalRequest, decode);
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
Stream<Object> _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||
yield* initialImageStream();
|
||||
|
||||
if (isCancelled) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,6 +142,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
yield* loadRequest(previewRequest, decode, evictOnError: false);
|
||||
|
||||
if (isCancelled) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -148,13 +150,9 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId));
|
||||
final codec = await loadCodecRequest(originalRequest);
|
||||
if (codec == null) {
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
throw StateError('Failed to load animated codec for asset ${key.assetId}');
|
||||
}
|
||||
yield codec;
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Generated
+1
-1
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 2.7.3
|
||||
- API version: 2.7.2
|
||||
- Generator version: 7.8.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 2.7.3+3044
|
||||
version: 2.7.2+3043
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
@@ -15225,7 +15225,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.2",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.2",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 2.7.3
|
||||
* 2.7.2
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-monorepo",
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.2",
|
||||
"description": "Monorepo for Immich",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be",
|
||||
|
||||
Generated
+46
@@ -553,6 +553,9 @@ importers:
|
||||
sanitize-filename:
|
||||
specifier: ^1.6.3
|
||||
version: 1.6.4
|
||||
sanitize-html:
|
||||
specifier: ^2.14.0
|
||||
version: 2.17.2
|
||||
semver:
|
||||
specifier: ^7.6.2
|
||||
version: 7.7.4
|
||||
@@ -656,6 +659,9 @@ importers:
|
||||
'@types/react':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.14
|
||||
'@types/sanitize-html':
|
||||
specifier: ^2.13.0
|
||||
version: 2.16.1
|
||||
'@types/semver':
|
||||
specifier: ^7.5.8
|
||||
version: 7.7.1
|
||||
@@ -5161,6 +5167,9 @@ packages:
|
||||
'@types/retry@0.12.2':
|
||||
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
|
||||
|
||||
'@types/sanitize-html@2.16.1':
|
||||
resolution: {integrity: sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==}
|
||||
|
||||
'@types/sax@1.2.7':
|
||||
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
|
||||
|
||||
@@ -7962,6 +7971,9 @@ packages:
|
||||
webpack:
|
||||
optional: true
|
||||
|
||||
htmlparser2@10.1.0:
|
||||
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
|
||||
|
||||
htmlparser2@6.1.0:
|
||||
resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
|
||||
|
||||
@@ -8291,6 +8303,10 @@ packages:
|
||||
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-plain-object@5.0.0:
|
||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-potential-custom-element-name@1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
|
||||
@@ -9676,6 +9692,9 @@ packages:
|
||||
parse-numeric-range@1.3.0:
|
||||
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
|
||||
|
||||
parse-srcset@1.0.2:
|
||||
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
|
||||
|
||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
||||
|
||||
@@ -10873,6 +10892,9 @@ packages:
|
||||
sanitize-filename@1.6.4:
|
||||
resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==}
|
||||
|
||||
sanitize-html@2.17.2:
|
||||
resolution: {integrity: sha512-EnffJUl46VE9uvZ0XeWzObHLurClLlT12gsOk1cHyP2Ol1P0BnBnsXmShlBmWVJM+dKieQI68R0tsPY5m/B+Jg==}
|
||||
|
||||
sass@1.97.1:
|
||||
resolution: {integrity: sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -17437,6 +17459,10 @@ snapshots:
|
||||
|
||||
'@types/retry@0.12.2': {}
|
||||
|
||||
'@types/sanitize-html@2.16.1':
|
||||
dependencies:
|
||||
htmlparser2: 10.1.0
|
||||
|
||||
'@types/sax@1.2.7':
|
||||
dependencies:
|
||||
'@types/node': 24.12.2
|
||||
@@ -20839,6 +20865,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
webpack: 5.104.1
|
||||
|
||||
htmlparser2@10.1.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
entities: 7.0.1
|
||||
|
||||
htmlparser2@6.1.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
@@ -21167,6 +21200,8 @@ snapshots:
|
||||
dependencies:
|
||||
isobject: 3.0.1
|
||||
|
||||
is-plain-object@5.0.0: {}
|
||||
|
||||
is-potential-custom-element-name@1.0.1:
|
||||
optional: true
|
||||
|
||||
@@ -22898,6 +22933,8 @@ snapshots:
|
||||
|
||||
parse-numeric-range@1.3.0: {}
|
||||
|
||||
parse-srcset@1.0.2: {}
|
||||
|
||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
@@ -24241,6 +24278,15 @@ snapshots:
|
||||
dependencies:
|
||||
truncate-utf8-bytes: 1.0.2
|
||||
|
||||
sanitize-html@2.17.2:
|
||||
dependencies:
|
||||
deepmerge: 4.3.1
|
||||
escape-string-regexp: 4.0.0
|
||||
htmlparser2: 10.1.0
|
||||
is-plain-object: 5.0.0
|
||||
parse-srcset: 1.0.2
|
||||
postcss: 8.5.8
|
||||
|
||||
sass@1.97.1:
|
||||
dependencies:
|
||||
chokidar: 4.0.3
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -110,6 +110,7 @@
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"semver": "^7.6.2",
|
||||
"sharp": "^0.34.5",
|
||||
"sirv": "^3.0.0",
|
||||
@@ -146,6 +147,7 @@
|
||||
"@types/picomatch": "^4.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/supertest": "^7.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
|
||||
@@ -848,7 +848,6 @@ export enum AssetVisibility {
|
||||
export enum CronJob {
|
||||
LibraryScan = 'LibraryScan',
|
||||
NightlyJobs = 'NightlyJobs',
|
||||
VersionCheck = 'VersionCheck',
|
||||
}
|
||||
|
||||
export enum ApiTag {
|
||||
|
||||
@@ -36,17 +36,15 @@ export class MaintenanceHealthRepository {
|
||||
}
|
||||
});
|
||||
|
||||
worker.on('exit', (code, signal) =>
|
||||
reject(new Error(`Server health check failed, server exited with ${signal ?? code}`)),
|
||||
);
|
||||
worker.on('error', (error) => reject(new Error(`Server health check failed, process threw: ${error}`)));
|
||||
worker.on('exit', (code, signal) => reject(`Server health check failed, server exited with ${signal ?? code}`));
|
||||
worker.on('error', (error) => reject(`Server health check failed, process threw: ${error}`));
|
||||
|
||||
setTimeout(() => {
|
||||
if (worker.exitCode === null) {
|
||||
reject(new Error('Server health check failed, took too long to start.'));
|
||||
reject('Server health check failed, took too long to start.');
|
||||
worker.kill('SIGTERM');
|
||||
}
|
||||
}, 180_000);
|
||||
}, 20_000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,19 +195,13 @@ where
|
||||
"asset_face"."id" = $2
|
||||
|
||||
-- PersonRepository.getByName
|
||||
with
|
||||
"similarity_threshold" as (
|
||||
select
|
||||
set_config('pg_trgm.word_similarity_threshold', '0.5', true) as "thresh"
|
||||
)
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"similarity_threshold",
|
||||
"person"
|
||||
where
|
||||
"person"."ownerId" = $1
|
||||
and f_unaccent ("person"."name") %> f_unaccent ($2)
|
||||
and f_unaccent ("person"."name") %>> f_unaccent ($2)
|
||||
order by
|
||||
f_unaccent ("person"."name") <->>> f_unaccent ($3)
|
||||
limit
|
||||
|
||||
@@ -310,13 +310,10 @@ export class PersonRepository {
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, { withHidden: true }] })
|
||||
getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions) {
|
||||
return this.db
|
||||
.with('similarity_threshold', (db) =>
|
||||
db.selectNoFrom(sql`set_config('pg_trgm.word_similarity_threshold', '0.5', true)`.as('thresh')),
|
||||
)
|
||||
.selectFrom(['similarity_threshold', 'person'])
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.where('person.ownerId', '=', userId)
|
||||
.where(() => sql`f_unaccent("person"."name") %> f_unaccent(${personName})`)
|
||||
.where(() => sql`f_unaccent("person"."name") %>> f_unaccent(${personName})`)
|
||||
.orderBy(sql`f_unaccent("person"."name") <->>> f_unaccent(${personName})`)
|
||||
.limit(100)
|
||||
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { ApiService, render } from 'src/services/api.service';
|
||||
|
||||
describe(ApiService.name, () => {
|
||||
describe('render', () => {
|
||||
it('should correctly render open graph tags', () => {
|
||||
const output = render('<!-- metadata:tags -->', {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
imageUrl: 'https://demo.immich.app/api/assets/123',
|
||||
});
|
||||
expect(output).toContain('<meta property="og:title" content="title" />');
|
||||
expect(output).toContain('<meta property="og:description" content="description" />');
|
||||
expect(output).toContain('<meta property="og:image" content="https://demo.immich.app/api/assets/123" />');
|
||||
});
|
||||
|
||||
it('should escape html tags', () => {
|
||||
expect(
|
||||
render('<!-- metadata:tags -->', {
|
||||
title: "<script>console.log('hello')</script>Test",
|
||||
description: 'description',
|
||||
}),
|
||||
).toContain(
|
||||
'<meta property="og:title" content="<script>console.log('hello')</script>Test" />',
|
||||
);
|
||||
});
|
||||
|
||||
it('should escape quotes', () => {
|
||||
expect(
|
||||
render('<!-- metadata:tags -->', {
|
||||
title: `0;url=https://example.com" http-equiv="refresh`,
|
||||
description: 'description',
|
||||
}),
|
||||
).toContain('<meta property="og:title" content="0;url=https://example.com" http-equiv="refresh" />');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,19 @@
|
||||
import { Injectable, NotAcceptableException } from '@nestjs/common';
|
||||
import { Interval } from '@nestjs/schedule';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { escape } from 'lodash';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { ONE_HOUR } from 'src/constants';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
import { OpenGraphTags } from 'src/utils/misc';
|
||||
|
||||
export const render = (index: string, meta: OpenGraphTags) => {
|
||||
const [title, description, imageUrl] = [meta.title, meta.description, meta.imageUrl].map((item) =>
|
||||
item ? escape(item) : '',
|
||||
item ? sanitizeHtml(item, { allowedTags: [] }) : '',
|
||||
);
|
||||
|
||||
const tags = `
|
||||
@@ -37,12 +40,18 @@ export class ApiService {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private sharedLinkService: SharedLinkService,
|
||||
private versionService: VersionService,
|
||||
private configRepository: ConfigRepository,
|
||||
private logger: LoggingRepository,
|
||||
) {
|
||||
this.logger.setContext(ApiService.name);
|
||||
}
|
||||
|
||||
@Interval(ONE_HOUR.as('milliseconds'))
|
||||
async onVersionCheck() {
|
||||
await this.versionService.handleQueueVersionCheck();
|
||||
}
|
||||
|
||||
ssr(excludePaths: string[]) {
|
||||
const { resourcePaths } = this.configRepository.getEnv();
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DateTime } from 'luxon';
|
||||
import { SemVer } from 'semver';
|
||||
import { defaults } from 'src/config';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { CronJob, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
|
||||
import { JobName, JobStatus, SystemMetadataKey } from 'src/enum';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -18,8 +18,6 @@ describe(VersionService.name, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, mocks } = newTestService(VersionService));
|
||||
mocks.cron.create.mockResolvedValue();
|
||||
mocks.cron.update.mockResolvedValue();
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
@@ -46,20 +44,6 @@ describe(VersionService.name, () => {
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
expect(mocks.versionHistory.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a version check cron job', async () => {
|
||||
mocks.versionHistory.getLatest.mockResolvedValue({
|
||||
id: 'version-1',
|
||||
createdAt: new Date(),
|
||||
version: serverVersion.toString(),
|
||||
});
|
||||
await sut.onBootstrap();
|
||||
expect(mocks.cron.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: CronJob.VersionCheck,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVersion', () => {
|
||||
@@ -88,13 +72,25 @@ describe(VersionService.name, () => {
|
||||
});
|
||||
|
||||
describe('handVersionCheck', () => {
|
||||
it('should not run if the last check was < 60 minutes ago', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
checkedAt: DateTime.utc().minus({ minutes: 5 }).toISO(),
|
||||
releaseVersion: '1.0.0',
|
||||
});
|
||||
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Skipped);
|
||||
});
|
||||
|
||||
it('should not run if version check is disabled', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ newVersionCheck: { enabled: false } });
|
||||
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Skipped);
|
||||
});
|
||||
|
||||
it('should run and notify if a new version is available', async () => {
|
||||
it('should run if it has been > 60 minutes', async () => {
|
||||
mocks.serverInfo.getLatestRelease.mockResolvedValue(mockVersionResponse('v100.0.0'));
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
checkedAt: DateTime.utc().minus({ minutes: 65 }).toISO(),
|
||||
releaseVersion: '1.0.0',
|
||||
});
|
||||
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Success);
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalled();
|
||||
expect(mocks.logger.log).toHaveBeenCalled();
|
||||
|
||||
@@ -4,11 +4,10 @@ import semver, { SemVer } from 'semver';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { CronJob, DatabaseLock, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum';
|
||||
import { DatabaseLock, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { VersionCheckMetadata } from 'src/types';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
|
||||
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
|
||||
return {
|
||||
@@ -25,15 +24,6 @@ export class VersionService extends BaseService {
|
||||
async onBootstrap(): Promise<void> {
|
||||
await this.handleVersionCheck();
|
||||
|
||||
const randomMinute = Math.floor(Math.random() * 60);
|
||||
const expression = `${randomMinute} * * * *`;
|
||||
this.logger.debug(`Scheduling version check for cron ${expression}`);
|
||||
this.cronRepository.create({
|
||||
name: CronJob.VersionCheck,
|
||||
expression,
|
||||
onTick: () => handlePromiseError(this.handleQueueVersionCheck(), this.logger),
|
||||
});
|
||||
|
||||
await this.databaseRepository.withLock(DatabaseLock.VersionHistory, async () => {
|
||||
const previous = await this.versionRepository.getLatest();
|
||||
const current = serverVersion.toString();
|
||||
@@ -86,6 +76,16 @@ export class VersionService extends BaseService {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
const versionCheck = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState);
|
||||
if (versionCheck?.checkedAt) {
|
||||
const lastUpdate = DateTime.fromISO(versionCheck.checkedAt);
|
||||
const elapsedTime = DateTime.now().diff(lastUpdate).as('minutes');
|
||||
// check once per hour (max)
|
||||
if (elapsedTime < 60) {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
}
|
||||
|
||||
const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease();
|
||||
const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion };
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import { AssetEditRepository } from 'src/repositories/asset-edit.repository';
|
||||
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CronRepository } from 'src/repositories/cron.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { EmailRepository } from 'src/repositories/email.repository';
|
||||
@@ -501,10 +500,6 @@ const newMockRepository = <T>(key: ClassConstructor<T>) => {
|
||||
});
|
||||
}
|
||||
|
||||
case CronRepository: {
|
||||
return automock(CronRepository, { args: [undefined, { setContext: () => {} }], strict: false });
|
||||
}
|
||||
|
||||
case EmailRepository: {
|
||||
return automock(EmailRepository, { args: [{ setContext: () => {} }] });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { JobName } from 'src/enum';
|
||||
import { CronRepository } from 'src/repositories/cron.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
@@ -17,7 +16,7 @@ const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(VersionService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [DatabaseRepository, VersionHistoryRepository],
|
||||
mock: [LoggingRepository, JobRepository, CronRepository],
|
||||
mock: [LoggingRepository, JobRepository],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.2",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -36,14 +36,18 @@
|
||||
|
||||
$effect(() => {
|
||||
if (!enablePlayback) {
|
||||
// Reset remaining time when playback is disabled.
|
||||
remainingSeconds = durationInSeconds;
|
||||
|
||||
if (player) {
|
||||
// Cancel video buffering.
|
||||
player.src = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
const video = player;
|
||||
return () => {
|
||||
video.pause();
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
};
|
||||
});
|
||||
const onMouseEnter = () => {
|
||||
if (playbackOnIconHover) {
|
||||
|
||||
@@ -70,12 +70,7 @@
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!force &&
|
||||
searchedPeople.length > 0 &&
|
||||
searchedPeople.length < maximumLengthSearchPeople &&
|
||||
searchName.startsWith(searchWord)
|
||||
) {
|
||||
if (!force && searchedPeople.length < maximumLengthSearchPeople && searchName.startsWith(searchWord)) {
|
||||
search();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user