Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cf8f769
[FEAT] Introduced RequireAllPeople flag to filter assets for people i…
FlorianObermayer Mar 22, 2026
35870e8
Merge pull request #1 from FlorianObermayer/feature/fo/add-require-al…
FlorianObermayer Mar 22, 2026
3f7f4c1
Add RequireAllPeople configuration to documentation
FlorianObermayer Mar 23, 2026
eb61981
[FEAT] Introduced RequireAllPeople flag to filter assets for people i…
FlorianObermayer Mar 22, 2026
2a47f8a
Add RequireAllPeople configuration to documentation
FlorianObermayer Mar 23, 2026
b32dcce
Merge branch 'main' of https://github.com/FlorianObermayer/ImmichFrame
FlorianObermayer Apr 11, 2026
8e7173e
Memories fix...yet again
Apr 16, 2026
593668e
resolve asset transition lockups and video stall freezes
Apr 23, 2026
262cbbc
comment cleanup
Apr 23, 2026
9439ab1
coderabbit nitpicks
Apr 23, 2026
c95396c
coderrabbit nitpicks 2
Apr 23, 2026
c46a180
coderabbit 3
Apr 23, 2026
4c2b11d
handle manual back skip mid-transition
Apr 24, 2026
4da6151
drain pending transition queue
Apr 24, 2026
39e4aab
clear videoStallTimeout
Apr 24, 2026
294e622
add transition epoch
Apr 24, 2026
4b05b08
nitpick fixes
Apr 24, 2026
3fcfefb
Stall-timer gating
Apr 24, 2026
279748d
watchdog recovery, comments
Apr 24, 2026
367532f
stop consecutive errors
Apr 26, 2026
27c8495
progress tween fix
Apr 27, 2026
46f5133
prevent unhandled promise failures
May 27, 2026
e72e812
[FEAT] Introduced RequireAllPeople flag to filter assets for people i…
FlorianObermayer Apr 11, 2026
653b393
Merge branch 'main' into Feat/Add-RequireAllPeople-setting-for-AND-ba…
FlorianObermayer Jul 23, 2026
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 @@ -124,4 +124,84 @@ public async Task LoadAssets_PersonHasNoAssets_DoesNotAffectOthers()
var expectedIds = p1Assets.Select(a => a.Id);
Assert.That(result.All(a => expectedIds.Contains(a.Id)));
}

[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
11 changes: 9 additions & 2 deletions ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ 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]);
Comment thread
FlorianObermayer marked this conversation as resolved.

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
1 change: 1 addition & 0 deletions docker/Settings.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"People": [
"UUID"
],
"RequireAllPeople": false,
"Tags": [
"Vacation",
"Travel/Europe"
Expand Down
1 change: 1 addition & 0 deletions docker/Settings.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Accounts:
- UUID
People:
- UUID
RequireAllPeople: false
Tags:
- Vacation
- Travel/Europe
1 change: 1 addition & 0 deletions docker/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ ApiKey=KEY
# Albums=ALBUM1,ALBUM2
# ExcludedAlbums=ALBUM3,ALBUM4
# People=PERSON1,PERSON2
# RequireAllPeople=false
# Webcalendars=https://calendar.google.com/calendar/ical/XXXXXX/public/basic.ics,https://user:pass@calendar.immichframe.dev/dav/calendars/basic.ics
# RefreshAlbumPeopleInterval=12
# ShowClock=true
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
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