diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index 5154a72c..7799859f 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -15,6 +15,7 @@ + diff --git a/Kyoo.Common/Models/Attributes/MergeAttributes.cs b/Kyoo.Common/Models/Attributes/MergeAttributes.cs new file mode 100644 index 00000000..57861f61 --- /dev/null +++ b/Kyoo.Common/Models/Attributes/MergeAttributes.cs @@ -0,0 +1,6 @@ +using System; + +namespace Kyoo.Models.Attributes +{ + public class NotMergableAttribute : Attribute { } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Collection.cs b/Kyoo.Common/Models/Collection.cs index ce6ba4c9..c0297521 100644 --- a/Kyoo.Common/Models/Collection.cs +++ b/Kyoo.Common/Models/Collection.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using System.Collections.Generic; using System.Linq; +using Kyoo.Models.Attributes; namespace Kyoo.Models { @@ -12,7 +13,7 @@ namespace Kyoo.Models public string Poster { get; set; } public string Overview { get; set; } [JsonIgnore] public string ImgPrimary { get; set; } - [JsonIgnore] public virtual IEnumerable Links { get; set; } + [NotMergable] [JsonIgnore] public virtual IEnumerable Links { get; set; } public virtual IEnumerable Shows { get => Links.Select(x => x.Show); diff --git a/Kyoo.Common/Models/Library.cs b/Kyoo.Common/Models/Library.cs index d6a7e702..354507d3 100644 --- a/Kyoo.Common/Models/Library.cs +++ b/Kyoo.Common/Models/Library.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Kyoo.Models.Attributes; using Newtonsoft.Json; namespace Kyoo.Models @@ -8,14 +11,34 @@ namespace Kyoo.Models [JsonIgnore] public long ID { get; set; } public string Slug { get; set; } public string Name { get; set; } - public string[] Paths { get; set; } - public virtual IEnumerable Providers { get; set; } - [JsonIgnore] public virtual IEnumerable Shows { get; set; } - [JsonIgnore] public virtual IEnumerable Collections { get; set; } + public IEnumerable Paths { get; set; } + + public IEnumerable Providers + { + get => ProviderLinks.Select(x => x.Provider); + set => ProviderLinks = value.Select(x => new ProviderLink(x, this)); + } + [NotMergable] [JsonIgnore] public virtual IEnumerable ProviderLinks { get; set; } + [NotMergable] [JsonIgnore] public virtual IEnumerable Links { get; set; } + + [JsonIgnore] public IEnumerable Shows + { + get => Links.Where(x => x.Show != null).Select(x => x.Show); + set => Links = Utility.MergeLists( + value?.Select(x => new LibraryLink(this, x)), + Links?.Where(x => x.Show == null)); + } + [JsonIgnore] public IEnumerable Collections + { + get => Links.Where(x => x.Collection != null).Select(x => x.Collection); + set => Links = Utility.MergeLists( + value?.Select(x => new LibraryLink(this, x)), + Links?.Where(x => x.Collection == null)); + } public Library() { } - public Library(string slug, string name, string[] paths, IEnumerable providers) + public Library(string slug, string name, IEnumerable paths, IEnumerable providers) { Slug = slug; Name = name; diff --git a/Kyoo.Common/Models/LibraryLink.cs b/Kyoo.Common/Models/LibraryLink.cs index 86635adf..36cfc749 100644 --- a/Kyoo.Common/Models/LibraryLink.cs +++ b/Kyoo.Common/Models/LibraryLink.cs @@ -9,5 +9,19 @@ namespace Kyoo.Models public virtual Show Show { get; set; } public long? CollectionID { get; set; } public virtual Collection Collection { get; set; } + + public LibraryLink() { } + + public LibraryLink(Library library, Show show) + { + Library = library; + Show = show; + } + + public LibraryLink(Library library, Collection collection) + { + Library = library; + Collection = collection; + } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/ProviderID.cs b/Kyoo.Common/Models/ProviderID.cs index fb90f648..e7d76268 100644 --- a/Kyoo.Common/Models/ProviderID.cs +++ b/Kyoo.Common/Models/ProviderID.cs @@ -16,26 +16,5 @@ namespace Kyoo.Models Name = name; Logo = logo; } - - protected bool Equals(ProviderID other) - { - return Name == other.Name; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != this.GetType()) - return false; - return Equals((ProviderID)obj); - } - - public override int GetHashCode() - { - return Name != null ? Name.GetHashCode() : 0; - } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/ProviderLink.cs b/Kyoo.Common/Models/ProviderLink.cs index 6bf0eacf..088e4f80 100644 --- a/Kyoo.Common/Models/ProviderLink.cs +++ b/Kyoo.Common/Models/ProviderLink.cs @@ -10,30 +10,12 @@ namespace Kyoo.Models [JsonIgnore] public long? LibraryID { get; set; } [JsonIgnore] public virtual Library Library { get; set; } - public string Name - { - get => Provider?.Name; - set - { - if (Provider != null) - Provider.Name = value; - else - Provider = new ProviderID {Name = value}; - } - } - - public string Logo - { - get => Provider?.Logo; - set - { - if (Provider != null) - Provider.Logo = value; - else - Provider = new ProviderID {Logo = value}; - } - } - public ProviderLink() { } + + public ProviderLink(ProviderID provider, Library library) + { + Provider = provider; + Library = library; + } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Show.cs b/Kyoo.Common/Models/Show.cs index 8d66e7c7..f6e9578b 100644 --- a/Kyoo.Common/Models/Show.cs +++ b/Kyoo.Common/Models/Show.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using System.Collections.Generic; using System.Linq; +using Kyoo.Models.Attributes; namespace Kyoo.Models { @@ -10,7 +11,7 @@ namespace Kyoo.Models public string Slug { get; set; } public string Title { get; set; } - public string[] Aliases { get; set; } + public IEnumerable Aliases { get; set; } [JsonIgnore] public string Path { get; set; } public string Overview { get; set; } public Status? Status { get; set; } @@ -31,10 +32,10 @@ namespace Kyoo.Models public virtual IEnumerable Genres { - get { return GenreLinks?.Select(x => x.Genre).OrderBy(x => x.Name); } - set { GenreLinks = value?.Select(x => new GenreLink(this, x)).ToList(); } + get => GenreLinks?.Select(x => x.Genre); + set => GenreLinks = value?.Select(x => new GenreLink(this, x)).ToList(); } - [JsonIgnore] public virtual IEnumerable GenreLinks { get; set; } + [NotMergable] [JsonIgnore] public virtual IEnumerable GenreLinks { get; set; } public virtual Studio Studio { get; set; } [JsonIgnore] public virtual IEnumerable People { get; set; } [JsonIgnore] public virtual IEnumerable Seasons { get; set; } @@ -55,7 +56,7 @@ namespace Kyoo.Models { Slug = slug; Title = title; - Aliases = aliases?.ToArray(); + Aliases = aliases; Path = path; Overview = overview; TrailerUrl = trailerUrl; @@ -83,7 +84,7 @@ namespace Kyoo.Models { Slug = slug; Title = title; - Aliases = aliases?.ToArray(); + Aliases = aliases; Path = path; Overview = overview; TrailerUrl = trailerUrl; diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index 19faf52f..7e1177e2 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -1,13 +1,14 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using JetBrains.Annotations; using Kyoo.Models; +using Kyoo.Models.Attributes; namespace Kyoo { @@ -64,9 +65,9 @@ namespace Kyoo return second; if (second == null) return first; - List list = first.ToList(); if (isEqual == null) - isEqual = (x, y) => x.Equals(y); + return first.Concat(second).ToList(); + List list = first.ToList(); return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToList(); } @@ -92,9 +93,18 @@ namespace Kyoo public static T Merge(T first, T second) { + // TODO During the merge, reference to the second values are not set to the first value (for child objects). + if (first == null) + return second; + if (second == null) + return first; + Type type = typeof(T); foreach (PropertyInfo property in type.GetProperties().Where(x => x.CanRead && x.CanWrite)) { + if (Attribute.GetCustomAttribute(property, typeof(NotMergableAttribute)) != null) + continue; + object oldValue = property.GetValue(first); object newValue = property.GetValue(second); object defaultValue = property.PropertyType.IsValueType @@ -106,7 +116,11 @@ namespace Kyoo else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) && property.PropertyType != typeof(string)) { - property.SetValue((IEnumerable)oldValue, (IEnumerable)newValue); + property.SetValue(first, RunGenericMethod( + typeof(Utility), + "MergeLists", + GetEnumerableType(property.PropertyType), + new []{ oldValue, newValue, null})); } } @@ -130,7 +144,26 @@ namespace Kyoo return obj; } - public static object RunGenericMethod([NotNull] object instance, + public static object RunGenericMethod( + [NotNull] Type owner, + [NotNull] string methodName, + [NotNull] Type type, + IEnumerable args) + { + if (owner == null) + throw new ArgumentNullException(nameof(owner)); + if (methodName == null) + throw new ArgumentNullException(nameof(methodName)); + if (type == null) + throw new ArgumentNullException(nameof(type)); + MethodInfo method = owner.GetMethod(methodName); + if (method == null) + throw new NullReferenceException($"A method named {methodName} could not be found on {owner.FullName}"); + return method.MakeGenericMethod(type).Invoke(null, args?.ToArray()); + } + + public static object RunGenericMethod( + [NotNull] object instance, [NotNull] string methodName, [NotNull] Type type, IEnumerable args) @@ -147,5 +180,24 @@ namespace Kyoo return method.MakeGenericMethod(type).Invoke(instance, args?.ToArray()); } + public static Type GetEnumerableType([NoEnumeration] [NotNull] IEnumerable list) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + Type type = list.GetType().GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t) + && t.GetGenericArguments().Any()) ?? list.GetType(); + return type.GetGenericArguments().First(); + } + + public static Type GetEnumerableType([NotNull] Type listType) + { + if (listType == null) + throw new ArgumentNullException(nameof(listType)); + if (!typeof(IEnumerable).IsAssignableFrom(listType)) + throw new InvalidOperationException($"The {nameof(listType)} parameter was not an IEnumerable."); + Type type = listType.GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t) + && t.GetGenericArguments().Any()) ?? listType; + return type.GetGenericArguments().First(); + } } } \ No newline at end of file diff --git a/Kyoo/Controllers/LibraryManager.cs b/Kyoo/Controllers/LibraryManager.cs index b0841e48..f024981a 100644 --- a/Kyoo/Controllers/LibraryManager.cs +++ b/Kyoo/Controllers/LibraryManager.cs @@ -181,7 +181,28 @@ namespace Kyoo.Controllers { if (obj == null) return; - _database.Add(obj); + ValidateNewEntry(_database.Entry(obj)); + } + + private void ValidateNewEntry(EntityEntry entry) + { + if (entry.State != EntityState.Detached) + return; + entry.State = EntityState.Added; + foreach (NavigationEntry navigation in entry.Navigations) + { + ValidateNavigation(navigation); + if (navigation.CurrentValue == null) + continue; + if (navigation.Metadata.IsCollection()) + { + IEnumerable entities = (IEnumerable)navigation.CurrentValue; + foreach (object childEntry in entities) + ValidateNewEntry(_database.Entry(childEntry)); + } + else + ValidateNewEntry(_database.Entry(navigation.CurrentValue)); + } } public void RegisterShowLinks(Library library, Collection collection, Show show) @@ -212,7 +233,6 @@ namespace Kyoo.Controllers } catch (DbUpdateException) { - ValidateChanges(); if (retryCount < MaxSaveRetry) await SaveChanges(retryCount + 1); else @@ -231,21 +251,7 @@ namespace Kyoo.Controllers continue; foreach (NavigationEntry navigation in sourceEntry.Navigations) - { - if (navigation.IsModified == false) - continue; - - object value = navigation.Metadata.PropertyInfo.GetValue(sourceEntry.Entity); - if (value == null) - continue; - object newValue = Validate(value); - if (newValue != value) - navigation.Metadata.PropertyInfo.SetValue(sourceEntry.Entity, newValue); - else - _database.Entry(value).State = EntityState.Detached; - } - - break; + ValidateNavigation(navigation); } } finally @@ -254,6 +260,22 @@ namespace Kyoo.Controllers _database.ChangeTracker.DetectChanges(); } } + + private void ValidateNavigation(NavigationEntry navigation) + { + // if (navigation.IsModified == false) + // return; + + object oldValue = navigation.CurrentValue; + if (oldValue == null) + return; + object newValue = Validate(oldValue); + if (oldValue == newValue) + return; + navigation.CurrentValue = newValue; + if (!navigation.Metadata.IsCollection()) + _database.Entry(oldValue).State = EntityState.Detached; + } #endregion #region Edit @@ -465,16 +487,19 @@ namespace Kyoo.Controllers switch(obj) { case ProviderLink link: - link.Provider = ValidateLink(() => link.Provider); //TODO Calling this methods make the obj in a deteached state. Don't know why. - link.Library = ValidateLink(() => link.Library); + link.Provider = ValidateLink(link.Provider); + link.Library = ValidateLink(link.Library); + _database.Entry(link).State = EntityState.Added; return obj; case GenreLink link: - link.Show = ValidateLink(() => link.Show); - link.Genre = ValidateLink(() => link.Genre); + link.Show = ValidateLink(link.Show); + link.Genre = ValidateLink(link.Genre); + _database.Entry(link).State = EntityState.Added; return obj; case PeopleLink link: - link.Show = ValidateLink(() => link.Show); - link.People = ValidateLink(() => link.People); + link.Show = ValidateLink(link.Show); + link.People = ValidateLink(link.People); + _database.Entry(link).State = EntityState.Added; return obj; } @@ -491,7 +516,7 @@ namespace Kyoo.Controllers ProviderID provider => GetProvider(provider.Name) ?? provider, IEnumerable list => Utility.RunGenericMethod(this, "ValidateList", - list.GetType().GetGenericArguments().First(), new [] {list}), + Utility.GetEnumerableType(list), new [] {list}), _ => obj }); } @@ -504,17 +529,38 @@ namespace Kyoo.Controllers if (tmp != x) _database.Entry(x).State = EntityState.Detached; return tmp ?? x; - }).Where(x => x != null).ToList(); + }).GroupBy(GetSlug).Select(x => x.First()).Where(x => x != null).ToList(); } - private T ValidateLink(Func linkGet) where T : class + private static object GetSlug(object obj) + { + return obj switch + { + Library library => library.Slug, + LibraryLink link => (link.Library.Slug, link.Collection.Slug), + Collection collection => collection.Slug, + CollectionLink link => (link.Collection.Slug, link.Show.Slug), + Show show => show.Slug, + Season season => (season.Show.Slug, season.SeasonNumber), + Episode episode => (episode.Show.Slug, episode.SeasonNumber, episode.EpisodeNumber), + Track track => track.ID, + Studio studio => studio.Slug, + People people => people.Slug, + PeopleLink link => (link.Show.Slug, link.People.Slug), + Genre genre => genre.Slug, + GenreLink link => (link.Show.Slug, link.Genre.Slug), + MetadataID id => (id.ProviderID, id.ShowID, id.SeasonID, id.EpisodeID, id.PeopleID), + ProviderID id => id.Name, + ProviderLink link => (link.ProviderID, link.LibraryID), + _ => obj + }; + } + + private T ValidateLink(T oldValue) where T : class { - if (linkGet == null) - throw new ArgumentNullException(nameof(linkGet)); - T oldValue = linkGet(); T newValue = Validate(oldValue); if (!ReferenceEquals(oldValue, newValue)) - _database.Entry(linkGet()).State = EntityState.Detached; + _database.Entry(oldValue).State = EntityState.Detached; return newValue; } @@ -522,11 +568,11 @@ namespace Kyoo.Controllers { if (library == null) return null; - library.Providers = library.Providers.Select(x => - { - x.Provider = _database.Providers.FirstOrDefault(y => y.Name == x.Name); - return x; - }).Where(x => x.Provider != null).ToList(); + // library.Providers = library.Providers.Select(x => + // { + // x.Provider = _database.Providers.FirstOrDefault(y => y.Name == x.Name); + // return x; + // }).Where(x => x.Provider != null).ToList(); return library; } diff --git a/Kyoo/Models/DatabaseContext.cs b/Kyoo/Models/DatabaseContext.cs index dac4d29b..9b9513d4 100644 --- a/Kyoo/Models/DatabaseContext.cs +++ b/Kyoo/Models/DatabaseContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using IdentityServer4.EntityFramework.Entities; @@ -88,11 +89,13 @@ namespace Kyoo public DbSet ProviderLinks { get; set; } - private ValueConverter stringArrayConverter = new ValueConverter( + private readonly ValueConverter, string> _stringArrayConverter = + new ValueConverter, string>( arr => string.Join("|", arr), str => str.Split("|", StringSplitOptions.None)); - private ValueComparer stringArrayComparer = new ValueComparer( + private readonly ValueComparer> _stringArrayComparer = + new ValueComparer>( (l1, l2) => l1.SequenceEqual(l2), arr => arr.Aggregate(0, (i, s) => s.GetHashCode())); @@ -101,8 +104,12 @@ namespace Kyoo { base.OnModelCreating(modelBuilder); - modelBuilder.Entity().Property(e => e.Paths).HasConversion(stringArrayConverter).Metadata.SetValueComparer(stringArrayComparer); - modelBuilder.Entity().Property(e => e.Aliases).HasConversion(stringArrayConverter).Metadata.SetValueComparer(stringArrayComparer); + modelBuilder.Entity().Property(e => e.Paths) + .HasConversion(_stringArrayConverter).Metadata + .SetValueComparer(_stringArrayComparer); + modelBuilder.Entity().Property(e => e.Aliases) + .HasConversion(_stringArrayConverter).Metadata + .SetValueComparer(_stringArrayComparer); modelBuilder.Entity() .Property(t => t.IsDefault) @@ -114,21 +121,22 @@ namespace Kyoo modelBuilder.Entity() .HasKey(x => new {x.ShowID, x.GenreID}); - - modelBuilder.Entity() - .Ignore(x => x.Genres); + modelBuilder.Entity() + .Ignore(x => x.Shows) + .Ignore(x => x.Collections) + .Ignore(x => x.Providers); + modelBuilder.Entity() .Ignore(x => x.Shows); + modelBuilder.Entity() + .Ignore(x => x.Genres); + modelBuilder.Entity() .Ignore(x => x.Slug) .Ignore(x => x.Name) .Ignore(x => x.ExternalIDs); - - modelBuilder.Entity() - .Ignore(x => x.Name) - .Ignore(x => x.Logo); modelBuilder.Entity() diff --git a/Kyoo/Views/API/LibrariesAPI.cs b/Kyoo/Views/API/LibrariesAPI.cs index ec32921d..32aa73f6 100644 --- a/Kyoo/Views/API/LibrariesAPI.cs +++ b/Kyoo/Views/API/LibrariesAPI.cs @@ -38,7 +38,7 @@ namespace Kyoo.Api return BadRequest(new {error = "The library's slug must be set and not empty"}); if (string.IsNullOrEmpty(library.Name)) return BadRequest(new {error = "The library's name must be set and not empty"}); - if (library.Paths == null || library.Paths.Length == 0) + if (library.Paths == null || !library.Paths.Any()) return BadRequest(new {error = "The library should have a least one path."}); if (_libraryManager.GetLibrary(library.Slug) != null) return BadRequest(new {error = "Duplicated library slug"});