diff --git a/Kyoo.Common/Models/Attributes/MergeAttributes.cs b/Kyoo.Common/Models/Attributes/MergeAttributes.cs index 57861f61..399f5389 100644 --- a/Kyoo.Common/Models/Attributes/MergeAttributes.cs +++ b/Kyoo.Common/Models/Attributes/MergeAttributes.cs @@ -3,4 +3,9 @@ using System; namespace Kyoo.Models.Attributes { public class NotMergableAttribute : Attribute { } + + public interface IOnMerge + { + void OnMerge(object merged); + } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Library.cs b/Kyoo.Common/Models/Library.cs index 354507d3..d3324b22 100644 --- a/Kyoo.Common/Models/Library.cs +++ b/Kyoo.Common/Models/Library.cs @@ -15,22 +15,22 @@ namespace Kyoo.Models public IEnumerable Providers { - get => ProviderLinks.Select(x => x.Provider); - set => ProviderLinks = value.Select(x => new ProviderLink(x, this)); + get => ProviderLinks?.Select(x => x.Provider); + set => ProviderLinks = value.Select(x => new ProviderLink(x, this)).ToList(); } [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); + 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); + 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)); diff --git a/Kyoo.Common/Models/Show.cs b/Kyoo.Common/Models/Show.cs index f6e9578b..4801fb35 100644 --- a/Kyoo.Common/Models/Show.cs +++ b/Kyoo.Common/Models/Show.cs @@ -5,7 +5,7 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { - public class Show + public class Show : IOnMerge { [JsonIgnore] public long ID { get; set; } @@ -102,6 +102,25 @@ namespace Kyoo.Models { return ExternalIDs?.FirstOrDefault(x => x.Provider.Name == provider)?.DataID; } + + public void OnMerge(object merged) + { + if (ExternalIDs != null) + foreach (MetadataID id in ExternalIDs) + id.Show = this; + if (GenreLinks != null) + foreach (GenreLink genre in GenreLinks) + genre.Show = this; + if (People != null) + foreach (PeopleLink link in People) + link.Show = this; + if (Seasons != null) + foreach (Season season in Seasons) + season.Show = this; + if (Episodes != null) + foreach (Episode episode in Episodes) + episode.Show = this; + } } public enum Status { Finished, Airing, Planned } diff --git a/Kyoo.Common/Models/Studio.cs b/Kyoo.Common/Models/Studio.cs index 9b3ea47b..f21857a7 100644 --- a/Kyoo.Common/Models/Studio.cs +++ b/Kyoo.Common/Models/Studio.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.Collections.Generic; +using Newtonsoft.Json; namespace Kyoo.Models { @@ -7,6 +8,8 @@ namespace Kyoo.Models [JsonIgnore] public long ID { get; set; } public string Slug { get; set; } public string Name { get; set; } + + public virtual IEnumerable Shows { get; set; } public Studio() { } diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index 7e1177e2..cf414473 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -124,6 +124,8 @@ namespace Kyoo } } + if (first is IOnMerge) + ((IOnMerge)first).OnMerge(second); return first; } diff --git a/Kyoo/Controllers/LibraryManager.cs b/Kyoo/Controllers/LibraryManager.cs index f024981a..b58a83eb 100644 --- a/Kyoo/Controllers/LibraryManager.cs +++ b/Kyoo/Controllers/LibraryManager.cs @@ -1,9 +1,9 @@ using System; using System.Collections; -using Kyoo.Models; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -184,27 +184,6 @@ namespace Kyoo.Controllers 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) { if (collection != null) @@ -239,43 +218,6 @@ namespace Kyoo.Controllers throw; } } - - private void ValidateChanges() - { - _database.ChangeTracker.AutoDetectChangesEnabled = false; - try - { - foreach (EntityEntry sourceEntry in _database.ChangeTracker.Entries()) - { - if (sourceEntry.State != EntityState.Added && sourceEntry.State != EntityState.Modified) - continue; - - foreach (NavigationEntry navigation in sourceEntry.Navigations) - ValidateNavigation(navigation); - } - } - finally - { - _database.ChangeTracker.AutoDetectChangesEnabled = true; - _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 @@ -476,30 +418,82 @@ namespace Kyoo.Controllers #endregion #region ValidateValue - private T Validate(T obj) where T : class + private void ValidateChanges() + { + _database.ChangeTracker.AutoDetectChangesEnabled = false; + try + { + foreach (EntityEntry sourceEntry in _database.ChangeTracker.Entries()) + { + if (sourceEntry.State != EntityState.Added && sourceEntry.State != EntityState.Modified) + continue; + + foreach (NavigationEntry navigation in sourceEntry.Navigations) + ValidateNavigation(navigation, EntityState.Added); + } + } + finally + { + _database.ChangeTracker.AutoDetectChangesEnabled = true; + _database.ChangeTracker.DetectChanges(); + } + } + + private void ValidateNewEntry(EntityEntry entry) + { + if (entry.State != EntityState.Detached) + return; + entry.State = EntityState.Added; + foreach (NavigationEntry navigation in entry.Navigations) + { + ValidateNavigation(navigation, EntityState.Detached); + 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)); + } + } + + private void ValidateNavigation(NavigationEntry navigation, EntityState? skipOtherState) + { + object oldValue = navigation.CurrentValue; + if (oldValue == null) + return; + object newValue = Validate(oldValue, skipOtherState); + if (oldValue == newValue) + return; + navigation.CurrentValue = newValue; + if (!navigation.Metadata.IsCollection()) + _database.Entry(oldValue).State = EntityState.Detached; + } + + private T Validate(T obj, EntityState? skipOtherState) where T : class { if (obj == null) return null; - if (!(obj is IEnumerable) && _database.Entry(obj).State != EntityState.Added) + if (skipOtherState != null && !(obj is IEnumerable) && _database.Entry(obj).State != skipOtherState) return obj; switch(obj) { case ProviderLink link: - link.Provider = ValidateLink(link.Provider); - link.Library = ValidateLink(link.Library); - _database.Entry(link).State = EntityState.Added; + link.Provider = ValidateLink(link.Provider, skipOtherState); + link.Library = ValidateLink(link.Library, skipOtherState); return obj; case GenreLink link: - link.Show = ValidateLink(link.Show); - link.Genre = ValidateLink(link.Genre); - _database.Entry(link).State = EntityState.Added; + link.Show = ValidateLink(link.Show, skipOtherState); + link.Genre = ValidateLink(link.Genre, skipOtherState); return obj; case PeopleLink link: - link.Show = ValidateLink(link.Show); - link.People = ValidateLink(link.People); - _database.Entry(link).State = EntityState.Added; + link.Show = ValidateLink(link.Show, skipOtherState); + link.People = ValidateLink(link.People, skipOtherState); return obj; } @@ -509,27 +503,27 @@ namespace Kyoo.Controllers Collection collection => GetCollection(collection.Slug) ?? collection, Show show => GetShow(show.Slug) ?? show, Season season => GetSeason(season.Show.Slug, season.SeasonNumber) ?? season, - Episode episode => GetEpisode(episode.Show.Slug, episode.SeasonNumber, episode.SeasonNumber) ?? episode, + Episode episode => GetEpisode(episode.Show.Slug, episode.SeasonNumber, episode.EpisodeNumber) ?? episode, Studio studio => GetStudio(studio.Slug) ?? studio, People people => GetPeople(people.Slug) ?? people, Genre genre => GetGenre(genre.Slug) ?? genre, ProviderID provider => GetProvider(provider.Name) ?? provider, IEnumerable list => Utility.RunGenericMethod(this, "ValidateList", - Utility.GetEnumerableType(list), new [] {list}), + Utility.GetEnumerableType(list), new object[] {list, skipOtherState}), _ => obj }); } - public IEnumerable ValidateList(IEnumerable list) where T : class + public IEnumerable ValidateList(IEnumerable list, EntityState? skipOtherState) where T : class { return list.Select(x => { - T tmp = Validate(x); + T tmp = Validate(x, skipOtherState); if (tmp != x) _database.Entry(x).State = EntityState.Detached; return tmp ?? x; - }).GroupBy(GetSlug).Select(x => x.First()).Where(x => x != null).ToList(); + })/*.GroupBy(GetSlug).Select(x => x.First()).Where(x => x != null)*/.ToList(); } private static object GetSlug(object obj) @@ -556,9 +550,9 @@ namespace Kyoo.Controllers }; } - private T ValidateLink(T oldValue) where T : class + private T ValidateLink(T oldValue, EntityState? skipOtherState) where T : class { - T newValue = Validate(oldValue); + T newValue = Validate(oldValue, skipOtherState); if (!ReferenceEquals(oldValue, newValue)) _database.Entry(oldValue).State = EntityState.Detached; return newValue; diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs index 3281c907..5dc6791c 100644 --- a/Kyoo/Controllers/ProviderManager.cs +++ b/Kyoo/Controllers/ProviderManager.cs @@ -15,7 +15,7 @@ namespace Kyoo.Controllers _providers = pluginManager.GetPlugins(); } - private async Task GetMetadata(Func> providerCall, Library library, string what) + private async Task GetMetadata(Func> providerCall, Library library, string what) where T : new() { T ret = new T(); @@ -32,13 +32,17 @@ namespace Kyoo.Controllers ret = Utility.Merge(ret, await providerCall(provider)); } catch (Exception ex) { - Console.Error.WriteLine($"\tThe provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}"); + await Console.Error.WriteLineAsync( + $"The provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}"); } } return ret; } - private async Task> GetMetadata(Func>> providerCall, Library library, string what) + private async Task> GetMetadata( + Func>> providerCall, + Library library, + string what) { List ret = new List(); @@ -54,7 +58,8 @@ namespace Kyoo.Controllers ret.AddRange(await providerCall(provider) ?? new List()); } catch (Exception ex) { - Console.Error.WriteLine($"\tThe provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}"); + await Console.Error.WriteLineAsync( + $"The provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}"); } } return ret; @@ -62,7 +67,10 @@ namespace Kyoo.Controllers public async Task GetCollectionFromName(string name, Library library) { - Collection collection = await GetMetadata(provider => provider.GetCollectionFromName(name), library, $"the collection {name}"); + Collection collection = await GetMetadata( + provider => provider.GetCollectionFromName(name), + library, + $"the collection {name}"); collection.Name ??= name; collection.Slug ??= Utility.ToSlug(name); return collection; @@ -85,12 +93,17 @@ namespace Kyoo.Controllers show.Slug = Utility.ToSlug(showName); show.Title ??= showName; show.IsMovie = isMovie; + show.GenreLinks = show.GenreLinks?.GroupBy(x => x.Genre.Slug).Select(x => x.First()).ToList(); + show.People = show.People?.GroupBy(x => x.Slug).Select(x => x.First()).ToList(); return show; } public async Task> SearchShows(string showName, bool isMovie, Library library) { - IEnumerable shows = await GetMetadata(provider => provider.SearchShows(showName, isMovie), library, $"the show {showName}"); + IEnumerable shows = await GetMetadata( + provider => provider.SearchShows(showName, isMovie), + library, + $"the show {showName}"); return shows.Select(show => { show.Slug = Utility.ToSlug(showName); @@ -102,16 +115,27 @@ namespace Kyoo.Controllers public async Task GetSeason(Show show, long seasonNumber, Library library) { - Season season = await GetMetadata(provider => provider.GetSeason(show, seasonNumber), library, $"the season {seasonNumber} of {show.Title}"); + Season season = await GetMetadata( + provider => provider.GetSeason(show, seasonNumber), + library, + $"the season {seasonNumber} of {show.Title}"); season.Show = show; season.SeasonNumber = season.SeasonNumber == -1 ? seasonNumber : season.SeasonNumber; season.Title ??= $"Season {season.SeasonNumber}"; return season; } - public async Task GetEpisode(Show show, string episodePath, long seasonNumber, long episodeNumber, long absoluteNumber, Library library) + public async Task GetEpisode(Show show, + string episodePath, + long seasonNumber, + long episodeNumber, + long absoluteNumber, + Library library) { - Episode episode = await GetMetadata(provider => provider.GetEpisode(show, seasonNumber, episodeNumber, absoluteNumber), library, "an episode"); + Episode episode = await GetMetadata( + provider => provider.GetEpisode(show, seasonNumber, episodeNumber, absoluteNumber), + library, + "an episode"); episode.Show = show; episode.Path = episodePath; episode.SeasonNumber = episode.SeasonNumber != -1 ? episode.SeasonNumber : seasonNumber; @@ -122,8 +146,17 @@ namespace Kyoo.Controllers public async Task> GetPeople(Show show, Library library) { - IEnumerable people = await GetMetadata(provider => provider.GetPeople(show), library, $"a cast member of {show.Title}"); - return people; + List people = await GetMetadata( + provider => provider.GetPeople(show), + library, + $"a cast member of {show.Title}"); + return people?.GroupBy(x => x.Slug) + .Select(x => x.First()) + .Select(x => + { + x.Show = show; + return x; + }).ToList(); } } } diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 46c7eba0..524ce91e 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -52,9 +52,9 @@ namespace Kyoo services.AddDbContext(options => { options.UseLazyLoadingProxies() - .UseSqlite(_configuration.GetConnectionString("Database")); - //.EnableSensitiveDataLogging(); - //.UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); + .UseSqlite(_configuration.GetConnectionString("Database")) + .EnableSensitiveDataLogging() + .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); }); services.AddDbContext(options => diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index ce3fcd4d..3a52741d 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -174,9 +174,7 @@ namespace Kyoo.Controllers return show; show = await _metadataProvider.SearchShow(showTitle, isMovie, library); show.Path = showPath; - show.People = (await _metadataProvider.GetPeople(show, library)) - .GroupBy(x => x.Slug) - .Select(x => x.First()); + show.People = await _metadataProvider.GetPeople(show, library); await _thumbnailsManager.Validate(show.People); await _thumbnailsManager.Validate(show); return show;