- `CustomFormatCache` is now a utility class for updating cache entries. - `CustomFormatCacheData` is now what `CustomFormatCache` used to be (data object used for serialization). - `CustomFormatCachePersister` is now specific to custom formats. Future cache types will have their own persister implementation.pull/231/head
parent
7f6a5a2ff6
commit
e99f4cb766
@ -1,35 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Cli.Cache;
|
||||
|
||||
public class CachePersister(ILogger log, IServiceCache serviceCache) : ICachePersister
|
||||
{
|
||||
public CustomFormatCache Load(IServiceConfiguration config)
|
||||
{
|
||||
var cache = serviceCache.Load<CustomFormatCache>(config);
|
||||
if (cache == null)
|
||||
{
|
||||
log.Debug("Custom format cache does not exist; proceeding without it");
|
||||
return new CustomFormatCache();
|
||||
}
|
||||
|
||||
// If the version is higher OR lower, we invalidate the cache. It means there's an
|
||||
// incompatibility that we do not support.
|
||||
if (cache.Version != CustomFormatCache.LatestVersion)
|
||||
{
|
||||
log.Information("Cache version mismatch ({OldVersion} vs {LatestVersion}); ignoring cache data",
|
||||
cache.Version, CustomFormatCache.LatestVersion);
|
||||
throw new CacheException("Version mismatch");
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
public void Save(IServiceConfiguration config, CustomFormatCache cache)
|
||||
{
|
||||
log.Debug("Saving Cache with {Mappings}", JsonSerializer.Serialize(cache.TrashIdMappings));
|
||||
|
||||
serviceCache.Save(cache, config);
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.Cli.Pipelines.CustomFormat;
|
||||
using Recyclarr.TrashGuide.CustomFormat;
|
||||
|
||||
namespace Recyclarr.Cli.Cache;
|
||||
|
||||
[CacheObjectName("custom-format-cache")]
|
||||
public record CustomFormatCache
|
||||
{
|
||||
public const int LatestVersion = 1;
|
||||
|
||||
public int Version { get; init; } = LatestVersion;
|
||||
|
||||
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
|
||||
public string? InstanceName { get; init; }
|
||||
|
||||
public IReadOnlyList<TrashIdMapping> TrashIdMappings { get; init; } = new List<TrashIdMapping>();
|
||||
|
||||
public CustomFormatCache Update(CustomFormatTransactionData transactions)
|
||||
{
|
||||
// Assume that RemoveStale() is called before this method, and that TrashIdMappings contains existing CFs
|
||||
// in the remote service that we want to keep and update.
|
||||
|
||||
var existingCfs = transactions.UpdatedCustomFormats
|
||||
.Concat(transactions.UnchangedCustomFormats)
|
||||
.Concat(transactions.NewCustomFormats);
|
||||
|
||||
return this with
|
||||
{
|
||||
TrashIdMappings = TrashIdMappings
|
||||
.DistinctBy(x => x.CustomFormatId)
|
||||
.Where(x => transactions.DeletedCustomFormats.All(y => y.CustomFormatId != x.CustomFormatId))
|
||||
.FullOuterJoin(existingCfs, JoinType.Hash,
|
||||
l => l.CustomFormatId,
|
||||
r => r.Id,
|
||||
// Keep existing service CFs, even if they aren't in user config
|
||||
l => l,
|
||||
// Add a new mapping for CFs in user's config
|
||||
r => new TrashIdMapping(r.TrashId, r.Name, r.Id),
|
||||
// Update existing mappings for CFs in user's config
|
||||
(l, r) => l with {TrashId = r.TrashId, CustomFormatName = r.Name})
|
||||
.Where(x => x.CustomFormatId != 0)
|
||||
.OrderBy(x => x.CustomFormatId)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public CustomFormatCache RemoveStale(IEnumerable<CustomFormatData> serviceCfs)
|
||||
{
|
||||
return this with
|
||||
{
|
||||
TrashIdMappings = TrashIdMappings
|
||||
.Where(x => x.CustomFormatId != 0 && serviceCfs.Any(y => y.Id == x.CustomFormatId))
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public int? FindId(CustomFormatData cf)
|
||||
{
|
||||
return TrashIdMappings.FirstOrDefault(c => c.TrashId == cf.TrashId)?.CustomFormatId;
|
||||
}
|
||||
}
|
||||
|
||||
public record TrashIdMapping(string TrashId, string CustomFormatName, int CustomFormatId);
|
@ -1,7 +1,7 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Cli.Console.Helpers;
|
||||
namespace Recyclarr.Cli.Cache;
|
||||
|
||||
public interface ICacheStoragePath
|
||||
{
|
@ -0,0 +1,46 @@
|
||||
using Recyclarr.TrashGuide.CustomFormat;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.CustomFormat.Cache;
|
||||
|
||||
public class CustomFormatCache(IEnumerable<TrashIdMapping> mappings)
|
||||
{
|
||||
private List<TrashIdMapping> _mappings = mappings.ToList(); // Deep clone with ToList()
|
||||
|
||||
public IReadOnlyList<TrashIdMapping> Mappings => _mappings;
|
||||
|
||||
public void Update(CustomFormatTransactionData transactions)
|
||||
{
|
||||
// Assume that RemoveStale() is called before this method, and that TrashIdMappings contains existing CFs
|
||||
// in the remote service that we want to keep and update.
|
||||
|
||||
var existingCfs = transactions.UpdatedCustomFormats
|
||||
.Concat(transactions.UnchangedCustomFormats)
|
||||
.Concat(transactions.NewCustomFormats);
|
||||
|
||||
_mappings = _mappings
|
||||
.DistinctBy(x => x.CustomFormatId)
|
||||
.Where(x => transactions.DeletedCustomFormats.All(y => y.CustomFormatId != x.CustomFormatId))
|
||||
.FullOuterJoin(existingCfs, JoinType.Hash,
|
||||
l => l.CustomFormatId,
|
||||
r => r.Id,
|
||||
// Keep existing service CFs, even if they aren't in user config
|
||||
l => l,
|
||||
// Add a new mapping for CFs in user's config
|
||||
r => new TrashIdMapping(r.TrashId, r.Name, r.Id),
|
||||
// Update existing mappings for CFs in user's config
|
||||
(l, r) => l with {TrashId = r.TrashId, CustomFormatName = r.Name})
|
||||
.Where(x => x.CustomFormatId != 0)
|
||||
.OrderBy(x => x.CustomFormatId)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void RemoveStale(IEnumerable<CustomFormatData> serviceCfs)
|
||||
{
|
||||
_mappings.RemoveAll(x => x.CustomFormatId == 0 || serviceCfs.All(y => y.Id != x.CustomFormatId));
|
||||
}
|
||||
|
||||
public int? FindId(CustomFormatData cf)
|
||||
{
|
||||
return _mappings.Find(c => c.TrashId == cf.TrashId)?.CustomFormatId;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using Recyclarr.Cli.Cache;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.CustomFormat.Cache;
|
||||
|
||||
public record TrashIdMapping(string TrashId, string CustomFormatName, int CustomFormatId);
|
||||
|
||||
[CacheObjectName("custom-format-cache")]
|
||||
public record CustomFormatCacheData(
|
||||
int Version,
|
||||
string InstanceName,
|
||||
IReadOnlyCollection<TrashIdMapping> TrashIdMappings);
|
@ -0,0 +1,38 @@
|
||||
using System.Text.Json;
|
||||
using Recyclarr.Cli.Cache;
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.CustomFormat.Cache;
|
||||
|
||||
public class CustomFormatCachePersister(ILogger log, IServiceCache serviceCache) : ICustomFormatCachePersister
|
||||
{
|
||||
public const int LatestVersion = 1;
|
||||
|
||||
public CustomFormatCache Load(IServiceConfiguration config)
|
||||
{
|
||||
var cacheData = serviceCache.Load<CustomFormatCacheData>(config);
|
||||
if (cacheData == null)
|
||||
{
|
||||
log.Debug("Custom format cache does not exist; proceeding without it");
|
||||
cacheData = new CustomFormatCacheData(LatestVersion, config.InstanceName, []);
|
||||
}
|
||||
|
||||
// If the version is higher OR lower, we invalidate the cache. It means there's an
|
||||
// incompatibility that we do not support.
|
||||
if (cacheData.Version != LatestVersion)
|
||||
{
|
||||
log.Information("Cache version mismatch ({OldVersion} vs {LatestVersion}); ignoring cache data",
|
||||
cacheData.Version, LatestVersion);
|
||||
throw new CacheException("Version mismatch");
|
||||
}
|
||||
|
||||
return new CustomFormatCache(cacheData.TrashIdMappings);
|
||||
}
|
||||
|
||||
public void Save(IServiceConfiguration config, CustomFormatCache cache)
|
||||
{
|
||||
var data = new CustomFormatCacheData(LatestVersion, config.InstanceName, cache.Mappings);
|
||||
log.Debug("Saving Custom Format Cache with {Mappings}", JsonSerializer.Serialize(data.TrashIdMappings));
|
||||
serviceCache.Save(data, config);
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Cli.Cache;
|
||||
namespace Recyclarr.Cli.Pipelines.CustomFormat.Cache;
|
||||
|
||||
public interface ICachePersister
|
||||
public interface ICustomFormatCachePersister
|
||||
{
|
||||
CustomFormatCache Load(IServiceConfiguration config);
|
||||
void Save(IServiceConfiguration config, CustomFormatCache cache);
|
@ -1,85 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Recyclarr.Cli.Cache;
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Cache;
|
||||
|
||||
[TestFixture]
|
||||
public class CachePersisterTest
|
||||
{
|
||||
private sealed class Context
|
||||
{
|
||||
public Context()
|
||||
{
|
||||
var log = Substitute.For<ILogger>();
|
||||
ServiceCache = Substitute.For<IServiceCache>();
|
||||
Persister = new CachePersister(log, ServiceCache);
|
||||
}
|
||||
|
||||
public CachePersister Persister { get; }
|
||||
public IServiceCache ServiceCache { get; }
|
||||
}
|
||||
|
||||
[TestCase(CustomFormatCache.LatestVersion - 1)]
|
||||
[TestCase(CustomFormatCache.LatestVersion + 1)]
|
||||
public void Throw_when_versions_mismatch(int versionToTest)
|
||||
{
|
||||
var ctx = new Context();
|
||||
var config = Substitute.For<IServiceConfiguration>();
|
||||
|
||||
var testCfObj = new CustomFormatCache
|
||||
{
|
||||
Version = versionToTest,
|
||||
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)}
|
||||
};
|
||||
|
||||
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
|
||||
|
||||
var act = () => ctx.Persister.Load(config);
|
||||
|
||||
act.Should().Throw<CacheException>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Accept_loaded_cache_when_versions_match()
|
||||
{
|
||||
var ctx = new Context();
|
||||
var config = Substitute.For<IServiceConfiguration>();
|
||||
|
||||
var testCfObj = new CustomFormatCache
|
||||
{
|
||||
Version = CustomFormatCache.LatestVersion,
|
||||
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)}
|
||||
};
|
||||
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
|
||||
var result = ctx.Persister.Load(config);
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Cache_is_valid_after_successful_load()
|
||||
{
|
||||
var ctx = new Context();
|
||||
var testCfObj = new CustomFormatCache();
|
||||
var config = Substitute.For<IServiceConfiguration>();
|
||||
|
||||
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
|
||||
var result = ctx.Persister.Load(config);
|
||||
result.Should().BeSameAs(testCfObj);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Save_works_with_valid_cf_cache()
|
||||
{
|
||||
var ctx = new Context();
|
||||
var testCfObj = new CustomFormatCache();
|
||||
var config = Substitute.For<IServiceConfiguration>();
|
||||
|
||||
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
|
||||
|
||||
var result = ctx.Persister.Load(config);
|
||||
ctx.Persister.Save(config, result);
|
||||
|
||||
ctx.ServiceCache.Received().Save(testCfObj, config);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using Recyclarr.Cli.Console.Helpers;
|
||||
using Recyclarr.Cli.Cache;
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Console.Helpers;
|
||||
namespace Recyclarr.Cli.Tests.Cache;
|
||||
|
||||
[TestFixture]
|
||||
public class CacheStoragePathTest
|
@ -0,0 +1,70 @@
|
||||
using AutoFixture;
|
||||
using Recyclarr.Cli.Cache;
|
||||
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.CustomFormat.Cache;
|
||||
|
||||
[TestFixture]
|
||||
public class CustomFormatCachePersisterTest
|
||||
{
|
||||
[TestCase(CustomFormatCachePersister.LatestVersion - 1)]
|
||||
[TestCase(CustomFormatCachePersister.LatestVersion + 1)]
|
||||
public void Throw_when_versions_mismatch(int versionToTest)
|
||||
{
|
||||
var fixture = NSubstituteFixture.Create();
|
||||
var serviceCache = fixture.Freeze<IServiceCache>();
|
||||
var sut = fixture.Create<CustomFormatCachePersister>();
|
||||
|
||||
var config = Substitute.For<IServiceConfiguration>();
|
||||
|
||||
var testCfObj = new CustomFormatCacheData(versionToTest, "",
|
||||
[
|
||||
new TrashIdMapping("", "", 5)
|
||||
]);
|
||||
|
||||
serviceCache.Load<CustomFormatCacheData>(config).Returns(testCfObj);
|
||||
|
||||
var act = () => sut.Load(config);
|
||||
|
||||
act.Should().Throw<CacheException>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Accept_loaded_cache_when_versions_match()
|
||||
{
|
||||
var fixture = NSubstituteFixture.Create();
|
||||
var serviceCache = fixture.Freeze<IServiceCache>();
|
||||
var sut = fixture.Create<CustomFormatCachePersister>();
|
||||
|
||||
var config = Substitute.For<IServiceConfiguration>();
|
||||
|
||||
var testCfObj = new CustomFormatCacheData(CustomFormatCachePersister.LatestVersion, "",
|
||||
[
|
||||
new TrashIdMapping("", "", 5)
|
||||
]);
|
||||
|
||||
serviceCache.Load<CustomFormatCacheData>(config).Returns(testCfObj);
|
||||
|
||||
var result = sut.Load(config);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Cache_is_valid_after_successful_load()
|
||||
{
|
||||
var fixture = NSubstituteFixture.Create();
|
||||
var serviceCache = fixture.Freeze<IServiceCache>();
|
||||
var sut = fixture.Create<CustomFormatCachePersister>();
|
||||
|
||||
TrashIdMapping[] mappings = [new TrashIdMapping("abc", "name", 123)];
|
||||
var config = Substitute.For<IServiceConfiguration>();
|
||||
|
||||
serviceCache.Load<CustomFormatCacheData>(config).Returns(new CustomFormatCacheData(1, "", mappings));
|
||||
|
||||
var result = sut.Load(config);
|
||||
|
||||
result.Mappings.Should().BeEquivalentTo(mappings);
|
||||
}
|
||||
}
|
Loading…
Reference in new issue