Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,84 @@ public async Task LoadAssets_PersonHasNoAssets_DoesNotAffectOthers()
Assert.That(result.Count, Is.EqualTo(10));
Assert.That(result.All(a => a.Id.StartsWith("p1_")));
}

[Test]
public async Task LoadAssets_RequireAllPeople_IssuesSingleQueryWithAllPersonIds()
{
// Arrange
var person1Id = Guid.NewGuid();
var person2Id = Guid.NewGuid();
_mockAccountSettings.SetupGet(s => s.People).Returns(new List<Guid> { person1Id, person2Id });
_mockAccountSettings.SetupGet(s => s.RequireAllPeople).Returns(true);

var assets = Enumerable.Range(0, 5).Select(i => CreateAsset($"combined_{i}")).ToList();

_mockImmichApi.Setup(api => api.SearchAssetsAsync(
It.Is<MetadataSearchDto>(d =>
d.PersonIds.Contains(person1Id) &&
d.PersonIds.Contains(person2Id) &&
d.PersonIds.Count == 2),
It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateSearchResult(assets, 5));

// Act
var result = (await _personAssetsPool.TestLoadAssets()).ToList();

// Assert
Assert.That(result.Count, Is.EqualTo(5));
// Only one call was made (AND mode), not one per person
_mockImmichApi.Verify(api => api.SearchAssetsAsync(It.IsAny<MetadataSearchDto>(), It.IsAny<CancellationToken>()), Times.Once);
}

[Test]
public async Task LoadAssets_RequireAllPeople_Paginates()
{
// Arrange
var person1Id = Guid.NewGuid();
var person2Id = Guid.NewGuid();
_mockAccountSettings.SetupGet(s => s.People).Returns(new List<Guid> { person1Id, person2Id });
_mockAccountSettings.SetupGet(s => s.RequireAllPeople).Returns(true);

int batchSize = 1000;
var page1Assets = Enumerable.Range(0, batchSize).Select(i => CreateAsset($"a_{i}")).ToList();
var page2Assets = Enumerable.Range(0, 15).Select(i => CreateAsset($"b_{i}")).ToList();

_mockImmichApi.Setup(api => api.SearchAssetsAsync(
It.Is<MetadataSearchDto>(d => d.PersonIds.Contains(person1Id) && d.PersonIds.Contains(person2Id) && d.Page == 1),
It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateSearchResult(page1Assets, batchSize));
_mockImmichApi.Setup(api => api.SearchAssetsAsync(
It.Is<MetadataSearchDto>(d => d.PersonIds.Contains(person1Id) && d.PersonIds.Contains(person2Id) && d.Page == 2),
It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateSearchResult(page2Assets, 15));

// Act
var result = (await _personAssetsPool.TestLoadAssets()).ToList();

// Assert
Assert.That(result.Count, Is.EqualTo(batchSize + 15));
_mockImmichApi.Verify(api => api.SearchAssetsAsync(It.IsAny<MetadataSearchDto>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
}

[Test]
public async Task LoadAssets_RequireAllPeople_NoSharedAssets_ReturnsEmpty()
{
// Arrange: two people configured, but no asset features both of them
var person1Id = Guid.NewGuid();
var person2Id = Guid.NewGuid();
_mockAccountSettings.SetupGet(s => s.People).Returns(new List<Guid> { person1Id, person2Id });
_mockAccountSettings.SetupGet(s => s.RequireAllPeople).Returns(true);

_mockImmichApi.Setup(api => api.SearchAssetsAsync(
It.Is<MetadataSearchDto>(d => d.PersonIds.Contains(person1Id) && d.PersonIds.Contains(person2Id)),
It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateSearchResult(new List<AssetResponseDto>(), 0));

// Act
var result = (await _personAssetsPool.TestLoadAssets()).ToList();

// Assert
Assert.That(result, Is.Empty);
_mockImmichApi.Verify(api => api.SearchAssetsAsync(It.IsAny<MetadataSearchDto>(), It.IsAny<CancellationToken>()), Times.Once);
}
}
1 change: 1 addition & 0 deletions ImmichFrame.Core/Interfaces/IServerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public interface IAccountSettings
public List<Guid> Albums { get; }
public List<Guid> ExcludedAlbums { get; }
public List<Guid> People { get; }
public bool RequireAllPeople { get; }
public List<string> Tags { get; }
public int? Rating { get; }

Expand Down
13 changes: 10 additions & 3 deletions ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@ protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(Cancella
{
return personAssets;
}

foreach (var personId in people)

// AND mode: pass all person IDs in a single query so the API returns only
// assets that feature every person in the list.
// OR mode (default): query each person separately and combine results.
var personIdGroups = accountSettings.RequireAllPeople
? [people]
: people.Select(id => (IList<Guid>)[id]);

foreach (var personIds in personIdGroups)
{
int page = 1;
int batchSize = 1000;
Expand All @@ -26,7 +33,7 @@ protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(Cancella
{
Page = page,
Size = batchSize,
PersonIds = [personId],
PersonIds = personIds,
WithExif = true,
WithPeople = true
};
Expand Down
1 change: 1 addition & 0 deletions ImmichFrame.WebApi.Tests/Resources/TestV1.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"People": [
"00000000-0000-0000-0000-000000000001"
],
"RequireAllPeople": true,
"Tags": [
"Tags_TEST"
],
Expand Down
2 changes: 2 additions & 0 deletions ImmichFrame.WebApi.Tests/Resources/TestV2.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"People": [
"00000000-0000-0000-0000-000000000001"
],
"RequireAllPeople": true,
"Tags": [
"Account1.Tags_TEST"
]
Expand All @@ -84,6 +85,7 @@
"People": [
"00000000-0000-0000-0000-000000000001"
],
"RequireAllPeople": true,
"Tags": [
"Account2.Tags_TEST"
]
Expand Down
2 changes: 2 additions & 0 deletions ImmichFrame.WebApi.Tests/Resources/TestV2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Accounts:
- 00000000-0000-0000-0000-000000000001
People:
- 00000000-0000-0000-0000-000000000001
RequireAllPeople: true
Tags:
- Account1.Tags_TEST
- ImmichServerUrl: Account2.ImmichServerUrl_TEST
Expand All @@ -72,5 +73,6 @@ Accounts:
- 00000000-0000-0000-0000-000000000001
People:
- 00000000-0000-0000-0000-000000000001
RequireAllPeople: true
Tags:
- Account2.Tags_TEST
3 changes: 2 additions & 1 deletion ImmichFrame.WebApi.Tests/Resources/TestV2_NoGeneral.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
],
"People": [
"00000000-0000-0000-0000-000000000001"
]
],
"RequireAllPeople": true
},
{
"ImmichServerUrl": "Account2.ImmichServerUrl_TEST",
Expand Down
2 changes: 2 additions & 0 deletions ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ServerSettingsV1 : IConfigSettable
public List<Guid> Albums { get; set; } = new List<Guid>();
public List<Guid> ExcludedAlbums { get; set; } = new List<Guid>();
public List<Guid> People { get; set; } = new List<Guid>();
public bool RequireAllPeople { get; set; } = false;
public List<string> Tags { get; set; } = new List<string>();
public int? Rating { get; set; }
public List<string> Webcalendars { get; set; } = new List<string>();
Expand Down Expand Up @@ -92,6 +93,7 @@ class AccountSettingsV1Adapter(ServerSettingsV1 _delegate) : IAccountSettings
public List<Guid> Albums => _delegate.Albums;
public List<Guid> ExcludedAlbums => _delegate.ExcludedAlbums;
public List<Guid> People => _delegate.People;
public bool RequireAllPeople => _delegate.RequireAllPeople;
public List<string> Tags => _delegate.Tags;
public int? Rating => _delegate.Rating;

Expand Down
1 change: 1 addition & 0 deletions ImmichFrame.WebApi/Models/ServerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public class ServerAccountSettings : IAccountSettings, IConfigSettable
public List<Guid> Albums { get; set; } = new();
public List<Guid> ExcludedAlbums { get; set; } = new();
public List<Guid> People { get; set; } = new();
public bool RequireAllPeople { get; set; } = false;
public List<string> Tags { get; set; } = new();
public int? Rating { get; set; }

Expand Down
4 changes: 3 additions & 1 deletion docs/docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ General:
# e.g. https://user:pass@calendar.immichframe.dev/dav/calendars/basic.ics
Webcalendars: # string[]
- UUID
# Interval in hours. Determines how often images are pulled from a person in immich.
# Interval in hours. Determines how often images are pulled from a album/person in immich.
RefreshAlbumPeopleInterval: 12 # int
# Date format. See https://date-fns.org/v4.1.0/docs/format for more information.
PhotoDateFormat: 'MM/dd/yyyy' # string
Expand Down Expand Up @@ -138,6 +138,8 @@ Accounts:
# UUID of People
People: # string[]
- UUID
# If this is set, all specified people must be present in an image for it to be displayed.
RequireAllPeople: false # boolean
# Tag values (full hierarchical paths, case-sensitive)
Tags: # string[]
- "Vacation"
Expand Down
Loading