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
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using ImmichFrame.Core.Api;
using ImmichFrame.Core.Logic.QueueMutator;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;

namespace ImmichFrame.Core.Tests.Logic.QueueMutator;

[TestFixture]
public class DuplicateAvoidingQueueMutatorTests
{
private IQueueMutator<AssetResponseDto> _queueMutator;

[SetUp]
public void Setup()
{
_queueMutator = new DuplicateAvoidingQueueMutator(2, new NullLogger<DuplicateAvoidingQueueMutator>());
}

[Test]
public void Mutate_NoItems_Succeeds()
{
IEnumerable<AssetResponseDto> result = [new()];

Assert.DoesNotThrow(() => result = _queueMutator.Mutate([]));
Assert.That(result, Is.Empty);
}

[Test]
public void Mutate_NoDuplicates_NoChange()
{
List<AssetResponseDto> original = [
new(){Id = "aap"},
new(){Id = "noot"},
new(){Id = "mies"},
new(){Id = "aardappel"},
];
IEnumerable<AssetResponseDto> result = [];
IEnumerable<AssetResponseDto> input = new List<AssetResponseDto>(original);

Assert.DoesNotThrow(() => result = _queueMutator.Mutate(input));
Assert.That(result, Is.EqualTo(original));
}

[Test]
public void Mutate_SeparatedDuplicates_NoChange()
{
List<AssetResponseDto> original = [
new(){Id = "aap"},
new(){Id = "mies"},
new(){Id = "noot"},
new(){Id = "mies"},
new(){Id = "aardappel"},
new(){Id = "mies"},
];
IEnumerable<AssetResponseDto> result = [];
IEnumerable<AssetResponseDto> input = new List<AssetResponseDto>(original);

Assert.DoesNotThrow(() => result = _queueMutator.Mutate(input));
Assert.That(result, Is.EqualTo(original));
}

[Test]
public void Mutate_AdjacentDuplicates_ShouldSeparate()
{
List<AssetResponseDto> original = [
new(){Id = "aap"},
new(){Id = "aap"},
new(){Id = "noot"},
new(){Id = "mies"},
new(){Id = "aardappel"},
];
IEnumerable<AssetResponseDto> result = [];
IEnumerable<AssetResponseDto> input = new List<AssetResponseDto>(original);

Assert.DoesNotThrow(() => result = _queueMutator.Mutate(input));
var assetResponseDtos = result as AssetResponseDto[] ?? result.ToArray();
for(int i = 0; i < assetResponseDtos.Length - 1; i++)
{
Assert.That(assetResponseDtos.ElementAt(i).Id, Is.Not.EqualTo(assetResponseDtos.ElementAt(i + 1).Id));
}
}
[Test]
public void Mutate_AdjacentDuplicates_differentBatch_NoChange()
{
List<AssetResponseDto> original = [
new(){Id = "aap"},
new(){Id = "noot"},
new(){Id = "noot"},
new(){Id = "mies"},
new(){Id = "aardappel"},
];
IEnumerable<AssetResponseDto> result = [];
IEnumerable<AssetResponseDto> input = new List<AssetResponseDto>(original);

Assert.DoesNotThrow(() => result = _queueMutator.Mutate(input));
Assert.That(result, Is.EqualTo(original));
}
[Test]
public void Mutate_LandscapeDuplicates_adjacent_NoChange()
{
List<AssetResponseDto> original = [
new(){Id = "aap"},
new(){Id = "peter"},
new(){Id = "noot", ExifInfo =new(){Orientation = "1", ExifImageWidth = 200, ExifImageHeight = 100}},
new(){Id = "noot", ExifInfo =new(){Orientation = "1", ExifImageWidth = 200, ExifImageHeight = 100}},
new(){Id = "mies"},
new(){Id = "aardappel"},
];
IEnumerable<AssetResponseDto> result = [];
IEnumerable<AssetResponseDto> input = new List<AssetResponseDto>(original);

Assert.DoesNotThrow(() => result = _queueMutator.Mutate(input));
Assert.That(result, Is.EqualTo(original));
}
}
35 changes: 35 additions & 0 deletions ImmichFrame.Core/Helpers/ImageHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,40 @@ public static Stream SaveDataUrlToStream(string dataUrl)

