Skip to content

Commit fb7124a

Browse files
authored
Merge pull request #442 from jpg0/fix/memories-day-cache
Fix: Memories cache should last one day
2 parents a8e3aa1 + d8e4955 commit fb7124a

4 files changed

Lines changed: 30 additions & 39 deletions

File tree

ImmichFrame.Core.Tests/Logic/Pool/MemoryAssetsPoolTests.cs

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,17 @@ namespace ImmichFrame.Core.Tests.Logic.Pool;
1414
[TestFixture]
1515
public class MemoryAssetsPoolTests
1616
{
17-
private Mock<IApiCache> _mockApiCache;
1817
private Mock<ImmichApi> _mockImmichApi;
1918
private Mock<IAccountSettings> _mockAccountSettings;
2019
private MemoryAssetsPool _memoryAssetsPool;
2120

2221
[SetUp]
2322
public void Setup()
2423
{
25-
_mockApiCache = new Mock<IApiCache>(); // Base constructor requires ILogger and IOptions, pass null for simplicity in mock
2624
_mockImmichApi = new Mock<ImmichApi>(null, null); // Base constructor requires ILogger, IHttpClientFactory, IOptions, pass null
2725
_mockAccountSettings = new Mock<IAccountSettings>();
2826

29-
_memoryAssetsPool = new MemoryAssetsPool(_mockApiCache.Object, _mockImmichApi.Object, _mockAccountSettings.Object);
27+
_memoryAssetsPool = new MemoryAssetsPool(_mockImmichApi.Object, _mockAccountSettings.Object);
3028
}
3129

3230
private List<AssetResponseDto> CreateSampleAssets(int count, bool withExif, int yearCreated)
@@ -76,10 +74,6 @@ public async Task LoadAssets_CallsSearchMemoriesAsync()
7674
// Let's simulate this by calling a method that would trigger LoadAssets if cache is empty.
7775
// Since LoadAssets is protected, we'll test its effects via GetAsset.
7876
// We need to ensure the cache is empty or expired for LoadAssets to be called.
79-
_mockApiCache.Setup(c => c.GetOrAddAsync(It.IsAny<string>(), It.IsAny<Func<Task<IEnumerable<AssetResponseDto>>>>()))
80-
.Returns<string, Func<Task<IEnumerable<AssetResponseDto>>>>(async (key, factory) => await factory());
81-
82-
8377
await _memoryAssetsPool.GetAssets(1, CancellationToken.None); // This should trigger LoadAssets
8478

8579
// Assert
@@ -99,9 +93,6 @@ public async Task LoadAssets_FetchesAssetInfo_WhenExifInfoIsNull()
9993
_mockImmichApi.Setup(x => x.GetAssetInfoAsync(new Guid(assetId), null, It.IsAny<CancellationToken>()))
10094
.ReturnsAsync(new AssetResponseDto { Id = assetId, ExifInfo = new ExifResponseDto { DateTimeOriginal = new DateTime(memoryYear, 1, 1) }, People = new List<PersonWithFacesResponseDto>() });
10195

102-
_mockApiCache.Setup(c => c.GetOrAddAsync<IEnumerable<AssetResponseDto>>(It.IsAny<string>(), It.IsAny<Func<Task<IEnumerable<AssetResponseDto>>>>()))
103-
.Returns<string, Func<Task<IEnumerable<AssetResponseDto>>>>(async (key, factory) => await factory());
104-
10596
// Act
10697
var resultAsset = (await _memoryAssetsPool.GetAssets(1, CancellationToken.None)).First(); // Triggers LoadAssets
10798

@@ -122,9 +113,6 @@ public async Task LoadAssets_DoesNotFetchAssetInfo_WhenExifInfoIsPresent()
122113
_mockImmichApi.Setup(x => x.SearchMemoriesAsync(It.IsAny<DateTimeOffset>(), null, null, null, It.IsAny<CancellationToken>()))
123114
.ReturnsAsync(memories);
124115

125-
_mockApiCache.Setup(c => c.GetOrAddAsync<IEnumerable<AssetResponseDto>>(It.IsAny<string>(), It.IsAny<Func<Task<IEnumerable<AssetResponseDto>>>>()))
126-
.Returns<string, Func<Task<IEnumerable<AssetResponseDto>>>>(async (key, factory) => await factory());
127-
128116
// Act
129117
var resultAsset = (await _memoryAssetsPool.GetAssets(1, CancellationToken.None)).First(); // Triggers LoadAssets
130118

@@ -154,11 +142,7 @@ public async Task LoadAssets_CorrectlyFormatsDescription_YearsAgo()
154142
_mockImmichApi.Setup(x => x.SearchMemoriesAsync(It.IsAny<DateTimeOffset>(), null, null, null, It.IsAny<CancellationToken>()))
155143
.ReturnsAsync(memories);
156144

157-
// Reset and re-setup cache mock for each iteration to ensure factory is called
158-
_mockApiCache = new Mock<IApiCache>();
159-
_mockApiCache.Setup(c => c.GetOrAddAsync<IEnumerable<AssetResponseDto>>(It.IsAny<string>(), It.IsAny<Func<Task<IEnumerable<AssetResponseDto>>>>()))
160-
.Returns<string, Func<Task<IEnumerable<AssetResponseDto>>>>(async (key, factory) => await factory());
161-
_memoryAssetsPool = new MemoryAssetsPool(_mockApiCache.Object, _mockImmichApi.Object, _mockAccountSettings.Object);
145+
_memoryAssetsPool = new MemoryAssetsPool(_mockImmichApi.Object, _mockAccountSettings.Object);
162146

163147

164148
// Act
@@ -180,9 +164,6 @@ public async Task LoadAssets_AggregatesAssetsFromMultipleMemories()
180164
_mockImmichApi.Setup(x => x.SearchMemoriesAsync(It.IsAny<DateTimeOffset>(), null, null, null, It.IsAny<CancellationToken>()))
181165
.ReturnsAsync(memories).Verifiable(Times.Once);
182166

183-
_mockApiCache.Setup(c => c.GetOrAddAsync(It.IsAny<string>(), It.IsAny<Func<Task<IEnumerable<AssetResponseDto>>>>()))
184-
.Returns<string, Func<Task<IEnumerable<AssetResponseDto>>>>(async (key, factory) => await factory());
185-
186167
// Act
187168
// We will rely on the fact that the factory in GetFromCacheAsync is called, and it returns the list.
188169
// The count can be indirectly verified if we could access the pool's internal list after LoadAssets.
@@ -191,15 +172,7 @@ public async Task LoadAssets_AggregatesAssetsFromMultipleMemories()
191172
// We need a way to inspect the result of LoadAssets directly.
192173
// We can make LoadAssets internal and use InternalsVisibleTo, or use reflection.
193174
// Or, we can rely on the setup of GetFromCacheAsync to capture the factory's result.
194-
IEnumerable<AssetResponseDto> loadedAssets = null;
195-
_mockApiCache.Setup(c => c.GetOrAddAsync<IEnumerable<AssetResponseDto>>(It.IsAny<string>(), It.IsAny<Func<Task<IEnumerable<AssetResponseDto>>>>()))
196-
.Returns<string, Func<Task<IEnumerable<AssetResponseDto>>>>(async (key, factory) =>
197-
{
198-
loadedAssets = await factory();
199-
return loadedAssets;
200-
});
201-
202-
await _memoryAssetsPool.GetAssets(4, CancellationToken.None); // Trigger load
175+
var loadedAssets = await _memoryAssetsPool.GetAssets(4, CancellationToken.None); // Trigger load
203176

204177
// Assert
205178
Assert.That(loadedAssets, Is.Not.Null);

ImmichFrame.Core/Helpers/ApiCache.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ namespace ImmichFrame.Core.Helpers;
44

55
public class ApiCache : IApiCache, IDisposable
66
{
7-
private readonly MemoryCacheEntryOptions _cacheOptions;
7+
private readonly Func<MemoryCacheEntryOptions> _cacheOptions;
88
private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
99

10-
public ApiCache(TimeSpan cacheDuration)
10+
public ApiCache(TimeSpan cacheDuration) : this(() => new MemoryCacheEntryOptions()
1111
{
12-
_cacheOptions = new()
13-
{
14-
AbsoluteExpirationRelativeToNow = cacheDuration
15-
};
12+
AbsoluteExpirationRelativeToNow = cacheDuration
13+
})
14+
{
15+
}
16+
17+
public ApiCache(Func<MemoryCacheEntryOptions> entryOptions)
18+
{
19+
_cacheOptions = entryOptions;
1620
}
1721

1822
public virtual Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> factory)
19-
=> _cache.GetOrCreateAsync<T>(key, _ => factory(), _cacheOptions);
23+
=> _cache.GetOrCreateAsync<T>(key, _ => factory(), _cacheOptions());
2024

2125
public void Dispose()
2226
=> _cache.Dispose();

ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using ImmichFrame.Core.Api;
2+
using ImmichFrame.Core.Helpers;
23
using ImmichFrame.Core.Interfaces;
4+
using Microsoft.Extensions.Caching.Memory;
35

46
namespace ImmichFrame.Core.Logic.Pool;
57

6-
public class MemoryAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, immichApi, accountSettings)
8+
public class MemoryAssetsPool(ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(new DailyApiCache(), immichApi, accountSettings)
79
{
810
protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
911
{
@@ -23,6 +25,7 @@ protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(Cancella
2325
asset.ExifInfo = assetInfo.ExifInfo;
2426
asset.People = assetInfo.People;
2527
}
28+
2629
asset.ExifInfo.Description = $"{yearsAgo} {(yearsAgo == 1 ? "year" : "years")} ago";
2730
}
2831

@@ -31,4 +34,15 @@ protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(Cancella
3134

3235
return memoryAssets;
3336
}
37+
}
38+
39+
class DailyApiCache : ApiCache
40+
{
41+
public DailyApiCache() : base(() => new MemoryCacheEntryOptions
42+
{
43+
AbsoluteExpiration = DateTimeOffset.Now.Date.AddDays(1)
44+
}
45+
)
46+
{
47+
}
3448
}

ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ private IAssetPool BuildPool(IAccountSettings accountSettings)
4646
pools.Add(new FavoriteAssetsPool(_apiCache, _immichApi, accountSettings));
4747

4848
if (accountSettings.ShowMemories)
49-
pools.Add(new MemoryAssetsPool(_apiCache, _immichApi, accountSettings));
49+
pools.Add(new MemoryAssetsPool(_immichApi, accountSettings));
5050

5151
if (accountSettings.Albums.Any())
5252
pools.Add(new AlbumAssetsPool(_apiCache, _immichApi, accountSettings));

0 commit comments

Comments
 (0)