diff --git a/Kyoo.Common/Utility/Merger.cs b/Kyoo.Common/Utility/Merger.cs
index cd860ea3..1918ba41 100644
--- a/Kyoo.Common/Utility/Merger.cs
+++ b/Kyoo.Common/Utility/Merger.cs
@@ -22,12 +22,13 @@ namespace Kyoo
/// The second enumerable to merge, if items from this list are equals to one from the first, they are not kept
/// Equality function to compare items. If this is null, duplicated elements are kept
/// The two list merged as an array
- public static T[] MergeLists(IEnumerable first,
- IEnumerable second,
- Func isEqual = null)
+ [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
+ public static T[] MergeLists([CanBeNull] IEnumerable first,
+ [CanBeNull] IEnumerable second,
+ [CanBeNull] Func isEqual = null)
{
if (first == null)
- return second.ToArray();
+ return second?.ToArray();
if (second == null)
return first.ToArray();
if (isEqual == null)
@@ -36,6 +37,31 @@ namespace Kyoo
return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
}
+ ///
+ /// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
+ ///
+ /// The first dictionary to merge
+ /// The second dictionary to merge
+ /// The type of the keys in dictionaries
+ /// The type of values in the dictionaries
+ /// A dictionary containing the result of the merge.
+ [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
+ public static IDictionary MergeDictionaries([CanBeNull] IDictionary first,
+ [CanBeNull] IDictionary second)
+ {
+ if (first == null)
+ return second;
+ if (second == null)
+ return first;
+ Dictionary merged = new();
+ merged.EnsureCapacity(first.Count + second.Count);
+ foreach ((T key, T2 value) in first)
+ merged.Add(key, value);
+ foreach ((T key, T2 value) in second)
+ merged.TryAdd(key, value);
+ return merged;
+ }
+
///
/// Set every fields of first to those of second. Ignore fields marked with the attribute
/// At the end, the OnMerge method of first will be called if first is a
@@ -63,16 +89,32 @@ namespace Kyoo
}
///
- /// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}.
+ /// Set every non-default values of seconds to the corresponding property of second.
+ /// Dictionaries are handled like anonymous objects with a property per key/pair value
+ /// (see for more details).
/// At the end, the OnMerge method of first will be called if first is a
///
- /// The object to complete
- /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
- /// Filter fields that will be merged
+ ///
+ /// This does the opposite of .
+ ///
+ ///
+ /// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"}
+ ///
+ ///
+ /// The object to complete
+ ///
+ ///
+ /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
+ ///
+ ///
+ /// Filter fields that will be merged
+ ///
/// Fields of T will be completed
///
/// If first is null
- public static T Complete([NotNull] T first, [CanBeNull] T second, Func where = null)
+ public static T Complete([NotNull] T first,
+ [CanBeNull] T second,
+ [InstantHandle] Func where = null)
{
if (first == null)
throw new ArgumentNullException(nameof(first));
@@ -93,7 +135,19 @@ namespace Kyoo
object defaultValue = property.GetCustomAttribute()?.Value
?? property.PropertyType.GetClrDefault();
- if (value?.Equals(defaultValue) == false && value != property.GetValue(first))
+ if (value?.Equals(defaultValue) != false || value == property.GetValue(first))
+ continue;
+ if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
+ {
+ Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))
+ .GenericTypeArguments;
+ property.SetValue(first, Utility.RunGenericMethod
/// The resource that will be saved
- /// You can throw this if the resource is illegal and should not be saved.
+ ///
+ /// You can throw this if the resource is illegal and should not be saved.
+ ///
protected virtual Task Validate(T resource)
{
if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute() != null)
diff --git a/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs b/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs
index 73691bf7..3fb9e72f 100644
--- a/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs
@@ -1,5 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
+using Microsoft.EntityFrameworkCore;
using Xunit;
using Xunit.Abstractions;
@@ -33,5 +38,161 @@ namespace Kyoo.Tests.Database
{
_repository = Repositories.LibraryManager.CollectionRepository;
}
+
+ [Fact]
+ public async Task CreateWithEmptySlugTest()
+ {
+ Collection collection = TestSample.GetNew();
+ collection.Slug = "";
+ await Assert.ThrowsAsync(() => _repository.Create(collection));
+ }
+
+ [Fact]
+ public async Task CreateWithNumberSlugTest()
+ {
+ Collection collection = TestSample.GetNew();
+ collection.Slug = "2";
+ Collection ret = await _repository.Create(collection);
+ Assert.Equal("2!", ret.Slug);
+ }
+
+ [Fact]
+ public async Task CreateWithoutNameTest()
+ {
+ Collection collection = TestSample.GetNew();
+ collection.Name = null;
+ await Assert.ThrowsAsync(() => _repository.Create(collection));
+ }
+
+ [Fact]
+ public async Task CreateWithExternalIdTest()
+ {
+ Collection collection = TestSample.GetNew();
+ collection.ExternalIDs = new[]
+ {
+ new MetadataID
+ {
+ Provider = TestSample.Get(),
+ Link = "link",
+ DataID = "id"
+ },
+ new MetadataID
+ {
+ Provider = TestSample.GetNew(),
+ Link = "new-provider-link",
+ DataID = "new-id"
+ }
+ };
+ await _repository.Create(collection);
+
+ Collection retrieved = await _repository.Get(2);
+ await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs);
+ Assert.Equal(2, retrieved.ExternalIDs.Count);
+ KAssert.DeepEqual(collection.ExternalIDs.First(), retrieved.ExternalIDs.First());
+ KAssert.DeepEqual(collection.ExternalIDs.Last(), retrieved.ExternalIDs.Last());
+ }
+
+ [Fact]
+ public async Task EditTest()
+ {
+ Collection value = await _repository.Get(TestSample.Get().Slug);
+ value.Name = "New Title";
+ value.Images = new Dictionary
+ {
+ [Images.Poster] = "poster"
+ };
+ await _repository.Edit(value, false);
+
+ await using DatabaseContext database = Repositories.Context.New();
+ Collection retrieved = await database.Collections.FirstAsync();
+
+ KAssert.DeepEqual(value, retrieved);
+ }
+
+ [Fact]
+ public async Task EditMetadataTest()
+ {
+ Collection value = await _repository.Get(TestSample.Get().Slug);
+ value.ExternalIDs = new[]
+ {
+ new MetadataID
+ {
+ Provider = TestSample.Get(),
+ Link = "link",
+ DataID = "id"
+ },
+ };
+ await _repository.Edit(value, false);
+
+ await using DatabaseContext database = Repositories.Context.New();
+ Collection retrieved = await database.Collections
+ .Include(x => x.ExternalIDs)
+ .ThenInclude(x => x.Provider)
+ .FirstAsync();
+
+ KAssert.DeepEqual(value, retrieved);
+ }
+
+ [Fact]
+ public async Task AddMetadataTest()
+ {
+ Collection value = await _repository.Get(TestSample.Get().Slug);
+ value.ExternalIDs = new List
+ {
+ new()
+ {
+ Provider = TestSample.Get(),
+ Link = "link",
+ DataID = "id"
+ },
+ };
+ await _repository.Edit(value, false);
+
+ {
+ await using DatabaseContext database = Repositories.Context.New();
+ Collection retrieved = await database.Collections
+ .Include(x => x.ExternalIDs)
+ .ThenInclude(x => x.Provider)
+ .FirstAsync();
+
+ KAssert.DeepEqual(value, retrieved);
+ }
+
+ value.ExternalIDs.Add(new MetadataID
+ {
+ Provider = TestSample.GetNew(),
+ Link = "link",
+ DataID = "id"
+ });
+ await _repository.Edit(value, false);
+
+ {
+ await using DatabaseContext database = Repositories.Context.New();
+ Collection retrieved = await database.Collections
+ .Include(x => x.ExternalIDs)
+ .ThenInclude(x => x.Provider)
+ .FirstAsync();
+
+ KAssert.DeepEqual(value, retrieved);
+ }
+ }
+
+ [Theory]
+ [InlineData("test")]
+ [InlineData("super")]
+ [InlineData("title")]
+ [InlineData("TiTlE")]
+ [InlineData("SuPeR")]
+ public async Task SearchTest(string query)
+ {
+ Collection value = new()
+ {
+ Slug = "super-test",
+ Name = "This is a test title",
+ };
+ await _repository.Create(value);
+ ICollection ret = await _repository.Search(query);
+ KAssert.DeepEqual(value, ret.First());
+ }
}
}
\ No newline at end of file
diff --git a/Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs b/Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs
index e6a913d2..f3e031b2 100644
--- a/Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs
+++ b/Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs
@@ -79,9 +79,10 @@ namespace Kyoo.Tests.Database
[Fact]
public async Task GetDuplicatedSlugTests()
{
- await _repositories.LibraryManager.Create(new Collection()
+ await _repositories.LibraryManager.Create(new Collection
{
- Slug = TestSample.Get().Slug
+ Slug = TestSample.Get().Slug,
+ Name = "name"
});
await Assert.ThrowsAsync(() => _repository.Get(TestSample.Get().Slug));
}
diff --git a/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs b/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs
index 12449b5e..79277f61 100644
--- a/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs
@@ -61,7 +61,7 @@ namespace Kyoo.Tests.Database
Library library = TestSample.GetNew();
library.Slug = "2";
Library ret = await _repository.Create(library);
- Assert.Equal("2!", library.Slug);
+ Assert.Equal("2!", ret.Slug);
}
[Fact]
diff --git a/Kyoo.Tests/Database/TestSample.cs b/Kyoo.Tests/Database/TestSample.cs
index d8b06333..ff79ccf9 100644
--- a/Kyoo.Tests/Database/TestSample.cs
+++ b/Kyoo.Tests/Database/TestSample.cs
@@ -18,6 +18,20 @@ namespace Kyoo.Tests
Paths = new [] {"/a/random/path"}
}
},
+ {
+ typeof(Collection),
+ () => new Collection
+ {
+ ID = 2,
+ Slug = "new-collection",
+ Name = "New Collection",
+ Overview = "A collection created by new sample",
+ Images = new Dictionary
+ {
+ [Images.Thumbnail] = "thumbnail"
+ }
+ }
+ },
{
typeof(Show),
() => new Show
diff --git a/Kyoo.Tests/Utility/MergerTests.cs b/Kyoo.Tests/Utility/MergerTests.cs
index 285532c2..4e0ed6e7 100644
--- a/Kyoo.Tests/Utility/MergerTests.cs
+++ b/Kyoo.Tests/Utility/MergerTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Kyoo.Models;
@@ -135,6 +136,68 @@ namespace Kyoo.Tests.Utility
Assert.Equal(3, ret.Numbers[3]);
}
+ private class MergeDictionaryTest
+ {
+ public int ID { get; set; }
+
+ public Dictionary Dictionary { get; set; }
+ }
+
+ [Fact]
+ public void GlobalMergeDictionariesTest()
+ {
+ MergeDictionaryTest test = new()
+ {
+ ID = 5,
+ Dictionary = new Dictionary
+ {
+ [2] = "two"
+ }
+ };
+ MergeDictionaryTest test2 = new()
+ {
+ Dictionary = new Dictionary
+ {
+ [3] = "third"
+ }
+ };
+ MergeDictionaryTest ret = Merger.Merge(test, test2);
+ Assert.True(ReferenceEquals(test, ret));
+ Assert.Equal(5, ret.ID);
+
+ Assert.Equal(2, ret.Dictionary.Count);
+ Assert.Equal("two", ret.Dictionary[2]);
+ Assert.Equal("third", ret.Dictionary[3]);
+ }
+
+ [Fact]
+ public void GlobalMergeDictionariesDuplicatesTest()
+ {
+ MergeDictionaryTest test = new()
+ {
+ ID = 5,
+ Dictionary = new Dictionary
+ {
+ [2] = "two"
+ }
+ };
+ MergeDictionaryTest test2 = new()
+ {
+ Dictionary = new Dictionary
+ {
+ [2] = "nope",
+ [3] = "third"
+ }
+ };
+ MergeDictionaryTest ret = Merger.Merge(test, test2);
+ Assert.True(ReferenceEquals(test, ret));
+ Assert.Equal(5, ret.ID);
+
+ Assert.Equal(2, ret.Dictionary.Count);
+ Assert.Equal("two", ret.Dictionary[2]);
+ Assert.Equal("third", ret.Dictionary[3]);
+ }
+
[Fact]
public void GlobalMergeListDuplicatesResourcesTest()
{
@@ -208,5 +271,99 @@ namespace Kyoo.Tests.Utility
Assert.Equal(1, ret[0]);
Assert.Equal(2, ret[1]);
}
+
+ [Fact]
+ public void MergeDictionariesTest()
+ {
+ Dictionary first = new()
+ {
+ [1] = "test",
+ [5] = "value"
+ };
+ Dictionary second = new()
+ {
+ [3] = "third",
+ };
+ IDictionary ret = Merger.MergeDictionaries(first, second);
+
+ Assert.Equal(3, ret.Count);
+ Assert.Equal("test", ret[1]);
+ Assert.Equal("value", ret[5]);
+ Assert.Equal("third", ret[3]);
+ }
+
+ [Fact]
+ public void MergeDictionariesDuplicateTest()
+ {
+ Dictionary first = new()
+ {
+ [1] = "test",
+ [5] = "value"
+ };
+ Dictionary second = new()
+ {
+ [3] = "third",
+ [5] = "new-value",
+ };
+ IDictionary ret = Merger.MergeDictionaries(first, second);
+
+ Assert.Equal(3, ret.Count);
+ Assert.Equal("test", ret[1]);
+ Assert.Equal("value", ret[5]);
+ Assert.Equal("third", ret[3]);
+ }
+
+ [Fact]
+ public void CompleteTest()
+ {
+ Genre genre = new()
+ {
+ ID = 5,
+ Name = "merged"
+ };
+ Genre genre2 = new()
+ {
+ Name = "test"
+ };
+ Genre ret = Merger.Complete(genre, genre2);
+ Assert.True(ReferenceEquals(genre, ret));
+ Assert.Equal(5, ret.ID);
+ Assert.Equal("test", genre.Name);
+ Assert.Null(genre.Slug);
+ }
+
+ [Fact]
+ public void CompleteDictionaryTest()
+ {
+ Collection collection = new()
+ {
+ ID = 5,
+ Name = "merged",
+ Images = new Dictionary
+ {
+ [Images.Logo] = "logo",
+ [Images.Poster] = "poster"
+ }
+
+ };
+ Collection collection2 = new()
+ {
+ Name = "test",
+ Images = new Dictionary
+ {
+ [Images.Poster] = "new-poster",
+ [Images.Thumbnail] = "thumbnails"
+ }
+ };
+ Collection ret = Merger.Complete(collection, collection2);
+ Assert.True(ReferenceEquals(collection, ret));
+ Assert.Equal(5, ret.ID);
+ Assert.Equal("test", ret.Name);
+ Assert.Null(ret.Slug);
+ Assert.Equal(3, ret.Images.Count);
+ Assert.Equal("new-poster", ret.Images[Images.Poster]);
+ Assert.Equal("thumbnails", ret.Images[Images.Thumbnail]);
+ Assert.Equal("logo", ret.Images[Images.Logo]);
+ }
}
}
\ No newline at end of file
diff --git a/Kyoo/Controllers/Repositories/CollectionRepository.cs b/Kyoo/Controllers/Repositories/CollectionRepository.cs
index 42483b08..016028cb 100644
--- a/Kyoo/Controllers/Repositories/CollectionRepository.cs
+++ b/Kyoo/Controllers/Repositories/CollectionRepository.cs
@@ -42,7 +42,7 @@ namespace Kyoo.Controllers
public override async Task> Search(string query)
{
return await _database.Collections
- .Where(_database.Like(x => x.Name, $"%{query}%"))
+ .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
@@ -53,7 +53,6 @@ namespace Kyoo.Controllers
{
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
- obj.ExternalIDs.ForEach(x => _database.MetadataIds().Attach(x));
await _database.SaveChangesAsync($"Trying to insert a duplicated collection (slug {obj.Slug} already exists).");
return obj;
}
@@ -62,24 +61,34 @@ namespace Kyoo.Controllers
protected override async Task Validate(Collection resource)
{
await base.Validate(resource);
- await resource.ExternalIDs.ForEachAsync(async x =>
- {
- x.Provider = await _providers.CreateIfNotExists(x.Provider);
- x.ProviderID = x.Provider.ID;
- _database.Entry(x.Provider).State = EntityState.Detached;
- });
+
+ if (string.IsNullOrEmpty(resource.Slug))
+ throw new ArgumentException("The collection's slug must be set and not empty");
+ if (string.IsNullOrEmpty(resource.Name))
+ throw new ArgumentException("The collection's name must be set and not empty");
+
+ if (resource.ExternalIDs != null)
+ {
+ foreach (MetadataID id in resource.ExternalIDs)
+ {
+ id.Provider = await _providers.CreateIfNotExists(id.Provider);
+ id.ProviderID = id.Provider.ID;
+ _database.Entry(id.Provider).State = EntityState.Detached;
+ }
+ _database.MetadataIds().AttachRange(resource.ExternalIDs);
+ }
}
///
protected override async Task EditRelations(Collection resource, Collection changed, bool resetOld)
{
+ await Validate(resource);
+
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
-
- await base.EditRelations(resource, changed, resetOld);
}
///