Skip to content

Commit 33e2f5d

Browse files
committed
feat: Sort memories chronologically
Adds a test to ensure that memories are returned in chronological order. The implementation is updated to sort memories by year.
1 parent e9a02d6 commit 33e2f5d

File tree

15 files changed

+317
-148
lines changed

15 files changed

+317
-148
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ private class TestableAlbumAssetsPool(IApiCache apiCache, ImmichApi immichApi, I
2323
: AlbumAssetsPool(apiCache, immichApi, accountSettings)
2424
{
2525
// Expose LoadAssets for testing
26-
public Task<IEnumerable<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default) => base.LoadAssets(ct);
26+
public Task<IList<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default) => base.LoadAssets(ct);
2727
}
2828

2929
[SetUp]

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ public class CachingApiAssetsPoolTests
2222
// Concrete implementation for testing the abstract class
2323
private class TestableCachingApiAssetsPool : CachingApiAssetsPool
2424
{
25-
public Func<Task<IEnumerable<AssetResponseDto>>> LoadAssetsFunc { get; set; }
25+
public Func<Task<IList<AssetResponseDto>>> LoadAssetsFunc { get; set; }
2626

2727
public TestableCachingApiAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings)
28-
: base(apiCache, immichApi, accountSettings)
28+
: base(apiCache, accountSettings)
2929
{
3030
}
3131

32-
protected override Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
32+
protected override Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
3333
{
34-
return LoadAssetsFunc != null ? LoadAssetsFunc() : Task.FromResult(Enumerable.Empty<AssetResponseDto>());
34+
return LoadAssetsFunc != null ? LoadAssetsFunc() : Task.FromResult<IList<AssetResponseDto>>(new List<AssetResponseDto>());
3535
}
3636
}
3737

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

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

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

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

115115
// Act
@@ -129,16 +129,16 @@ public async Task AllAssets_UsesCache_LoadAssetsCalledOnce()
129129
_testPool.LoadAssetsFunc = () =>
130130
{
131131
loadAssetsCallCount++;
132-
return Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
132+
return Task.FromResult<IList<AssetResponseDto>>(assets);
133133
};
134134