return new MemoryStream(binData);
}

public static double GetAspectRatioFloat(double width, double height)
{
if (width <= 0 || height <= 0)
throw new ArgumentException("Width and height must be positive.");

return width / height;
}

public static bool IsLandscape(double exifWidth, double exifHeight, string exifOrientation)
{
double width;
double height;
if (exifOrientation == "1"
|| exifOrientation == "2"
|| exifOrientation == "3"
|| exifOrientation == "4")
{
width = exifWidth;
height = exifHeight;
}else if (exifOrientation == "5"
|| exifOrientation == "6"
|| exifOrientation == "7"
|| exifOrientation == "8")
{
height = exifWidth;
width = exifHeight;
}
else
{
throw new ArgumentException(nameof(exifOrientation));
}

return GetAspectRatioFloat(width, height) > 1;
}
}
}
7 changes: 5 additions & 2 deletions ImmichFrame.Core/Logic/MultiImmichFrameLogicDelegate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using ImmichFrame.Core.Api;
using ImmichFrame.Core.Helpers;
using ImmichFrame.Core.Interfaces;
using ImmichFrame.Core.Logic.QueueMutator;
using ImmichFrame.Core.Models;
using Microsoft.Extensions.Logging;

Expand All @@ -13,13 +14,15 @@ public class MultiImmichFrameLogicDelegate : IImmichFrameLogic
private readonly IServerSettings _serverSettings;
private readonly IAccountSelectionStrategy _accountSelectionStrategy;
private readonly ILogger<MultiImmichFrameLogicDelegate> _logger;
private readonly IQueueMutator<AssetResponseDto> _queueMutator;

