Skip to content
Open
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
2 changes: 1 addition & 1 deletion ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ private class TestableAlbumAssetsPool(IApiCache apiCache, ImmichApi immichApi, I
: AlbumAssetsPool(apiCache, immichApi, accountSettings)
{
// Expose LoadAssets for testing
public Task<IEnumerable<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default) => base.LoadAssets(ct);
public Task<IList<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default) => base.LoadAssets(ct);
}

[SetUp]
Expand Down
38 changes: 19 additions & 19 deletions ImmichFrame.Core.Tests/Logic/Pool/CachingApiAssetsPoolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ public class CachingApiAssetsPoolTests
// Concrete implementation for testing the abstract class
private class TestableCachingApiAssetsPool : CachingApiAssetsPool
{
public Func<Task<IEnumerable<AssetResponseDto>>> LoadAssetsFunc { get; set; }
public Func<Task<IList<AssetResponseDto>>> LoadAssetsFunc { get; set; }

public TestableCachingApiAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings)
: base(apiCache, immichApi, accountSettings)
: base(apiCache, accountSettings)
{
}

protected override Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
protected override Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
{
return LoadAssetsFunc != null ? LoadAssetsFunc() : Task.FromResult(Enumerable.Empty<AssetResponseDto>());
return LoadAssetsFunc != null ? LoadAssetsFunc() : Task.FromResult<IList<AssetResponseDto>>(new List<AssetResponseDto>());
}
}

Expand All @@ -47,9 +47,9 @@ public void Setup()
// Default setup for ApiCache to execute the factory function
_mockApiCache.Setup(c => c.GetOrAddAsync(
It.IsAny<string>(),
It.IsAny<Func<Task<IEnumerable<AssetResponseDto>>>>()
It.IsAny<Func<Task<IList<AssetResponseDto>>>>()
))
.Returns<string, Func<Task<IEnumerable<AssetResponseDto>>>>(async (key, factory) => await factory());
.Returns<string, Func<Task<IList<AssetResponseDto>>>>(async (key, factory) => await factory());

