diff --git a/e2e/package.json b/e2e/package.json index 3d4f072d4..25be88ada 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "3.3.1", + "version": "3.3.3", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index a227c2959..42091b6bf 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "immich-ml" -version = "3.3.1" +version = "3.3.3" description = "" authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }] requires-python = ">=3.12,<4.0" diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 505de7f6a..70580a9f4 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -676,7 +676,7 @@ wheels = [ [[package]] name = "immich-ml" -version = "3.3.1" +version = "3.3.3" source = { editable = "." } dependencies = [ { name = "fastapi" }, diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 2a5f18ae3..54143abfc 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -22,8 +22,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 3030199, - "android.injected.version.name" => "3.3.1", + "android.injected.version.code" => 3030399, + "android.injected.version.name" => "3.3.3", } ) 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', track: 'beta') @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 3030199, - "android.injected.version.name" => "3.3.1", + "android.injected.version.code" => 3030399, + "android.injected.version.name" => "3.3.3", } ) 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') diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index e2ceb531f..4fef21539 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -78,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 3.3.1 + 3.3.3 CFBundleSignature ???? CFBundleURLTypes diff --git a/mobile/lib/presentation/widgets/memory/memory_title.widget.dart b/mobile/lib/presentation/widgets/memory/memory_title.widget.dart index 8d6d1948b..4976545fd 100644 --- a/mobile/lib/presentation/widgets/memory/memory_title.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_title.widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/extensions/object_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:intl/intl.dart'; @@ -10,7 +11,7 @@ String getMemoryTitle(Translations t, Memory memory, {RemoteAsset? asset, bool p switch (memory.type) { MemoryTypeEnum.onThisDay => preferDate - ? DateFormat.yMMMMd().format(memory.memoryAt) + ? DateFormat.yMMMMd(resolvedDateTimeLocale()).format(memory.memoryAt) : t.years_ago(years: DateTime.now().year - memory.data.year), MemoryTypeEnum.birthday => _getBirthdayTitle(t, memory.data, asset), }; diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 000720842..b8c2b8e2b 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 3.3.1-local.3+3030201 +version: 3.3.3-local.1+3030400 environment: sdk: '>=3.12.0 <4.0.0' diff --git a/mobile/test/unit/presentation/memory_list_page_test.dart b/mobile/test/unit/presentation/memory_list_page_test.dart index 235a38650..ddc154b12 100644 --- a/mobile/test/unit/presentation/memory_list_page_test.dart +++ b/mobile/test/unit/presentation/memory_list_page_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/presentation/pages/memory_list.page.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; +import 'package:intl/intl.dart'; import '../factories/remote_asset_factory.dart'; import 'presentation_context.dart'; @@ -44,6 +45,15 @@ void main() { expect(find.textContaining('ago'), findsNothing); }); + testWidgets('shows the date when intl has no date formats for the app language', (tester) async { + Intl.defaultLocale = 'kab'; + addTearDown(() => Intl.defaultLocale = null); + await pumpPage(tester, [newMemory(type: MemoryTypeEnum.onThisDay, memoryAt: DateTime(2025, 10, 7))]); + await tester.pump(); + + expect(find.text('October 7, 2025'), findsOneWidget); + }); + testWidgets('shows the person name for a birthday memory', (tester) async { await pumpPage(tester, [ newMemory(type: MemoryTypeEnum.birthday, memoryAt: DateTime(2025, 10, 7), personName: 'Alice'), diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 229e03141..d9b315d0d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -18211,7 +18211,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "3.3.1", + "version": "3.3.3", "contact": {} }, "tags": [ diff --git a/package.json b/package.json index 5f893a278..95f9b54b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "immich-monorepo", - "version": "3.3.1", + "version": "3.3.3", "description": "Monorepo for Immich", "type": "module", "private": true, diff --git a/packages/cli/package.json b/packages/cli/package.json index 5f1903fe1..8928bd6a6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "3.3.1", + "version": "3.3.3", "description": "Command Line Interface (CLI) for Immich", "repository": { "type": "git", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 09816ca2a..519ce59c9 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "3.3.1", + "version": "3.3.3", "description": "Auto-generated TypeScript SDK for the Immich API", "repository": { "type": "git", diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index f5686dade..6fa324eed 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 3.3.1 + * 3.3.3 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package.json b/server/package.json index 30499e046..7946093e2 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "3.3.1", + "version": "3.3.3", "description": "", "author": "", "private": true, diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 7eb7c3bd1..9218f6a79 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -239,38 +239,52 @@ from "person_group" inner join "person" as "owned" on "owned"."personGroupId" = "person_group"."id" and "owned"."ownerId" = $5 - inner join "asset_face" on "asset_face"."personGroupId" = "person_group"."id" - and "asset_face"."deletedAt" is null - and "asset_face"."isVisible" is true - inner join "asset" on "asset"."id" = "asset_face"."assetId" - and "asset"."visibility" = 'timeline' - and "asset"."deletedAt" is null - and ( - "asset"."ownerId" = any ($6::uuid[]) - or exists ( - select - 1 as "exists" - from - "album_asset" - inner join "album" on "album"."id" = "album_asset"."albumId" - and "album"."deletedAt" is null - inner join "album_user" on "album_user"."albumId" = "album"."id" - and "album_user"."userId" = $7::uuid - where - "album_asset"."assetId" = "asset"."id" - ) - ) + inner join ( + select + "asset_face"."personGroupId", + count("asset"."id") as "faceCount" + from + "asset_face" + inner join "asset" on "asset"."id" = "asset_face"."assetId" + and "asset"."visibility" = 'timeline' + and "asset"."deletedAt" is null + and ( + "asset"."ownerId" = any ($6::uuid[]) + or exists ( + select + 1 as "exists" + from + "album_asset" + inner join "album" on "album"."id" = "album_asset"."albumId" + and "album"."deletedAt" is null + inner join "album_user" on "album_user"."albumId" = "album"."id" + and "album_user"."userId" = $7::uuid + where + "album_asset"."assetId" = "asset"."id" + ) + ) + where + "asset_face"."personGroupId" = any ( + array( + ( + select + "person"."personGroupId" + from + "person" + where + "person"."ownerId" = $8 + ) + ) + ) + and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true + group by + "asset_face"."personGroupId" + ) as "faces" on "faces"."personGroupId" = "person_group"."id" where - "owned"."isHidden" = $8 - and 1 = 1 -group by - "person_group"."id", - "owned"."ownerId", - "owned"."personGroupId" -having ( "owned"."name" != $9 - or count("asset"."id") >= COALESCE( + or "faces"."faceCount" >= COALESCE( ( SELECT value -> 'people' ->> 'minimumFaces' @@ -283,17 +297,19 @@ having '3' )::int ) + and "owned"."isHidden" = $11 + and 1 = 1 order by "owned"."isHidden" asc, "owned"."isFavorite" desc, NULLIF("owned"."name", '') is null asc, - count("asset"."id") desc, + "faces"."faceCount" desc, NULLIF("owned"."name", '') asc nulls last, "owned"."createdAt" limit - $11 -offset $12 +offset + $13 -- PersonRepository.getAllWithoutFaces select @@ -901,49 +917,65 @@ from "person_group" inner join "person" as "owned" on "owned"."personGroupId" = "person_group"."id" and "owned"."ownerId" = $2 - inner join "asset_face" on "asset_face"."personGroupId" = "person_group"."id" - and "asset_face"."deletedAt" is null - and "asset_face"."isVisible" is true - inner join "asset" on "asset"."id" = "asset_face"."assetId" - and "asset"."visibility" = 'timeline' - and "asset"."deletedAt" is null - and ( - "asset"."ownerId" = any ($3::uuid[]) - or exists ( - select - 1 as "exists" - from - "album_asset" - inner join "album" on "album"."id" = "album_asset"."albumId" - and "album"."deletedAt" is null - inner join "album_user" on "album_user"."albumId" = "album"."id" - and "album_user"."userId" = $4::uuid - where - "album_asset"."assetId" = "asset"."id" - ) - ) + inner join ( + select + "asset_face"."personGroupId", + count("asset"."id") as "faceCount" + from + "asset_face" + inner join "asset" on "asset"."id" = "asset_face"."assetId" + and "asset"."visibility" = 'timeline' + and "asset"."deletedAt" is null + and ( + "asset"."ownerId" = any ($3::uuid[]) + or exists ( + select + 1 as "exists" + from + "album_asset" + inner join "album" on "album"."id" = "album_asset"."albumId" + and "album"."deletedAt" is null + inner join "album_user" on "album_user"."albumId" = "album"."id" + and "album_user"."userId" = $4::uuid + where + "album_asset"."assetId" = "asset"."id" + ) + ) + where + "asset_face"."personGroupId" = any ( + array( + ( + select + "person"."personGroupId" + from + "person" + where + "person"."ownerId" = $5 + ) + ) + ) + and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true + group by + "asset_face"."personGroupId" + ) as "faces" on "faces"."personGroupId" = "person_group"."id" where - 1 = 1 - group by - "person_group"."id", - "owned"."ownerId", - "owned"."personGroupId" - having ( - "owned"."name" != $5 - or count("asset"."id") >= COALESCE( + "owned"."name" != $6 + or "faces"."faceCount" >= COALESCE( ( SELECT value -> 'people' ->> 'minimumFaces' FROM user_metadata WHERE - "userId" = $6 + "userId" = $7 AND key = 'preferences' ), '3' )::int ) + and 1 = 1 ) as "people" -- PersonRepository.createGroup diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 9b9235b8b..d6378d6ec 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -224,8 +224,6 @@ const asPerson = ({ ownedPerson, otherPeople, sharedBy, sharedWith }: PersonGrou sharedWith, }); -const faceCount = (eb: ExpressionBuilder) => eb.fn.count('asset.id'); - const withPerson = ({ viewingUserId }: WithPersonOptions) => { return (eb: ExpressionBuilder) => jsonObjectFrom( @@ -448,25 +446,40 @@ export class PersonRepository { .innerJoin('person as owned', (join) => join.onRef('owned.personGroupId', '=', 'person_group.id').on('owned.ownerId', '=', userId), ) - .innerJoin('asset_face', (join) => - join - .onRef('asset_face.personGroupId', '=', 'person_group.id') - .on('asset_face.deletedAt', 'is', null) - .on('asset_face.isVisible', 'is', true), + .innerJoin( + (eb) => + eb + .selectFrom('asset_face') + .innerJoin('asset', (join) => + join + .onRef('asset.id', '=', 'asset_face.assetId') + .on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) + .on('asset.deletedAt', 'is', null) + .on((eb) => + eb.or([eb('asset.ownerId', '=', anyUuid([userId, ...partnerIds])), inSharedAlbum(eb, userId)]), + ), + ) + .select('asset_face.personGroupId') + .select((eb) => eb.fn.count('asset.id').as('faceCount')) + // an array instead of a join or `in`, which the planner may turn into a scan of every face in the instance + .where('asset_face.personGroupId', '=', (eb) => { + const ownedGroupIds = eb + .selectFrom('person') + .select('person.personGroupId') + .where('person.ownerId', '=', userId); + return eb.fn.any(eb.fn('array', [ownedGroupIds])); + }) + .where('asset_face.deletedAt', 'is', null) + .where('asset_face.isVisible', 'is', true) + .groupBy('asset_face.personGroupId') + .as('faces'), + (join) => join.onRef('faces.personGroupId', '=', 'person_group.id'), ) - .innerJoin('asset', (join) => - join - .onRef('asset.id', '=', 'asset_face.assetId') - .on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) - .on('asset.deletedAt', 'is', null) - .on((eb) => eb.or([eb('asset.ownerId', '=', anyUuid([userId, ...partnerIds])), inSharedAlbum(eb, userId)])), - ) - .groupBy(['person_group.id', 'owned.ownerId', 'owned.personGroupId']) - .having((eb) => + .where((eb) => eb.or([ eb('owned.name', '!=', ''), eb( - faceCount, + 'faces.faceCount', '>=', sql`COALESCE( (SELECT value -> 'people' ->> 'minimumFaces' @@ -509,7 +522,7 @@ export class PersonRepository { .$if(!options.closestFaceAssetId, (qb) => qb .orderBy(sql`NULLIF("owned"."name", '') is null`, 'asc') - .orderBy(faceCount, 'desc') + .orderBy('faces.faceCount', 'desc') .orderBy(sql`NULLIF("owned"."name", '')`, (om) => om.asc().nullsLast()) .orderBy('owned.createdAt'), ) diff --git a/web/package.json b/web/package.json index 51853ea0e..985b9290c 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "3.3.1", + "version": "3.3.3", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": {