135135
// Setup cache to really cache after the first call
136-
IEnumerable<AssetResponseDto> cachedValue = null;
136+
IList<AssetResponseDto> cachedValue = null;
137137
_mockApiCache.Setup(c => c.GetOrAddAsync(
138138
It.IsAny<string>(),
139-
It.IsAny<Func<Task<IEnumerable<AssetResponseDto>>>>()
139+
It.IsAny<Func<Task<IList<AssetResponseDto>>>>()
140140
))
141-
.Returns<string, Func<Task<IEnumerable<AssetResponseDto>>>>(async (key, factory) =>
141+
.Returns<string, Func<Task<IList<AssetResponseDto>>>>(async (key, factory) =>
142142
{
143143
if (cachedValue == null)
144144
{
@@ -162,7 +162,7 @@ public async Task ApplyAccountFilters_FiltersArchived()
162162
{
163163
// Arrange
164164
var assets = CreateSampleAssets(); // Asset "3" is archived
165-
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
165+
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
166166
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(false);
167167

168168
// Act
@@ -178,7 +178,7 @@ public async Task ApplyAccountFilters_FiltersImagesUntilDate()
178178
{
179179
// Arrange
180180
var assets = CreateSampleAssets();
181-
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
181+
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
182182
var untilDate = DateTime.Now.AddDays(-7); // Assets "1" (10 days ago), "5" (1 year ago) should match
183183
_mockAccountSettings.SetupGet(s => s.ImagesUntilDate).Returns(untilDate);
184184
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true); // Include asset "3" for date check if not filtered by archive
@@ -201,7 +201,7 @@ public async Task ApplyAccountFilters_FiltersImagesFromDate()
201201
{
202202
// Arrange
203203
var assets = CreateSampleAssets();
204-
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
204+
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
205205
var fromDate = DateTime.Now.AddDays(-7); // Assets "3" (5 days ago), "4" (2 days ago) should match
206206
_mockAccountSettings.SetupGet(s => s.ImagesFromDate).Returns(fromDate);
207207
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true);
@@ -224,7 +224,7 @@ public async Task ApplyAccountFilters_FiltersImagesFromDays()
224224
{
225225
// Arrange
226226
var assets = CreateSampleAssets();
227-
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
227+
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
228228
_mockAccountSettings.SetupGet(s => s.ImagesFromDays).Returns(7); // Last 7 days
229229
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true);
230230
var fromDate = DateTime.Today.AddDays(-7);
@@ -248,7 +248,7 @@ public async Task ApplyAccountFilters_FiltersRating()
248248
{
249249
// Arrange
250250
var assets = CreateSampleAssets(); // Asset "1" (rating 5), "3" (rating 3), "4" (rating 5), "5" (rating 1)
251-
_testPool.LoadAssetsFunc = () => Task.FromResult<IEnumerable<AssetResponseDto>>(assets);
251+
_testPool.LoadAssetsFunc = () => Task.FromResult<IList<AssetResponseDto>>(assets);
252252
_mockAccountSettings.SetupGet(s => s.Rating).Returns(5);
253253
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true);
254254

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

274274
_mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(false); // No archived (Asset "3" out)
275275
_mockAccountSettings.SetupGet(s => s.ImagesFromDays).Returns(15); // Last 15 days (Asset "5" out)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ private class TestableFavoriteAssetsPool : FavoriteAssetsPool
2424
public TestableFavoriteAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings)
2525
: base(apiCache, immichApi, accountSettings) { }
2626

27-
public Task<IEnumerable<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default)
27+
public Task<IList<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default)
2828
{
2929
return base.LoadAssets(ct);
3030
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ private class TestablePersonAssetsPool : PersonAssetsPool
2424
public TestablePersonAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings)
2525
: base(apiCache, immichApi, accountSettings) { }
2626

27-
public Task<IEnumerable<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default)
27+
public Task<IList<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default)
2828
{
2929
return base.LoadAssets(ct);
3030
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using ImmichFrame.Core.Api;
2+
using ImmichFrame.Core.Exceptions;
3+
using ImmichFrame.Core.Interfaces;
4+
using ImmichFrame.Core.Logic.Pool;
5+
6+
namespace ImmichFrame.Core.Logic;
7+
8+
public class LogicPoolAdapter(IAssetPool pool, ImmichApi immichApi, string? webhook) : IImmichFrameLogic
9+
{
10+
public async Task<AssetResponseDto?> GetNextAsset()
11+
=> (await pool.GetAssets(1)).FirstOrDefault();
12+
13+
public Task<IEnumerable<AssetResponseDto>> GetAssets()
14+
=> pool.GetAssets(25);
15+
16+
public Task<AssetResponseDto> GetAssetInfoById(Guid assetId)
17+
=> immichApi.GetAssetInfoAsync(assetId, null);
18+
19+
public async Task<IEnumerable<AlbumResponseDto>> GetAlbumInfoById(Guid assetId)
20+
=> await immichApi.GetAllAlbumsAsync(assetId, null);
21+
22+
public Task<long> GetTotalAssets() => pool.GetAssetCount();
23+
24+
public Task SendWebhookNotification(IWebhookNotification notification) =>
25+
WebhookHelper.SendWebhookNotification(notification, webhook);
26+
27+
public virtual async Task<(string fileName, string ContentType, Stream fileStream)> GetImage(Guid id)
28+
{
29+
var data = await immichApi.ViewAssetAsync(id, string.Empty, AssetMediaSize.Preview);
30+
31+
if (data == null)
32+
throw new AssetNotFoundException($"Asset {id} was not found!");
33+
34+
var contentType = "";
35+
if (data.Headers.ContainsKey("Content-Type"))
36+
{
37+
contentType = data.Headers["Content-Type"].FirstOrDefault() ?? "";
38+
}
39+
40+
var ext = contentType.ToLower() == "image/webp" ? "webp" : "jpeg";
41+
var fileName = $"{id}.{ext}";
42+
43+
return (fileName, contentType, data.Stream);
44+
}
45+
}

ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
namespace ImmichFrame.Core.Logic.Pool;
66

7-
public class AlbumAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, immichApi, accountSettings)
7+
public class AlbumAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, accountSettings)
88
{
9-
protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
9+
protected override async Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
1010
{
1111
var excludedAlbumAssets = new List<AssetResponseDto>();
1212

@@ -24,6 +24,6 @@ protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(Cancella
2424
albumAssets.AddRange(albumInfo.Assets);
2525
}
2626

27-
return albumAssets.WhereExcludes(excludedAlbumAssets, t => t.Id);
27+
return albumAssets.WhereExcludes(excludedAlbumAssets, t => t.Id).ToList();
2828
}
2929
}

ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,48 @@
33