// Default account settings
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true);
Expand All @@ -76,7 +76,7 @@ public async Task GetAssetCount_ReturnsCorrectCount_AfterFiltering()
{
// Arrange
var assets = CreateSampleAssets();
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(false); // Filter out archived

// Act
Expand All @@ -92,7 +92,7 @@ public async Task GetAssets_ReturnsRequestedNumberOfAssets()
{
// Arrange
var assets = CreateSampleAssets(); // Total 5 assets, 4 images if ShowArchived = true
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true); // Asset "3" included

// Act
Expand All @@ -109,7 +109,7 @@ public async Task GetAssets_ReturnsAllAvailableIfLessThanRequested()
{
// Arrange
var assets = CreateSampleAssets().Where(a => a.Type == AssetTypeEnum.IMAGE && !a.IsArchived).ToList(); // 3 assets
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(false);

// Act
Expand All @@ -129,16 +129,16 @@ public async Task AllAssets_UsesCache_LoadAssetsCalledOnce()
_testPool.LoadAssetsFunc = () =>
{
loadAssetsCallCount++;
return Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
return Task.FromResult<IList<AssetResponseDto>>(assets);
};

// Setup cache to really cache after the first call
IEnumerable<AssetResponseDto> cachedValue = null;
IList<AssetResponseDto> cachedValue = null;
_mockApiCache.Setup(c => c.GetOrAddAsync(
It.IsAny<string>(),
It.IsAny<Func<Task<IEnumerable<AssetResponseDto>>>>()
It.IsAny<Func<Task<IList<AssetResponseDto>>>>()
))
.Returns<string, Func<Task<IEnumerable<AssetResponseDto>>>>(async (key, factory) =>
.Returns<string, Func<Task<IList<AssetResponseDto>>>>(async (key, factory) =>
{
if (cachedValue == null)
{
Expand All @@ -162,7 +162,7 @@ public async Task ApplyAccountFilters_FiltersArchived()
{
// Arrange
var assets = CreateSampleAssets(); // Asset "3" is archived
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(false);

// Act
Expand All @@ -178,7 +178,7 @@ public async Task ApplyAccountFilters_FiltersImagesUntilDate()
{
// Arrange
var assets = CreateSampleAssets();
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
var untilDate = DateTime.Now.AddDays(-7); // Assets "1" (10 days ago), "5" (1 year ago) should match
_mockAccountSettings.SetupGet(s => s.ImagesUntilDate).Returns(untilDate);
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true); // Include asset "3" for date check if not filtered by archive
Expand All @@ -201,7 +201,7 @@ public async Task ApplyAccountFilters_FiltersImagesFromDate()
{
// Arrange
var assets = CreateSampleAssets();
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
var fromDate = DateTime.Now.AddDays(-7); // Assets "3" (5 days ago), "4" (2 days ago) should match
_mockAccountSettings.SetupGet(s => s.ImagesFromDate).Returns(fromDate);
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true);
Expand All @@ -224,7 +224,7 @@ public async Task ApplyAccountFilters_FiltersImagesFromDays()
{
// Arrange
var assets = CreateSampleAssets();
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
_mockAccountSettings.SetupGet(s => s.ImagesFromDays).Returns(7); // Last 7 days
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true);
var fromDate = DateTime.Today.AddDays(-7);
Expand All @@ -248,7 +248,7 @@ public async Task ApplyAccountFilters_FiltersRating()
{
// Arrange
var assets = CreateSampleAssets(); // Asset "1" (rating 5), "3" (rating 3), "4" (rating 5), "5" (rating 1)
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
_mockAccountSettings.SetupGet(s => s.Rating).Returns(5);
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true);

Expand All @@ -269,7 +269,7 @@ public async Task ApplyAccountFilters_CombinedFilters()
{
// Arrange
var assets = CreateSampleAssets();
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);

_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(false); // No archived (Asset "3" out)
_mockAccountSettings.SetupGet(s => s.ImagesFromDays).Returns(15); // Last 15 days (Asset "5" out)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
public TestableFavoriteAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings)
: base(apiCache, immichApi, accountSettings) { }

public Task<IEnumerable<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default)
public Task<IList<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default)
{
return base.LoadAssets(ct);
}
Expand All @@ -34,7 +34,7 @@
public void Setup()
{
_mockApiCache = new Mock<IApiCache>();
_mockImmichApi = new Mock<ImmichApi>(null, null);

Check warning on line 37 in ImmichFrame.Core.Tests/Logic/Pool/FavoriteAssetsPoolTests.cs

View workflow job for this annotation

GitHub Actions / test

Cannot convert null literal to non-nullable reference type.
_mockAccountSettings = new Mock<IAccountSettings>();
_favoriteAssetsPool = new TestableFavoriteAssetsPool(_mockApiCache.Object, _mockImmichApi.Object, _mockAccountSettings.Object);
}
Expand Down
2 changes: 1 addition & 1 deletion ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private class TestablePersonAssetsPool : PersonAssetsPool
public TestablePersonAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings)
: base(apiCache, immichApi, accountSettings) { }

public Task<IEnumerable<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default)
public Task<IList<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default)
{
return base.LoadAssets(ct);
}
Expand Down
45 changes: 45 additions & 0 deletions ImmichFrame.Core/Logic/LogicPoolAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using ImmichFrame.Core.Api;
using ImmichFrame.Core.Exceptions;
using ImmichFrame.Core.Interfaces;
using ImmichFrame.Core.Logic.Pool;

namespace ImmichFrame.Core.Logic;

public class LogicPoolAdapter(IAssetPool pool, ImmichApi immichApi, string? webhook) : IImmichFrameLogic
{
public async Task<AssetResponseDto?> GetNextAsset()
=> (await pool.GetAssets(1)).FirstOrDefault();

public Task<IEnumerable<AssetResponseDto>> GetAssets()
=> pool.GetAssets(25);

public Task<AssetResponseDto> GetAssetInfoById(Guid assetId)
=> immichApi.GetAssetInfoAsync(assetId, null);

public async Task<IEnumerable<AlbumResponseDto>> GetAlbumInfoById(Guid assetId)
=> await immichApi.GetAllAlbumsAsync(assetId, null);

public Task<long> GetTotalAssets() => pool.GetAssetCount();

public Task SendWebhookNotification(IWebhookNotification notification) =>
WebhookHelper.SendWebhookNotification(notification, webhook);

public virtual async Task<(string fileName, string ContentType, Stream fileStream)> GetImage(Guid id)
{
var data = await immichApi.ViewAssetAsync(id, string.Empty, AssetMediaSize.Preview);

if (data == null)
throw new AssetNotFoundException($"Asset {id} was not found!");

var contentType = "";
if (data.Headers.ContainsKey("Content-Type"))
{
contentType = data.Headers["Content-Type"].FirstOrDefault() ?? "";
}

var ext = contentType.ToLower() == "image/webp" ? "webp" : "jpeg";
var fileName = $"{id}.{ext}";

return (fileName, contentType, data.Stream);
}
}
6 changes: 3 additions & 3 deletions ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace ImmichFrame.Core.Logic.Pool;

public class AlbumAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, immichApi, accountSettings)
public class AlbumAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, accountSettings)

Check warning on line 7 in ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs

View workflow job for this annotation

GitHub Actions / test