public MultiImmichFrameLogicDelegate(IServerSettings serverSettings,
Func<IAccountSettings, IAccountImmichFrameLogic> logicFactory, ILogger<MultiImmichFrameLogicDelegate> logger,
IAccountSelectionStrategy accountSelectionStrategy)
IAccountSelectionStrategy accountSelectionStrategy, IQueueMutator<AssetResponseDto> queueMutator)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_accountSelectionStrategy = accountSelectionStrategy;
_queueMutator = queueMutator;
_serverSettings = serverSettings;
_accountToDelegate = serverSettings.Accounts.ToFrozenDictionary(
keySelector: a => a,
Expand All @@ -32,7 +35,7 @@ public MultiImmichFrameLogicDelegate(IServerSettings serverSettings,


public async Task<IEnumerable<AssetResponseDto>> GetAssets()
=> (await _accountSelectionStrategy.GetAssets()).Shuffle().Select(it => it.ToAsset());
=> _queueMutator.Mutate((await _accountSelectionStrategy.GetAssets()).Select(it => it.ToAsset()));


public Task<AssetResponseDto> GetAssetInfoById(Guid assetId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using ImmichFrame.Core.Api;
using ImmichFrame.Core.Helpers;
using Microsoft.Extensions.Logging;

namespace ImmichFrame.Core.Logic.QueueMutator;

public class DuplicateAvoidingQueueMutator: QueueMutatorBase<AssetResponseDto>
{
private readonly int _batchSize;
private readonly ILogger<DuplicateAvoidingQueueMutator> _logger;

public DuplicateAvoidingQueueMutator(int batchSize, ILogger<DuplicateAvoidingQueueMutator> logger)
{
if(batchSize <= 0) throw new ArgumentException("Batch size must be greater than 0", nameof(batchSize));
_batchSize = batchSize;
_logger = logger;
}
public override IEnumerable<AssetResponseDto> Mutate(IEnumerable<AssetResponseDto> source)
{
var remaining = new Queue<AssetResponseDto>(source);
List<AssetResponseDto> result = new(remaining.Count);
var batch = new List<AssetResponseDto>(_batchSize);

int consecutiveDuplicates = 0;
while (remaining.Count > 0)
{
var currentItem = remaining.Dequeue();

bool landscape = false;
try
{

landscape = ImageHelper.IsLandscape(currentItem.ExifInfo.ExifImageWidth!.Value,
currentItem.ExifInfo.ExifImageHeight!.Value, currentItem.ExifInfo.Orientation);
}
catch
{
_logger.LogWarning("Failed to determine orientation for asset {AssetId}, defaulting to portrait", currentItem.Id);
}


if (!landscape && batch.Any(x => x.Id == currentItem.Id))
{
remaining.Enqueue(currentItem);
consecutiveDuplicates++;
// no other items to process, break to avoid infinite loop
if (consecutiveDuplicates >= remaining.Count) break;
continue;
}

batch.Add(currentItem);

if (batch.Count >= _batchSize)
{
result.AddRange(batch);
batch.Clear();
}

consecutiveDuplicates = 0;
}

if (batch.Count > 0)
{
result.AddRange(batch);
}

return Next?.Mutate(result) ?? result;
}
}
10 changes: 10 additions & 0 deletions ImmichFrame.Core/Logic/QueueMutator/IQueueMutator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Diagnostics.Contracts;

namespace ImmichFrame.Core.Logic.QueueMutator;

public interface IQueueMutator<T>
{
[Pure]
IEnumerable<T> Mutate(IEnumerable<T> source);
void SetNext(IQueueMutator<T> next);
}
13 changes: 13 additions & 0 deletions ImmichFrame.Core/Logic/QueueMutator/QueueMutatorBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace ImmichFrame.Core.Logic.QueueMutator;

public abstract class QueueMutatorBase<T>: IQueueMutator<T>
{
protected IQueueMutator<T>? Next;

public abstract IEnumerable<T> Mutate(IEnumerable<T> source);

public void SetNext(IQueueMutator<T> next)
{
Next = next;
}
}
11 changes: 11 additions & 0 deletions ImmichFrame.Core/Logic/QueueMutator/RandomQueueMutator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace ImmichFrame.Core.Logic.QueueMutator;

public class RandomQueueMutator<T>: QueueMutatorBase<T>
{
private readonly Random _random = new();
public override IEnumerable<T> Mutate(IEnumerable<T> source)
{
var result = source.OrderBy(_ => _random.Next());
return Next?.Mutate(result) ?? result;
}
}
10 changes: 10 additions & 0 deletions ImmichFrame.WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
using ImmichFrame.WebApi.Models;
using Microsoft.AspNetCore.Authentication;
using System.Reflection;
using ImmichFrame.Core.Api;
using ImmichFrame.Core.Logic;
using ImmichFrame.Core.Logic.AccountSelection;
using ImmichFrame.Core.Logic.QueueMutator;
using ImmichFrame.WebApi.Helpers.Config;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;

var builder = WebApplication.CreateBuilder(args);
//log the version number
Expand Down Expand Up @@ -73,6 +76,13 @@ _ _ __ ___ _ __ ___ _ ___| |__ | |_ _ __ __ _ _ __ ___ ___
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddTransient<IQueueMutator<AssetResponseDto>>(x =>
{
var mutator = new RandomQueueMutator<AssetResponseDto>();
mutator.SetNext(new DuplicateAvoidingQueueMutator(2, x.GetService<ILogger<DuplicateAvoidingQueueMutator>>()!));
return mutator;
});

builder.Services.AddAuthorization(options => { options.AddPolicy("AllowAnonymous", policy => policy.RequireAssertion(context => true)); });

builder.Services.AddAuthentication("ImmichFrameScheme")
Expand Down