44
namespace ImmichFrame.Core.Logic.Pool;
55

6-
public abstract class CachingApiAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : IAssetPool
6+
public abstract class CachingApiAssetsPool(IApiCache apiCache, IAccountSettings accountSettings) : IAssetPool
77
{
8-
private readonly Random _random = new();
9-
8+
private int _next; //next asset to return
9+
1010
public async Task<long> GetAssetCount(CancellationToken ct = default)
11-
{
12-
return (await AllAssets(ct)).Count();
13-
}
11+
=> (await AllAssets(ct)).Count;
1412

1513
public async Task<IEnumerable<AssetResponseDto>> GetAssets(int requested, CancellationToken ct = default)
1614
{
17-
return (await AllAssets(ct)).OrderBy(_ => _random.Next()).Take(requested);
15+
if (requested == 0)
16+
{
17+
return new List<AssetResponseDto>();
18+
}
19+
20+
var all = await AllAssets(ct);
21+
22+
if (all.Count < requested)
23+
{
24+
requested = all.Count; //limit request to what we have
25+
}
26+
27+
var tail = all.TakeLast(all.Count - _next).ToList();
28+
29+
if (tail.Count >= requested)
30+
{
31+
_next += requested;
32+
return tail.Take(requested);
33+
}
34+
35+
// not enough left in tail; need to read head too
36+
var overrun = requested - tail.Count;
37+
_next = overrun;
38+
return tail.Concat(all.Take(overrun));
1839
}
1940

20-
private async Task<IEnumerable<AssetResponseDto>> AllAssets(CancellationToken ct = default)
41+
private async Task<IList<AssetResponseDto>> AllAssets(CancellationToken ct = default)
2142
{
2243
return await apiCache.GetOrAddAsync(GetType().FullName!, () => ApplyAccountFilters(LoadAssets(ct)));
2344
}
2445

2546

26-
protected async Task<IEnumerable<AssetResponseDto>> ApplyAccountFilters(Task<IEnumerable<AssetResponseDto>> unfiltered)
47+
protected async Task<IList<AssetResponseDto>> ApplyAccountFilters(Task<IList<AssetResponseDto>> unfiltered)
2748
{
2849
// Display only Images
2950
var assets = (await unfiltered).Where(x => x.Type == AssetTypeEnum.IMAGE);
@@ -47,9 +68,9 @@ protected async Task<IEnumerable<AssetResponseDto>> ApplyAccountFilters(Task<IEn
4768
{
4869
assets = assets.Where(x => x.ExifInfo.Rating == rating);
4970
}
50-
51-
return assets;
71+
72+
return assets.ToList();
5273
}
53-
54-
protected abstract Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default);
74+
75+
protected abstract Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default);
5576
}

ImmichFrame.Core/Logic/Pool/FavoriteAssetsPool.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
namespace ImmichFrame.Core.Logic.Pool;
55

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

ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33

44
namespace ImmichFrame.Core.Logic.Pool;
55

6-
public class MemoryAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, immichApi, accountSettings)
6+
public class MemoryAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, accountSettings)
77
{
8-
protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
8+
protected override async Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
99
{
1010
var memories = await immichApi.SearchMemoriesAsync(DateTime.Now, null, null, null, ct);
1111

1212
var memoryAssets = new List<AssetResponseDto>();
13-
foreach (var memory in memories)
13+
foreach (var memory in memories.OrderBy(m => m.MemoryAt))
1414
{
1515
var assets = memory.Assets.ToList();
1616
var yearsAgo = DateTime.Now.Year - memory.Data.Year;

ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
namespace ImmichFrame.Core.Logic.Pool;
55

6-
public class PersonAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, immichApi, accountSettings)
6+
public class PersonAssetsPool(IApiCache apiCache, ImmichApi immichApi, IAccountSettings accountSettings) : CachingApiAssetsPool(apiCache, accountSettings)
77
{
8-
protected override async Task<IEnumerable<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
8+
protected override async Task<IList<AssetResponseDto>> LoadAssets(CancellationToken ct = default)
99
{
1010
var personAssets = new List<AssetResponseDto>();
1111

0 commit comments

Comments
 (0)