Parameter 'IAccountSettings accountSettings' is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
{
protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
protected override async Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
{
var excludedAlbumAssets = new List<AssetResponseDto>();

Expand All @@ -24,6 +24,6 @@
albumAssets.AddRange(albumInfo.Assets);
}

return albumAssets.WhereExcludes(excludedAlbumAssets, t => t.Id);
return albumAssets.WhereExcludes(excludedAlbumAssets, t => t.Id).ToList();
}
}
47 changes: 34 additions & 13 deletions ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,48 @@

namespace ImmichFrame.Core.Logic.Pool;

public abstract class CachingApiAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : IAssetPool
public abstract class CachingApiAssetsPool(IApiCache apiCache, IAccountSettings accountSettings) : IAssetPool
{
private readonly Random _random = new();
private int _next; //next asset to return
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Thread safety: Concurrent GetAssets calls will corrupt _next.

The _next field is mutated without synchronization (lines 31, 37), so concurrent calls to GetAssets will race and produce incorrect results or skip assets. If this pool is accessed from multiple threads (e.g., concurrent requests in a web app), the iteration state will be corrupted.

Apply thread-local state or locking:

+    private readonly object _lock = new();
     private int _next; //next asset to return
 
     public async Task<IEnumerable<AssetResponseDto>> GetAssets(int requested, CancellationToken ct = default)
     {
+        lock (_lock)
+        {
             if (requested == 0)
             {
                 return new List<AssetResponseDto>();
             }
             
             var all = await AllAssets(ct);
 
             if (all.Count < requested)
             {
                 requested = all.Count; //limit request to what we have
             }
             
             var tail = all.TakeLast(all.Count - _next).ToList();
             
             if (tail.Count >= requested)
             {
                 _next += requested;
                 return tail.Take(requested);
             }
 
             // not enough left in tail; need to read head too
             var overrun = requested - tail.Count;
             _next = overrun;
             return tail.Concat(all.Take(overrun));
+        }
     }

Note: If AllAssets performs async I/O, consider using SemaphoreSlim instead of lock to avoid blocking threads.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs around line 8 (field
_next) and the mutations at lines ~31 and ~37, the mutable field _next is
modified without synchronization causing races when GetAssets is called
concurrently; fix by protecting access to _next with a thread-safe strategy:
either make _next thread-local (e.g., use ThreadLocal<int> or maintain a
per-call index) or serialize updates with a lock (create a private readonly
object _lock and read/update _next inside lock blocks), and if AllAssets or
surrounding logic is asynchronous avoid blocking locks and use an async-aware
semaphore (SemaphoreSlim.WaitAsync/Release) to guard the critical section;
ensure all reads and writes of _next use the chosen synchronization so
concurrent callers cannot corrupt iteration state.


public async Task<long> GetAssetCount(CancellationToken ct = default)
{
return (await AllAssets(ct)).Count();
}
=> (await AllAssets(ct)).Count;

public async Task<IEnumerable<AssetResponseDto>> GetAssets(int requested, CancellationToken ct = default)
{
return (await AllAssets(ct)).OrderBy(_ => _random.Next()).Take(requested);
if (requested == 0)
{
return new List<AssetResponseDto>();
}

var all = await AllAssets(ct);

if (all.Count < requested)
{
requested = all.Count; //limit request to what we have
}

var tail = all.TakeLast(all.Count - _next).ToList();

if (tail.Count >= requested)
{
_next += requested;
return tail.Take(requested);
}

// not enough left in tail; need to read head too
var overrun = requested - tail.Count;
_next = overrun;
return tail.Concat(all.Take(overrun));
}

private async Task<IEnumerable<AssetResponseDto>> AllAssets(CancellationToken ct = default)
private async Task<IList<AssetResponseDto>> AllAssets(CancellationToken ct = default)
{
return await apiCache.GetOrAddAsync(GetType().FullName!, () => ApplyAccountFilters(LoadAssets(ct)));
}


protected async Task<IEnumerable<AssetResponseDto>> ApplyAccountFilters(Task<IEnumerable<AssetResponseDto>> unfiltered)
protected async Task<IList<AssetResponseDto>> ApplyAccountFilters(Task<IList<AssetResponseDto>> unfiltered)
{
// Display only Images
var assets = (await unfiltered).Where(x => x.Type == AssetTypeEnum.IMAGE);
Expand All @@ -47,9 +68,9 @@ protected async Task<IEnumerable<AssetResponseDto>> ApplyAccountFilters(Task<IEn
{
assets = assets.Where(x => x.ExifInfo.Rating == rating);
}
return assets;

return assets.ToList();
}
protected abstract Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default);

protected abstract Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default);
}
4 changes: 2 additions & 2 deletions ImmichFrame.Core/Logic/Pool/FavoriteAssetsPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

namespace ImmichFrame.Core.Logic.Pool;

