Compare commits
10
Commits
09bb2a9fb5
...
2c1f321627
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c1f321627 | ||
|
|
e8c1fd297d | ||
|
|
8e3e3a48ae | ||
|
|
dc5553b239 | ||
|
|
32e02a05d2 | ||
|
|
c581bce715 | ||
|
|
dd23264385 | ||
|
|
6dbe72b6a1 | ||
|
|
72f96701f0 | ||
|
|
b4ae7f0dcf |
@@ -35,6 +35,7 @@ jobs:
|
|||||||
filters: |
|
filters: |
|
||||||
docs:
|
docs:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
|
- 'deployment/**'
|
||||||
open-api:
|
open-api:
|
||||||
- 'open-api/immich-openapi-specs.json'
|
- 'open-api/immich-openapi-specs.json'
|
||||||
force-filters: |
|
force-filters: |
|
||||||
|
|||||||
+18
-28
@@ -2,34 +2,24 @@
|
|||||||
# Manual edits may be lost in future updates.
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||||
version = "4.52.10"
|
version = "5.28.1"
|
||||||
constraints = "4.52.10"
|
constraints = "5.28.1"
|
||||||
hashes = [
|
hashes = [
|
||||||
"h1:57oeYqz+pYEPAw2GvHARjcER0QYU0teS6DJzS26kCAY=",
|
"h1:9YL6g+iEfg3OzX6qSZOYsictVO0fNP09D6jRwIZKwQ0=",
|
||||||
"h1:6bnAgXSboppQbj79XmRtQLj/58d+b4EozydI8XrqwRE=",
|
"h1:GjOKfCmvcyE06fUp5WrAzfCLkTQaCg2Ko/kFlq8OfpM=",
|
||||||
"h1:6eP/jGOz1ioH17d3pygKhvsRa4AEKvedKqd+wgyiHYk=",
|
"h1:QTIohIPsVCqEmknrR2PdbVXyK1fjS805rMaZpEEpzFQ=",
|
||||||
"h1:84gJm57BBSvtfwt7t9ZiU7EKKDQARjsztoDuqB5DfqU=",
|
"h1:SFb952CVUFUoFjfor+GUqAV6DQ6bnUfaFcWL2yJlFBo=",
|
||||||
"h1:99tME9L/Ug/dXElX3Ac7Kx+KD7nhWEEwOTqaQubW18w=",
|
"h1:aso3s6IuIQju5L/OLrywCfQl7aA5TF+sb5pZzo8JG20=",
|
||||||
"h1:TnMGVaPpIH5NZX324v+y6j1iW3ym0MqqNekxHlzXT3A=",
|
"h1:cxGa9LRKCzwNacgnLjXnHUygkvAEd6pb8g8sRwzPSRE=",
|
||||||
"h1:Z1L5jI4GiJxAAsfNyZHU4MGTluWEjjKtlB8Xn6cA0Dg=",
|
"h1:kBW0qcDqNupOnsi7vvx/0TQRxXLtmDKAHwOwH5l03pc=",
|
||||||
"h1:dB/WsY27MLla/cwjYggolJhVeHUeS59rlOObAWK2e24=",
|
"h1:t6x20B8TpQAG0TsUlyWLrAWNO+V8JE8K3Ij0hW0feSU=",
|
||||||
"h1:gpffNocq9fHK+uQehZQK6YmL1aHeCGrVM1p53dcFP/0=",
|
"zh:219d464dd727c0e5c90a97b44aa5d7988acc4eec47f3684f7ecd006b17c46d9c",
|
||||||
"h1:jvfw1A2Uck4AjNmmWXwGH6P0sGQrnNY/KUVlqgyqYUM=",
|
"zh:2511f2380209c81121e6eb943d7e68d03602a4fcf4f0311dfc67e198179b9303",
|
||||||
"h1:ojvrl9uI1EPtAwKRHim9eSc4Lc88thU/2BFGdJLEwrc=",
|
"zh:2ae200482bf592cd703981c4d4fe87a52c0dfae81a70261295f59b1bb0d01a6c",
|
||||||
"h1:rJZJK06NYCcSZhY5fwsRUSznSmAmRWVg4sO0auvbURo=",
|
"zh:7df6fe6573379c2faf2e801fdbca58ac909266dbee604feaa993439b8616c9b3",
|
||||||
"h1:y2m6i2SnQ3ZxiJHlMWNLHJrEbNqQT6OpDGyhFP+Nmds=",
|
"zh:9fb5a295588351ce565515dc2a770047e8bab786a4a9665511ab9a9ca2db5f1b",
|
||||||
"zh:06c09cc50d6c61fb5f1ca79e4b22d5f383c6a848f885ed542dadef44ab4bfd55",
|
"zh:a182f4aec86c74e9db4affa22323c5a21f5d92c1fcf91b934570753d04f4daf8",
|
||||||
"zh:205bdfdac49d6b25ec2ca7ecffd39bf47126340e1943066bdbbd64d6d72577d2",
|
"zh:a39bb8624fb6c714e38bc0e632aae117a9fede5a18a248ab15dd1b60013c0324",
|
||||||
"zh:341ed688772d8e2bd9a148c57d56d5dfbe3d625bc50296a505cbae3d29694562",
|
"zh:bef2ffbdb68569549fe432b550592e07456759a7288f84f7c37c9d933a493f54",
|
||||||
"zh:3790619bae55558ab4d93f53686f7cf55f35138f800ea07b0b9c20cce75b9c0d",
|
|
||||||
"zh:44284a5b67e34f1381bbc47b6e765cee581037e25cd3b459caec069493b63898",
|
|
||||||
"zh:493a173f3d5245db82782e8c1aba63e34c6fcf6b952b9288926b519bfe9d9725",
|
|
||||||
"zh:4eb7846381fee6eefdb7e0aaf80153ed71d35547752f96987218a9429645844f",
|
|
||||||
"zh:850b1d1da770f48def40223d754ff65c31fabb1a7f334ed6ebb40eba09c41572",
|
|
||||||
"zh:9a29a08a9fd40d7c1beefe8597617c2fc475d62ec6d5ca8fcc2cd1a056a67a2e",
|
|
||||||
"zh:9e57472f0a56863204f5b2eef125d4fb1222eca793c6a5ac9f4df6c1776edc48",
|
|
||||||
"zh:a9eff98931da596c2d335359841bfd668f4b6cde257e15546a558c7f5d72773b",
|
|
||||||
"zh:c9293fb3390c79e4f5072e019c0a2c4c7408a55a55e78ae37f739a1da75886ef",
|
|
||||||
"zh:c9e98abec7f7a1e2669dd9c065896217125d774640e4673c1303a942736b3eb4",
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
cloudflare = {
|
cloudflare = {
|
||||||
source = "cloudflare/cloudflare"
|
source = "cloudflare/cloudflare"
|
||||||
version = "4.52.10"
|
version = "5.28.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-28
@@ -2,34 +2,24 @@
|
|||||||
# Manual edits may be lost in future updates.
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||||
version = "4.52.10"
|
version = "5.28.1"
|
||||||
constraints = "4.52.10"
|
constraints = "5.28.1"
|
||||||
hashes = [
|
hashes = [
|
||||||
"h1:57oeYqz+pYEPAw2GvHARjcER0QYU0teS6DJzS26kCAY=",
|
"h1:9YL6g+iEfg3OzX6qSZOYsictVO0fNP09D6jRwIZKwQ0=",
|
||||||
"h1:6bnAgXSboppQbj79XmRtQLj/58d+b4EozydI8XrqwRE=",
|
"h1:GjOKfCmvcyE06fUp5WrAzfCLkTQaCg2Ko/kFlq8OfpM=",
|
||||||
"h1:6eP/jGOz1ioH17d3pygKhvsRa4AEKvedKqd+wgyiHYk=",
|
"h1:QTIohIPsVCqEmknrR2PdbVXyK1fjS805rMaZpEEpzFQ=",
|
||||||
"h1:84gJm57BBSvtfwt7t9ZiU7EKKDQARjsztoDuqB5DfqU=",
|
"h1:SFb952CVUFUoFjfor+GUqAV6DQ6bnUfaFcWL2yJlFBo=",
|
||||||
"h1:99tME9L/Ug/dXElX3Ac7Kx+KD7nhWEEwOTqaQubW18w=",
|
"h1:aso3s6IuIQju5L/OLrywCfQl7aA5TF+sb5pZzo8JG20=",
|
||||||
"h1:TnMGVaPpIH5NZX324v+y6j1iW3ym0MqqNekxHlzXT3A=",
|
"h1:cxGa9LRKCzwNacgnLjXnHUygkvAEd6pb8g8sRwzPSRE=",
|
||||||
"h1:Z1L5jI4GiJxAAsfNyZHU4MGTluWEjjKtlB8Xn6cA0Dg=",
|
"h1:kBW0qcDqNupOnsi7vvx/0TQRxXLtmDKAHwOwH5l03pc=",
|
||||||
"h1:dB/WsY27MLla/cwjYggolJhVeHUeS59rlOObAWK2e24=",
|
"h1:t6x20B8TpQAG0TsUlyWLrAWNO+V8JE8K3Ij0hW0feSU=",
|
||||||
"h1:gpffNocq9fHK+uQehZQK6YmL1aHeCGrVM1p53dcFP/0=",
|
"zh:219d464dd727c0e5c90a97b44aa5d7988acc4eec47f3684f7ecd006b17c46d9c",
|
||||||
"h1:jvfw1A2Uck4AjNmmWXwGH6P0sGQrnNY/KUVlqgyqYUM=",
|
"zh:2511f2380209c81121e6eb943d7e68d03602a4fcf4f0311dfc67e198179b9303",
|
||||||
"h1:ojvrl9uI1EPtAwKRHim9eSc4Lc88thU/2BFGdJLEwrc=",
|
"zh:2ae200482bf592cd703981c4d4fe87a52c0dfae81a70261295f59b1bb0d01a6c",
|
||||||
"h1:rJZJK06NYCcSZhY5fwsRUSznSmAmRWVg4sO0auvbURo=",
|
"zh:7df6fe6573379c2faf2e801fdbca58ac909266dbee604feaa993439b8616c9b3",
|
||||||
"h1:y2m6i2SnQ3ZxiJHlMWNLHJrEbNqQT6OpDGyhFP+Nmds=",
|
"zh:9fb5a295588351ce565515dc2a770047e8bab786a4a9665511ab9a9ca2db5f1b",
|
||||||
"zh:06c09cc50d6c61fb5f1ca79e4b22d5f383c6a848f885ed542dadef44ab4bfd55",
|
"zh:a182f4aec86c74e9db4affa22323c5a21f5d92c1fcf91b934570753d04f4daf8",
|
||||||
"zh:205bdfdac49d6b25ec2ca7ecffd39bf47126340e1943066bdbbd64d6d72577d2",
|
"zh:a39bb8624fb6c714e38bc0e632aae117a9fede5a18a248ab15dd1b60013c0324",
|
||||||
"zh:341ed688772d8e2bd9a148c57d56d5dfbe3d625bc50296a505cbae3d29694562",
|
"zh:bef2ffbdb68569549fe432b550592e07456759a7288f84f7c37c9d933a493f54",
|
||||||
"zh:3790619bae55558ab4d93f53686f7cf55f35138f800ea07b0b9c20cce75b9c0d",
|
|
||||||
"zh:44284a5b67e34f1381bbc47b6e765cee581037e25cd3b459caec069493b63898",
|
|
||||||
"zh:493a173f3d5245db82782e8c1aba63e34c6fcf6b952b9288926b519bfe9d9725",
|
|
||||||
"zh:4eb7846381fee6eefdb7e0aaf80153ed71d35547752f96987218a9429645844f",
|
|
||||||
"zh:850b1d1da770f48def40223d754ff65c31fabb1a7f334ed6ebb40eba09c41572",
|
|
||||||
"zh:9a29a08a9fd40d7c1beefe8597617c2fc475d62ec6d5ca8fcc2cd1a056a67a2e",
|
|
||||||
"zh:9e57472f0a56863204f5b2eef125d4fb1222eca793c6a5ac9f4df6c1776edc48",
|
|
||||||
"zh:a9eff98931da596c2d335359841bfd668f4b6cde257e15546a558c7f5d72773b",
|
|
||||||
"zh:c9293fb3390c79e4f5072e019c0a2c4c7408a55a55e78ae37f739a1da75886ef",
|
|
||||||
"zh:c9e98abec7f7a1e2669dd9c065896217125d774640e4673c1303a942736b3eb4",
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
cloudflare = {
|
cloudflare = {
|
||||||
source = "cloudflare/cloudflare"
|
source = "cloudflare/cloudflare"
|
||||||
version = "4.52.10"
|
version = "5.28.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ class TimelineFactory {
|
|||||||
|
|
||||||
TimelineService place(String place) => TimelineService(_timelineRepository.place(place, groupBy));
|
TimelineService place(String place) => TimelineService(_timelineRepository.place(place, groupBy));
|
||||||
|
|
||||||
TimelineService person(List<String> userIds, String personId) =>
|
TimelineService person(String userId, String personId) =>
|
||||||
TimelineService(_timelineRepository.person(userIds, personId, groupBy));
|
TimelineService(_timelineRepository.person(userId, personId, groupBy));
|
||||||
|
|
||||||
TimelineService fromAssets(List<BaseAsset> assets, TimelineOrigin type) =>
|
TimelineService fromAssets(List<BaseAsset> assets, TimelineOrigin type) =>
|
||||||
TimelineService(_timelineRepository.fromAssets(assets, type));
|
TimelineService(_timelineRepository.fromAssets(assets, type));
|
||||||
|
|||||||
@@ -366,10 +366,10 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
|
|||||||
origin: TimelineOrigin.place,
|
origin: TimelineOrigin.place,
|
||||||
);
|
);
|
||||||
|
|
||||||
TimelineQuery person(List<String> userIds, String personId, GroupAssetsBy groupBy) => (
|
TimelineQuery person(String userId, String personId, GroupAssetsBy groupBy) => (
|
||||||
bucketSource: () => _watchPersonBucket(userIds, personId, groupBy: groupBy),
|
bucketSource: () => _watchPersonBucket(userId, personId, groupBy: groupBy),
|
||||||
assetSource: (offset, count) =>
|
assetSource: (offset, count) =>
|
||||||
_getPersonBucketAssets(userIds, personId, groupBy: groupBy, offset: offset, count: count),
|
_getPersonBucketAssets(userId, personId, groupBy: groupBy, offset: offset, count: count),
|
||||||
origin: TimelineOrigin.person,
|
origin: TimelineOrigin.person,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -430,7 +430,7 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
|
|||||||
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
|
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression<bool> _personAssetFilter(List<String> userIds, String personId) {
|
Expression<bool> _personAssetFilter(String userId, String personId) {
|
||||||
final faceAssetIds = _db.assetFaceEntity.selectOnly()
|
final faceAssetIds = _db.assetFaceEntity.selectOnly()
|
||||||
..addColumns([_db.assetFaceEntity.assetId])
|
..addColumns([_db.assetFaceEntity.assetId])
|
||||||
..where(
|
..where(
|
||||||
@@ -438,22 +438,23 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
|
|||||||
_db.assetFaceEntity.isVisible.equals(true) &
|
_db.assetFaceEntity.isVisible.equals(true) &
|
||||||
_db.assetFaceEntity.deletedAt.isNull(),
|
_db.assetFaceEntity.deletedAt.isNull(),
|
||||||
);
|
);
|
||||||
|
final partnerIds = _db.partnerEntity.selectOnly()
|
||||||
|
..addColumns([_db.partnerEntity.sharedById])
|
||||||
|
..where(_db.partnerEntity.sharedWithId.equals(userId));
|
||||||
final albumAssetIds = _db.remoteAlbumAssetEntity.selectOnly()..addColumns([_db.remoteAlbumAssetEntity.assetId]);
|
final albumAssetIds = _db.remoteAlbumAssetEntity.selectOnly()..addColumns([_db.remoteAlbumAssetEntity.assetId]);
|
||||||
return _db.remoteAssetEntity.id.isInQuery(faceAssetIds) &
|
return _db.remoteAssetEntity.id.isInQuery(faceAssetIds) &
|
||||||
(_db.remoteAssetEntity.ownerId.isIn(userIds) | _db.remoteAssetEntity.id.isInQuery(albumAssetIds)) &
|
(_db.remoteAssetEntity.ownerId.equals(userId) |
|
||||||
|
_db.remoteAssetEntity.ownerId.isInQuery(partnerIds) |
|
||||||
|
_db.remoteAssetEntity.id.isInQuery(albumAssetIds)) &
|
||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline);
|
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Bucket>> _watchPersonBucket(
|
Stream<List<Bucket>> _watchPersonBucket(String userId, String personId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
|
||||||
List<String> userIds,
|
|
||||||
String personId, {
|
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
|
||||||
}) {
|
|
||||||
if (groupBy == GroupAssetsBy.none) {
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
..addColumns([_db.remoteAssetEntity.id.count()])
|
..addColumns([_db.remoteAssetEntity.id.count()])
|
||||||
..where(_personAssetFilter(userIds, personId));
|
..where(_personAssetFilter(userId, personId));
|
||||||
|
|
||||||
return query.map((row) {
|
return query.map((row) {
|
||||||
final count = row.read(_db.remoteAssetEntity.id.count())!;
|
final count = row.read(_db.remoteAssetEntity.id.count())!;
|
||||||
@@ -466,7 +467,7 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
|
|||||||
|
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
..addColumns([assetCountExp, dateExp])
|
..addColumns([assetCountExp, dateExp])
|
||||||
..where(_personAssetFilter(userIds, personId))
|
..where(_personAssetFilter(userId, personId))
|
||||||
..groupBy([dateExp])
|
..groupBy([dateExp])
|
||||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
@@ -478,14 +479,14 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<BaseAsset>> _getPersonBucketAssets(
|
Future<List<BaseAsset>> _getPersonBucketAssets(
|
||||||
List<String> userIds,
|
String userId,
|
||||||
String personId, {
|
String personId, {
|
||||||
required int offset,
|
required int offset,
|
||||||
required int count,
|
required int count,
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
}) {
|
}) {
|
||||||
final query = _db.remoteAssetEntity.select()
|
final query = _db.remoteAssetEntity.select()
|
||||||
..where((_) => _personAssetFilter(userIds, personId))
|
..where((_) => _personAssetFilter(userId, personId))
|
||||||
..orderBy(_assetDateOrder(groupBy))
|
..orderBy(_assetDateOrder(groupBy))
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ class _PersonPageState extends ConsumerState<PersonPage> {
|
|||||||
throw Exception('User must be logged in to view person timeline');
|
throw Exception('User must be logged in to view person timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
final users = ref.watch(timelineUsersProvider).valueOrNull ?? [user.id];
|
final timelineService = ref.watch(timelineFactoryProvider).person(user.id, _person.id);
|
||||||
final timelineService = ref.watch(timelineFactoryProvider).person(users, _person.id);
|
|
||||||
ref.onDispose(timelineService.dispose);
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
}),
|
}),
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 3.3.5-local.12+3030504
|
version: 3.3.5-local.13+3030505
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.12.0 <4.0.0'
|
sdk: '>=3.12.0 <4.0.0'
|
||||||
|
|||||||
@@ -103,6 +103,22 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('person assets', () {
|
group('person assets', () {
|
||||||
|
test('includes partner assets even when the partner is not shown in the timeline', () async {
|
||||||
|
final user = await ctx.newUser();
|
||||||
|
final partner = await ctx.newUser();
|
||||||
|
final stranger = await ctx.newUser();
|
||||||
|
await ctx.newPartner(sharedById: partner.id, sharedWithId: user.id, inTimeline: false);
|
||||||
|
|
||||||
|
final person = await ctx.newPerson(ownerId: user.id);
|
||||||
|
final partnerAsset = await ctx.newRemoteAsset(ownerId: partner.id);
|
||||||
|
final strangerAsset = await ctx.newRemoteAsset(ownerId: stranger.id);
|
||||||
|
await ctx.newFace(assetId: partnerAsset.id, personId: person.id);
|
||||||
|
await ctx.newFace(assetId: strangerAsset.id, personId: person.id);
|
||||||
|
|
||||||
|
final assets = await sut.person(user.id, person.id, .day).assetSource(0, 10);
|
||||||
|
expect(assets.map((a) => (a as RemoteAsset).id), [partnerAsset.id]);
|
||||||
|
});
|
||||||
|
|
||||||
test('does not duplicate an asset that has multiple face records for the same person', () async {
|
test('does not duplicate an asset that has multiple face records for the same person', () async {
|
||||||
// Regression check for #26723: an INNER JOIN between remote_asset_entity and asset_face_entity
|
// Regression check for #26723: an INNER JOIN between remote_asset_entity and asset_face_entity
|
||||||
// fanned out one asset into N rows when N face records pointed at the same (asset, person) pair
|
// fanned out one asset into N rows when N face records pointed at the same (asset, person) pair
|
||||||
@@ -113,7 +129,7 @@ void main() {
|
|||||||
await ctx.newFace(assetId: asset.id, personId: person.id);
|
await ctx.newFace(assetId: asset.id, personId: person.id);
|
||||||
await ctx.newFace(assetId: asset.id, personId: person.id);
|
await ctx.newFace(assetId: asset.id, personId: person.id);
|
||||||
|
|
||||||
final query = sut.person([user.id], person.id, .day);
|
final query = sut.person(user.id, person.id, .day);
|
||||||
|
|
||||||
final buckets = await query.bucketSource().first;
|
final buckets = await query.bucketSource().first;
|
||||||
expect(buckets, hasLength(1));
|
expect(buckets, hasLength(1));
|
||||||
@@ -140,7 +156,7 @@ void main() {
|
|||||||
await ctx.newFace(assetId: shiftedLater.id, personId: person.id);
|
await ctx.newFace(assetId: shiftedLater.id, personId: person.id);
|
||||||
await ctx.newFace(assetId: shiftedEarlier.id, personId: person.id);
|
await ctx.newFace(assetId: shiftedEarlier.id, personId: person.id);
|
||||||
|
|
||||||
final query = sut.person([user.id], person.id, .day);
|
final query = sut.person(user.id, person.id, .day);
|
||||||
|
|
||||||
final buckets = await query.bucketSource().first;
|
final buckets = await query.bucketSource().first;
|
||||||
expect(buckets, hasLength(2));
|
expect(buckets, hasLength(2));
|
||||||
|
|||||||
@@ -6343,7 +6343,7 @@
|
|||||||
"state": "Added"
|
"state": "Added"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"x-immich-permission": "clusterGroup.read"
|
"x-immich-permission": "clusterGroup.admin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/cluster-groups/{id}/requests": {
|
"/cluster-groups/{id}/requests": {
|
||||||
@@ -25811,6 +25811,7 @@
|
|||||||
"backup.delete",
|
"backup.delete",
|
||||||
"clusterGroup.read",
|
"clusterGroup.read",
|
||||||
"clusterGroup.leave",
|
"clusterGroup.leave",
|
||||||
|
"clusterGroup.admin",
|
||||||
"clusterGroupRequest.create",
|
"clusterGroupRequest.create",
|
||||||
"clusterGroupRequest.read",
|
"clusterGroupRequest.read",
|
||||||
"clusterGroupRequest.delete",
|
"clusterGroupRequest.delete",
|
||||||
@@ -28466,11 +28467,13 @@
|
|||||||
"ServerFeatureLevel": {
|
"ServerFeatureLevel": {
|
||||||
"description": "Server feature level",
|
"description": "Server feature level",
|
||||||
"enum": [
|
"enum": [
|
||||||
1
|
1,
|
||||||
|
2
|
||||||
],
|
],
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"FeatureLevel"
|
"FeatureLevel",
|
||||||
|
"Passkeys"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ServerFeaturesDto": {
|
"ServerFeaturesDto": {
|
||||||
|
|||||||
@@ -407,7 +407,7 @@
|
|||||||
"matchType": {
|
"matchType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Match type",
|
"title": "Match type",
|
||||||
"enum": ["contains", "startsWith", "exact", "regex"],
|
"enum": ["contains", "startsWith", "exact", "regex", "present", "missing"],
|
||||||
"default": "contains",
|
"default": "contains",
|
||||||
"description": "Type of pattern matching to perform",
|
"description": "Type of pattern matching to perform",
|
||||||
"uiHint": {
|
"uiHint": {
|
||||||
@@ -424,7 +424,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["property", "pattern"]
|
"required": ["property"]
|
||||||
},
|
},
|
||||||
"uiHints": ["Filter"]
|
"uiHints": ["Filter"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,13 +3,18 @@ import { AssetVisibility } from '@immich/sdk';
|
|||||||
import type { Manifest } from '../dist/index.d.ts';
|
import type { Manifest } from '../dist/index.d.ts';
|
||||||
|
|
||||||
type MatchValueConfig = {
|
type MatchValueConfig = {
|
||||||
pattern: string;
|
pattern?: string;
|
||||||
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith';
|
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith' | 'missing' | 'present';
|
||||||
caseSensitive?: boolean;
|
caseSensitive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const matchValueResult = (value: string, config: MatchValueConfig) => {
|
const matchValueResult = (value: string | null, config: MatchValueConfig) => {
|
||||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
const { pattern = '', matchType = 'contains', caseSensitive = false } = config;
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return { workflow: { continue: matchType === 'missing' } };
|
||||||
|
}
|
||||||
|
|
||||||
const searchName = caseSensitive ? value : value.toLowerCase();
|
const searchName = caseSensitive ? value : value.toLowerCase();
|
||||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||||
|
|
||||||
@@ -32,6 +37,15 @@ const matchValueResult = (value: string, config: MatchValueConfig) => {
|
|||||||
return { workflow: { continue: regex.test(value) } };
|
return { workflow: { continue: regex.test(value) } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'present': {
|
||||||
|
return { workflow: { continue: true } };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'missing': {
|
||||||
|
// value is not null at this point
|
||||||
|
return { workflow: { continue: false } };
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -135,11 +149,10 @@ const methods = wrapper<Manifest>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
assetExifFilter: ({ config, data }) => {
|
assetExifFilter: ({ config, data }) => {
|
||||||
if (!data.asset.exifInfo || data.asset.exifInfo[config.property] === null) {
|
let value = data.asset.exifInfo?.[config.property];
|
||||||
return { workflow: { continue: false } };
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchValueResult(String(data.asset.exifInfo[config.property]), config);
|
// Empty EXIF value is treated as missing to mirror behavior of the web UI
|
||||||
|
return matchValueResult(value === null || value === undefined || value === '' ? null : String(value), config);
|
||||||
},
|
},
|
||||||
|
|
||||||
assetDateFilter: ({ config, data }) => {
|
assetDateFilter: ({ config, data }) => {
|
||||||
|
|||||||
@@ -8351,6 +8351,7 @@ export enum Permission {
|
|||||||
BackupDelete = "backup.delete",
|
BackupDelete = "backup.delete",
|
||||||
ClusterGroupRead = "clusterGroup.read",
|
ClusterGroupRead = "clusterGroup.read",
|
||||||
ClusterGroupLeave = "clusterGroup.leave",
|
ClusterGroupLeave = "clusterGroup.leave",
|
||||||
|
ClusterGroupAdmin = "clusterGroup.admin",
|
||||||
ClusterGroupRequestCreate = "clusterGroupRequest.create",
|
ClusterGroupRequestCreate = "clusterGroupRequest.create",
|
||||||
ClusterGroupRequestRead = "clusterGroupRequest.read",
|
ClusterGroupRequestRead = "clusterGroupRequest.read",
|
||||||
ClusterGroupRequestDelete = "clusterGroupRequest.delete",
|
ClusterGroupRequestDelete = "clusterGroupRequest.delete",
|
||||||
@@ -8823,7 +8824,8 @@ export enum ReleaseType {
|
|||||||
Prerelease = "prerelease"
|
Prerelease = "prerelease"
|
||||||
}
|
}
|
||||||
export enum ServerFeatureLevel {
|
export enum ServerFeatureLevel {
|
||||||
FeatureLevel = 1
|
FeatureLevel = 1,
|
||||||
|
Passkeys = 2
|
||||||
}
|
}
|
||||||
export enum UserMetadataKey {
|
export enum UserMetadataKey {
|
||||||
Preferences = "preferences",
|
Preferences = "preferences",
|
||||||
|
|||||||
Generated
+460
-445
File diff suppressed because it is too large
Load Diff
@@ -94,7 +94,7 @@ export class ClusterGroupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/regenerate-people')
|
@Post(':id/regenerate-people')
|
||||||
@Authenticated({ permission: Permission.ClusterGroupRead })
|
@Authenticated({ permission: Permission.ClusterGroupAdmin })
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
@Endpoint({
|
@Endpoint({
|
||||||
summary: 'Regenerate people of users in cluster group',
|
summary: 'Regenerate people of users in cluster group',
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ export enum Permission {
|
|||||||
|
|
||||||
ClusterGroupRead = 'clusterGroup.read',
|
ClusterGroupRead = 'clusterGroup.read',
|
||||||
ClusterGroupLeave = 'clusterGroup.leave',
|
ClusterGroupLeave = 'clusterGroup.leave',
|
||||||
|
ClusterGroupAdmin = 'clusterGroup.admin',
|
||||||
ClusterGroupRequestCreate = 'clusterGroupRequest.create',
|
ClusterGroupRequestCreate = 'clusterGroupRequest.create',
|
||||||
ClusterGroupRequestRead = 'clusterGroupRequest.read',
|
ClusterGroupRequestRead = 'clusterGroupRequest.read',
|
||||||
ClusterGroupRequestDelete = 'clusterGroupRequest.delete',
|
ClusterGroupRequestDelete = 'clusterGroupRequest.delete',
|
||||||
@@ -1340,6 +1341,7 @@ export const SearchOrderFieldSchema = z.enum(SearchOrderField).meta({ id: 'Searc
|
|||||||
|
|
||||||
export enum ServerFeatureLevel {
|
export enum ServerFeatureLevel {
|
||||||
FeatureLevel = 1,
|
FeatureLevel = 1,
|
||||||
|
Passkeys = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServerFeatureLevelSchema = z
|
export const ServerFeatureLevelSchema = z
|
||||||
|
|||||||
@@ -38,7 +38,48 @@ select
|
|||||||
and "asset_file"."type" = $1
|
and "asset_file"."type" = $1
|
||||||
) as agg
|
) as agg
|
||||||
) as "files",
|
) as "files",
|
||||||
to_json("asset_exif") as "exifInfo"
|
(
|
||||||
|
select
|
||||||
|
to_json(obj)
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"asset_exif"."assetId",
|
||||||
|
"asset_exif"."autoStackId",
|
||||||
|
"asset_exif"."bitsPerSample",
|
||||||
|
"asset_exif"."city",
|
||||||
|
"asset_exif"."colorspace",
|
||||||
|
"asset_exif"."country",
|
||||||
|
"asset_exif"."dateTimeOriginal",
|
||||||
|
"asset_exif"."description",
|
||||||
|
"asset_exif"."exifImageHeight",
|
||||||
|
"asset_exif"."exifImageWidth",
|
||||||
|
"asset_exif"."exposureTime",
|
||||||
|
"asset_exif"."fileSizeInByte",
|
||||||
|
"asset_exif"."fNumber",
|
||||||
|
"asset_exif"."focalLength",
|
||||||
|
"asset_exif"."fps",
|
||||||
|
"asset_exif"."iso",
|
||||||
|
"asset_exif"."latitude",
|
||||||
|
"asset_exif"."lensModel",
|
||||||
|
"asset_exif"."livePhotoCID",
|
||||||
|
"asset_exif"."longitude",
|
||||||
|
"asset_exif"."make",
|
||||||
|
"asset_exif"."model",
|
||||||
|
"asset_exif"."modifyDate",
|
||||||
|
"asset_exif"."orientation",
|
||||||
|
"asset_exif"."profileDescription",
|
||||||
|
"asset_exif"."projectionType",
|
||||||
|
"asset_exif"."rating",
|
||||||
|
"asset_exif"."state",
|
||||||
|
"asset_exif"."tags",
|
||||||
|
"asset_exif"."timeZone"
|
||||||
|
from
|
||||||
|
"asset_exif"
|
||||||
|
where
|
||||||
|
"asset_exif"."assetId" = "asset"."id"
|
||||||
|
) as obj
|
||||||
|
) as "exifInfo"
|
||||||
from
|
from
|
||||||
"asset"
|
"asset"
|
||||||
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||||
@@ -239,7 +280,48 @@ select
|
|||||||
"asset_edit"."assetId" = "asset"."id"
|
"asset_edit"."assetId" = "asset"."id"
|
||||||
) as agg
|
) as agg
|
||||||
) as "edits",
|
) as "edits",
|
||||||
to_json("asset_exif") as "exifInfo",
|
(
|
||||||
|
select
|
||||||
|
to_json(obj)
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"asset_exif"."assetId",
|
||||||
|
"asset_exif"."autoStackId",
|
||||||
|
"asset_exif"."bitsPerSample",
|
||||||
|
"asset_exif"."city",
|
||||||
|
"asset_exif"."colorspace",
|
||||||
|
"asset_exif"."country",
|
||||||
|
"asset_exif"."dateTimeOriginal",
|
||||||
|
"asset_exif"."description",
|
||||||
|
"asset_exif"."exifImageHeight",
|
||||||
|
"asset_exif"."exifImageWidth",
|
||||||
|
"asset_exif"."exposureTime",
|
||||||
|
"asset_exif"."fileSizeInByte",
|
||||||
|
"asset_exif"."fNumber",
|
||||||
|
"asset_exif"."focalLength",
|
||||||
|
"asset_exif"."fps",
|
||||||
|
"asset_exif"."iso",
|
||||||
|
"asset_exif"."latitude",
|
||||||
|
"asset_exif"."lensModel",
|
||||||
|
"asset_exif"."livePhotoCID",
|
||||||
|
"asset_exif"."longitude",
|
||||||
|
"asset_exif"."make",
|
||||||
|
"asset_exif"."model",
|
||||||
|
"asset_exif"."modifyDate",
|
||||||
|
"asset_exif"."orientation",
|
||||||
|
"asset_exif"."profileDescription",
|
||||||
|
"asset_exif"."projectionType",
|
||||||
|
"asset_exif"."rating",
|
||||||
|
"asset_exif"."state",
|
||||||
|
"asset_exif"."tags",
|
||||||
|
"asset_exif"."timeZone"
|
||||||
|
from
|
||||||
|
"asset_exif"
|
||||||
|
where
|
||||||
|
"asset_exif"."assetId" = "asset"."id"
|
||||||
|
) as obj
|
||||||
|
) as "exifInfo",
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
to_json(obj)
|
to_json(obj)
|
||||||
@@ -449,7 +531,48 @@ where
|
|||||||
select
|
select
|
||||||
"asset"."id",
|
"asset"."id",
|
||||||
"asset"."visibility",
|
"asset"."visibility",
|
||||||
to_json("asset_exif") as "exifInfo",
|
(
|
||||||
|
select
|
||||||
|
to_json(obj)
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"asset_exif"."assetId",
|
||||||
|
"asset_exif"."autoStackId",
|
||||||
|
"asset_exif"."bitsPerSample",
|
||||||
|
"asset_exif"."city",
|
||||||
|
"asset_exif"."colorspace",
|
||||||
|
"asset_exif"."country",
|
||||||
|
"asset_exif"."dateTimeOriginal",
|
||||||
|
"asset_exif"."description",
|
||||||
|
"asset_exif"."exifImageHeight",
|
||||||
|
"asset_exif"."exifImageWidth",
|
||||||
|
"asset_exif"."exposureTime",
|
||||||
|
"asset_exif"."fileSizeInByte",
|
||||||
|
"asset_exif"."fNumber",
|
||||||
|
"asset_exif"."focalLength",
|
||||||
|
"asset_exif"."fps",
|
||||||
|
"asset_exif"."iso",
|
||||||
|
"asset_exif"."latitude",
|
||||||
|
"asset_exif"."lensModel",
|
||||||
|
"asset_exif"."livePhotoCID",
|
||||||
|
"asset_exif"."longitude",
|
||||||
|
"asset_exif"."make",
|
||||||
|
"asset_exif"."model",
|
||||||
|
"asset_exif"."modifyDate",
|
||||||
|
"asset_exif"."orientation",
|
||||||
|
"asset_exif"."profileDescription",
|
||||||
|
"asset_exif"."projectionType",
|
||||||
|
"asset_exif"."rating",
|
||||||
|
"asset_exif"."state",
|
||||||
|
"asset_exif"."tags",
|
||||||
|
"asset_exif"."timeZone"
|
||||||
|
from
|
||||||
|
"asset_exif"
|
||||||
|
where
|
||||||
|
"asset_exif"."assetId" = "asset"."id"
|
||||||
|
) as obj
|
||||||
|
) as "exifInfo",
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
coalesce(json_agg(agg), '[]')
|
coalesce(json_agg(agg), '[]')
|
||||||
|
|||||||
@@ -134,7 +134,48 @@ select
|
|||||||
"asset"."type",
|
"asset"."type",
|
||||||
"asset"."width",
|
"asset"."width",
|
||||||
"asset"."height",
|
"asset"."height",
|
||||||
to_json("asset_exif") as "exifInfo"
|
(
|
||||||
|
select
|
||||||
|
to_json(obj)
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"asset_exif"."assetId",
|
||||||
|
"asset_exif"."autoStackId",
|
||||||
|
"asset_exif"."bitsPerSample",
|
||||||
|
"asset_exif"."city",
|
||||||
|
"asset_exif"."colorspace",
|
||||||
|
"asset_exif"."country",
|
||||||
|
"asset_exif"."dateTimeOriginal",
|
||||||
|
"asset_exif"."description",
|
||||||
|
"asset_exif"."exifImageHeight",
|
||||||
|
"asset_exif"."exifImageWidth",
|
||||||
|
"asset_exif"."exposureTime",
|
||||||
|
"asset_exif"."fileSizeInByte",
|
||||||
|
"asset_exif"."fNumber",
|
||||||
|
"asset_exif"."focalLength",
|
||||||
|
"asset_exif"."fps",
|
||||||
|
"asset_exif"."iso",
|
||||||
|
"asset_exif"."latitude",
|
||||||
|
"asset_exif"."lensModel",
|
||||||
|
"asset_exif"."livePhotoCID",
|
||||||
|
"asset_exif"."longitude",
|
||||||
|
"asset_exif"."make",
|
||||||
|
"asset_exif"."model",
|
||||||
|
"asset_exif"."modifyDate",
|
||||||
|
"asset_exif"."orientation",
|
||||||
|
"asset_exif"."profileDescription",
|
||||||
|
"asset_exif"."projectionType",
|
||||||
|
"asset_exif"."rating",
|
||||||
|
"asset_exif"."state",
|
||||||
|
"asset_exif"."tags",
|
||||||
|
"asset_exif"."timeZone"
|
||||||
|
from
|
||||||
|
"asset_exif"
|
||||||
|
where
|
||||||
|
"asset_exif"."assetId" = "asset"."id"
|
||||||
|
) as obj
|
||||||
|
) as "exifInfo"
|
||||||
from
|
from
|
||||||
"asset"
|
"asset"
|
||||||
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||||
@@ -1420,7 +1461,48 @@ offset
|
|||||||
|
|
||||||
-- SearchRepository.searchMetadataV3 (order-rating-withExif)
|
-- SearchRepository.searchMetadataV3 (order-rating-withExif)
|
||||||
select
|
select
|
||||||
to_json("asset_exif") as "exifInfo",
|
(
|
||||||
|
select
|
||||||
|
to_json(obj)
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"asset_exif"."assetId",
|
||||||
|
"asset_exif"."autoStackId",
|
||||||
|
"asset_exif"."bitsPerSample",
|
||||||
|
"asset_exif"."city",
|
||||||
|
"asset_exif"."colorspace",
|
||||||
|
"asset_exif"."country",
|
||||||
|
"asset_exif"."dateTimeOriginal",
|
||||||
|
"asset_exif"."description",
|
||||||
|
"asset_exif"."exifImageHeight",
|
||||||
|
"asset_exif"."exifImageWidth",
|
||||||
|
"asset_exif"."exposureTime",
|
||||||
|
"asset_exif"."fileSizeInByte",
|
||||||
|
"asset_exif"."fNumber",
|
||||||
|
"asset_exif"."focalLength",
|
||||||
|
"asset_exif"."fps",
|
||||||
|
"asset_exif"."iso",
|
||||||
|
"asset_exif"."latitude",
|
||||||
|
"asset_exif"."lensModel",
|
||||||
|
"asset_exif"."livePhotoCID",
|
||||||
|
"asset_exif"."longitude",
|
||||||
|
"asset_exif"."make",
|
||||||
|
"asset_exif"."model",
|
||||||
|
"asset_exif"."modifyDate",
|
||||||
|
"asset_exif"."orientation",
|
||||||
|
"asset_exif"."profileDescription",
|
||||||
|
"asset_exif"."projectionType",
|
||||||
|
"asset_exif"."rating",
|
||||||
|
"asset_exif"."state",
|
||||||
|
"asset_exif"."tags",
|
||||||
|
"asset_exif"."timeZone"
|
||||||
|
from
|
||||||
|
"asset_exif"
|
||||||
|
where
|
||||||
|
"asset_exif"."assetId" = "asset"."id"
|
||||||
|
) as obj
|
||||||
|
) as "exifInfo",
|
||||||
"asset"."id",
|
"asset"."id",
|
||||||
"asset"."updateId",
|
"asset"."updateId",
|
||||||
"asset"."createdAt",
|
"asset"."createdAt",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export class ClusterGroupService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async regeneratePeople(auth: AuthDto, id: string) {
|
async regeneratePeople(auth: AuthDto, id: string) {
|
||||||
await this.requireAccess({ auth, permission: Permission.ClusterGroupRead, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ClusterGroupAdmin, ids: [id] });
|
||||||
|
|
||||||
await this.jobRepository.queue({
|
await this.jobRepository.queue({
|
||||||
name: JobName.FacialRecognitionQueueAll,
|
name: JobName.FacialRecognitionQueueAll,
|
||||||
|
|||||||
@@ -356,6 +356,10 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
|||||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case Permission.ClusterGroupAdmin: {
|
||||||
|
return access.clusterGroup.checkOwnerAccess(auth.user.id, ids);
|
||||||
|
}
|
||||||
|
|
||||||
case Permission.ClusterGroupRead: {
|
case Permission.ClusterGroupRead: {
|
||||||
const isMember = await access.clusterGroup.checkOwnerAccess(auth.user.id, ids);
|
const isMember = await access.clusterGroup.checkOwnerAccess(auth.user.id, ids);
|
||||||
const isInvited = await access.clusterGroup.checkInviteAccess(auth.user.id, setDifference(ids, isMember));
|
const isInvited = await access.clusterGroup.checkInviteAccess(auth.user.id, setDifference(ids, isMember));
|
||||||
|
|||||||
@@ -123,10 +123,15 @@ export function withExif<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
|||||||
return qb.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId').select(selectExifInfo);
|
return qb.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId').select(selectExifInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectReturnedExifInfo = (eb: ExpressionBuilder<DB, 'asset'>) =>
|
||||||
|
jsonObjectFrom(eb.selectFrom('asset_exif').select(columns.exif).whereRef('asset_exif.assetId', '=', 'asset.id')).as(
|
||||||
|
'exifInfo',
|
||||||
|
);
|
||||||
|
|
||||||
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
||||||
return qb
|
return qb
|
||||||
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||||
.select((eb) => eb.fn.toJson(eb.table('asset_exif')).as('exifInfo'))
|
.select(selectReturnedExifInfo)
|
||||||
.$narrowType<{ exifInfo: NotNull }>();
|
.$narrowType<{ exifInfo: NotNull }>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,7 +821,7 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
|||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
// postgres eliminates the left join when no exif column is referenced, so unused joins are free
|
// postgres eliminates the left join when no exif column is referenced, so unused joins are free
|
||||||
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||||
.$if(!!options.withExif, (qb) => qb.select(selectExifInfo))
|
.$if(!!options.withExif, (qb) => qb.select(selectReturnedExifInfo))
|
||||||
.$if(scopeGlobally, (qb) => qb.where(ownershipPredicate))
|
.$if(scopeGlobally, (qb) => qb.where(ownershipPredicate))
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb.or([eb('asset.visibility', '!=', AssetVisibility.Locked), eb('asset.ownerId', '=', scope.lockedOwnerId)]),
|
eb.or([eb('asset.visibility', '!=', AssetVisibility.Locked), eb('asset.ownerId', '=', scope.lockedOwnerId)]),
|
||||||
|
|||||||
@@ -530,6 +530,112 @@ describe('core plugin', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('assetExifFilter', () => {
|
||||||
|
describe('missing', () => {
|
||||||
|
it('should favorite asset if it has no orientation', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||||
|
await ctx.newExif({ assetId: asset.id, orientation: null });
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetExifFilter',
|
||||||
|
config: { property: 'orientation', matchType: 'missing' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetFavorite',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id }),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not favorite asset if it has orientation', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||||
|
await ctx.newExif({ assetId: asset.id, orientation: '2' });
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetExifFilter',
|
||||||
|
config: { property: 'orientation', matchType: 'missing' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetFavorite',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id }),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('present', () => {
|
||||||
|
it('should favorite asset if it has orientation', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||||
|
await ctx.newExif({ assetId: asset.id, orientation: '2' });
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetExifFilter',
|
||||||
|
config: { property: 'orientation', matchType: 'present' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetFavorite',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id }),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not favorite asset if it has no orientation', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||||
|
await ctx.newExif({ assetId: asset.id, orientation: null });
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetExifFilter',
|
||||||
|
config: { property: 'orientation', matchType: 'present' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetFavorite',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id }),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('assetDateFilter', () => {
|
describe('assetDateFilter', () => {
|
||||||
it('should favorite assets created during the first 7 days of a specific year and month', async () => {
|
it('should favorite assets created during the first 7 days of a specific year and month', async () => {
|
||||||
const { user } = await ctx.newUser();
|
const { user } = await ctx.newUser();
|
||||||
|
|||||||
@@ -61,16 +61,14 @@
|
|||||||
currentMemories.flatMap((memory) => {
|
currentMemories.flatMap((memory) => {
|
||||||
const asset = memory.assets[frame % memory.assets.length];
|
const asset = memory.assets[frame % memory.assets.length];
|
||||||
return asset
|
return asset
|
||||||
? [
|
? {
|
||||||
{
|
id: memory.id,
|
||||||
id: memory.id,
|
title: $memoryLaneTitle(memory),
|
||||||
title: $memoryLaneTitle(memory),
|
href: Route.viewMemory({ id: memory.id, assetId: asset.id }),
|
||||||
href: Route.viewMemory({ id: memory.id, assetId: asset.id }),
|
alt: $t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(asset)) } }),
|
||||||
alt: $t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(asset)) } }),
|
src: getAssetMediaUrl({ id: asset.id }),
|
||||||
src: getAssetMediaUrl({ id: asset.id }),
|
type: memory.type,
|
||||||
type: memory.type,
|
}
|
||||||
},
|
|
||||||
]
|
|
||||||
: [];
|
: [];
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,5 +24,5 @@ export function updateObject(target: any, source: any): boolean {
|
|||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
export function isMismatched<T>(option: T | undefined, value: T): boolean {
|
export function isMismatched<T>(option: T | undefined, value: T): boolean {
|
||||||
return option === undefined ? false : option !== value;
|
return option !== undefined && option !== value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const getAdminConfigQuery = () =>
|
|||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ['config', 'admin'],
|
queryKey: ['config', 'admin'],
|
||||||
queryFn: ({ signal }) => getAdminConfig({ signal }),
|
queryFn: ({ signal }) => getAdminConfig({ signal }),
|
||||||
|
gcTime: Infinity,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getAdminConfigDefaultsQuery = () =>
|
export const getAdminConfigDefaultsQuery = () =>
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export const getPublicConfigQuery = () =>
|
|||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ['config', 'public'],
|
queryKey: ['config', 'public'],
|
||||||
queryFn: ({ signal }) => getPublicConfig({ signal }),
|
queryFn: ({ signal }) => getPublicConfig({ signal }),
|
||||||
|
gcTime: Infinity,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ export const getUserConfigQuery = () =>
|
|||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ['config', 'user'],
|
queryKey: ['config', 'user'],
|
||||||
queryFn: ({ signal }) => getUserConfig({ signal }),
|
queryFn: ({ signal }) => getUserConfig({ signal }),
|
||||||
|
gcTime: Infinity,
|
||||||
select: withEffectiveFlags,
|
select: withEffectiveFlags,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const getServerConfigQuery = () =>
|
|||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ['server', 'config'],
|
queryKey: ['server', 'config'],
|
||||||
queryFn: ({ signal }) => getServerConfig({ signal }),
|
queryFn: ({ signal }) => getServerConfig({ signal }),
|
||||||
|
gcTime: Infinity,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getServerStatisticsQuery = () =>
|
export const getServerStatisticsQuery = () =>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class SidebarStore {
|
|||||||
|
|
||||||
/** Toggles the sidebar visibility, if available at the current screen width. */
|
/** Toggles the sidebar visibility, if available at the current screen width. */
|
||||||
toggle() {
|
toggle() {
|
||||||
this.isOpen = mediaQueryManager.isFullSidebar ? true : !this.isOpen;
|
this.isOpen = mediaQueryManager.isFullSidebar || !this.isOpen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
|
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
|
||||||
import { CastDestinationType, CastState, type ICastDestination } from '#lib/managers/cast-manager.svelte.ts';
|
import { CastDestinationType, CastState, type ICastDestination } from '#lib/managers/cast-manager.svelte.ts';
|
||||||
import 'chromecast-caf-sender';
|
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
|
|
||||||
const FRAMEWORK_LINK = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
|
const FRAMEWORK_LINK = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ export const calculateFittedFontSize = (
|
|||||||
return clamp(fontSize, MIN_FONT_SIZE, MAX_FONT_SIZE);
|
return clamp(fontSize, MIN_FONT_SIZE, MAX_FONT_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line tscompat/tscompat -- This assigns the Canvas 2D font property, not a font() method.
|
|
||||||
context.font = getReferenceFont();
|
context.font = getReferenceFont();
|
||||||
|
|
||||||
let maxCharWidth = 0;
|
let maxCharWidth = 0;
|
||||||
@@ -146,7 +145,6 @@ export const calculateFittedFontSize = (
|
|||||||
return clamp((1.4 * fitWidth) / text.length, MIN_FONT_SIZE, MAX_FONT_SIZE);
|
return clamp((1.4 * fitWidth) / text.length, MIN_FONT_SIZE, MAX_FONT_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line tscompat/tscompat -- This assigns the Canvas 2D font property, not a font() method.
|
|
||||||
context.font = getReferenceFont();
|
context.font = getReferenceFont();
|
||||||
|
|
||||||
const metrics = context.measureText(text);
|
const metrics = context.measureText(text);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export async function acquireWakeLock() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line tscompat/tscompat -- The feature check above guards Wake Lock access.
|
|
||||||
if (sentinel && !sentinel.released) {
|
if (sentinel && !sentinel.released) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -20,7 +19,6 @@ export async function acquireWakeLock() {
|
|||||||
}
|
}
|
||||||
acquiring = true;
|
acquiring = true;
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line tscompat/tscompat -- Browsers without Wake Lock return before this call.
|
|
||||||
sentinel = await navigator.wakeLock.request('screen');
|
sentinel = await navigator.wakeLock.request('screen');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to acquire wake lock:', error);
|
console.warn('Failed to acquire wake lock:', error);
|
||||||
@@ -38,7 +36,6 @@ export async function releaseWakeLock() {
|
|||||||
// Unset first to avoid race condition after await
|
// Unset first to avoid race condition after await
|
||||||
sentinel = undefined;
|
sentinel = undefined;
|
||||||
|
|
||||||
// eslint-disable-next-line tscompat/tscompat -- A sentinel is only acquired on browsers supporting Wake Lock.
|
|
||||||
await toReleaseSentinel.release();
|
await toReleaseSentinel.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -226,7 +226,7 @@
|
|||||||
return { albumId, order: album.order };
|
return { albumId, order: album.order };
|
||||||
});
|
});
|
||||||
|
|
||||||
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 1);
|
const isShared = $derived(viewMode !== AlbumPageViewMode.SELECT_ASSETS && album.albumUsers.length > 1);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (assetViewerManager.isViewing || !isShared) {
|
if (assetViewerManager.isViewing || !isShared) {
|
||||||
@@ -248,10 +248,10 @@
|
|||||||
const isEditor = $derived(isAlbumEditor(album));
|
const isEditor = $derived(isAlbumEditor(album));
|
||||||
|
|
||||||
const isSelectionMode = $derived(
|
const isSelectionMode = $derived(
|
||||||
viewMode === AlbumPageViewMode.SELECT_ASSETS ? true : viewMode === AlbumPageViewMode.SELECT_THUMBNAIL,
|
viewMode === AlbumPageViewMode.SELECT_ASSETS || viewMode === AlbumPageViewMode.SELECT_THUMBNAIL,
|
||||||
);
|
);
|
||||||
const singleSelect = $derived(
|
const singleSelect = $derived(
|
||||||
viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : viewMode === AlbumPageViewMode.SELECT_THUMBNAIL,
|
viewMode !== AlbumPageViewMode.SELECT_ASSETS && viewMode === AlbumPageViewMode.SELECT_THUMBNAIL,
|
||||||
);
|
);
|
||||||
const showArchiveIcon = $derived(viewMode !== AlbumPageViewMode.SELECT_ASSETS);
|
const showArchiveIcon = $derived(viewMode !== AlbumPageViewMode.SELECT_ASSETS);
|
||||||
const onSelect = ({ id }: { id: string }) => {
|
const onSelect = ({ id }: { id: string }) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
let gCastEnabled = $state(authManager.authenticated ? authManager.preferences.cast.gCastEnabled : false);
|
let gCastEnabled = $state(authManager.authenticated && authManager.preferences.cast.gCastEnabled);
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+2
-3
@@ -5,14 +5,13 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"#i18n/*": ["../i18n/*"],
|
"#i18n/*": ["../i18n/*"],
|
||||||
"#test-data/*": ["./src/test-data/*"],
|
"#test-data/*": ["./src/test-data/*"],
|
||||||
"#tests/*": ["./src/../tests/*"],
|
"#tests/*": ["./src/../tests/*"]
|
||||||
"chromecast-caf-sender": ["./node_modules/@types/chromecast-caf-sender/index.d.ts"]
|
|
||||||
},
|
},
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "es2022",
|
"target": "es2022",
|
||||||
"types": ["$app/types", "vitest/globals"]
|
"types": ["$app/types", "vitest/globals", "chromecast-caf-sender"]
|
||||||
},
|
},
|
||||||
"exclude": ["vite.config.ts", "src/service-worker/*"],
|
"exclude": ["vite.config.ts", "src/service-worker/*"],
|
||||||
"extends": "$app/tsconfig",
|
"extends": "$app/tsconfig",
|
||||||
|
|||||||
Reference in New Issue
Block a user