public class FavoriteAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, immichApi, accountSettings)
public class FavoriteAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, accountSettings)
{
protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
protected override async Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
{
var favoriteAssets = new List<AssetResponseDto>();

Expand Down
7 changes: 3 additions & 4 deletions ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

namespace ImmichFrame.Core.Logic.Pool;

public class MemoryAssetsPool(ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(new DailyApiCache(), immichApi, accountSettings)
public class MemoryAssetsPool(ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(new DailyApiCache(), accountSettings)
{
protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
protected override async Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
{
var memories = await immichApi.SearchMemoriesAsync(DateTime.Now, null, null, null, ct);

var memoryAssets = new List<AssetResponseDto>();
foreach (var memory in memories)
foreach (var memory in memories.OrderBy(m => m.MemoryAt))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Chronological sorting implemented correctly.

The addition of .OrderBy(m => m.MemoryAt) ensures memories are processed in chronological order (oldest to newest), which addresses the core requirement of issue #163. This will display memories as "1 year ago", "2 years ago", etc., in the correct sequence.

However, there's a critical concern raised in the PR comments: JoeRu noted that sorting may be undone by random/shuffle operations in TotalAccountImagesSelectionStrategy.cs and MultiImmichFrameLogicDelegate.cs. These randomizations could override the sorting done here, making this fix ineffective.

Run the following script to identify potential shuffle operations that might override this sorting:


🏁 Script executed:

#!/bin/bash
# Search for random/shuffle operations in the codebase that might affect asset order

echo "=== Checking TotalAccountImagesSelectionStrategy.cs ==="
rg -nC3 -g 'TotalAccountImagesSelectionStrategy.cs' '\b(Random|Shuffle|OrderBy.*Guid\.NewGuid)\b'

echo "=== Checking MultiImmichFrameLogicDelegate.cs ==="
rg -nC3 -g 'MultiImmichFrameLogicDelegate.cs' '\b(Random|Shuffle|OrderBy.*Guid\.NewGuid)\b'

echo "=== Checking for any shuffle operations in Logic/AccountSelection ==="
rg -nC3 -g '*.cs' --type-add 'cs:*.cs' 'Logic/AccountSelection' -e '\b(Random|Shuffle|OrderBy.*Guid\.NewGuid)\b'

echo "=== Checking for any shuffle operations in PooledImmichFrameLogic ==="
rg -nC3 -g 'PooledImmichFrameLogic.cs' '\b(Random|Shuffle|OrderBy.*Guid\.NewGuid)\b'

Length of output: 1953


Sorting is negated by downstream shuffle operations.

  • TotalAccountImagesSelectionStrategy.cs (line 63): assets.Shuffle() overrides the chronological sort.
  • MultiImmichFrameLogicDelegate.cs (line 34): .Shuffle() on the assembled assets also discards prior ordering.

Reapply OrderBy(m => m.MemoryAt) after the final shuffle or relocate/remove these shuffles to ensure memories remain in chronological order.

🤖 Prompt for AI Agents
In ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs around line 15, the foreach
over memories.OrderBy(m => m.MemoryAt) is being negated by downstream Shuffle()
calls (TotalAccountImagesSelectionStrategy.cs line 63 and
MultiImmichFrameLogicDelegate.cs line 34); to fix, ensure chronological ordering
is preserved by either removing those downstream Shuffle() calls or reapplying
OrderBy(m => m.MemoryAt) after the final Shuffle (i.e., in the final asset
assembly/return path), so the resulting collection is explicitly sorted by
MemoryAt before being consumed or returned.

{
var assets = memory.Assets.ToList();
var yearsAgo = DateTime.Now.Year - memory.Data.Year;
Expand All @@ -25,7 +25,6 @@ protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(Cancella
asset.ExifInfo = assetInfo.ExifInfo;
asset.People = assetInfo.People;
}

asset.ExifInfo.Description = $"{yearsAgo} {(yearsAgo == 1 ? "year" : "years")} ago";
}

Expand Down
16 changes: 8 additions & 8 deletions ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@

namespace ImmichFrame.Core.Logic.Pool;

public class PersonAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, immichApi, accountSettings)
public class PersonAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, accountSettings)

Check warning on line 6 in ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs

View workflow job for this annotation

GitHub Actions / test

Parameter 'IAccountSettings accountSettings' is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
{
protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
protected override async Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
{
var personAssets = new List<AssetResponseDto>();

foreach (var personId in accountSettings.People)
{
int page = 1;
int batchSize = 1000;
int total;
int returned;
do
{
var metadataBody = new MetadataSearchDto
Expand All @@ -27,12 +27,12 @@
};

var personInfo = await immichApi.SearchAssetsAsync(metadataBody, ct);

total = personInfo.Assets.Total;

personAssets.AddRange(personInfo.Assets.Items);
var items = personInfo.Assets.Items;
returned = items.Count;
personAssets.AddRange(items);
page++;
} while (total == batchSize);
} while (returned == batchSize && !ct.IsCancellationRequested);
}

return personAssets;
Expand Down
Loading
Loading