Finishing the reflection based validation

This commit is contained in:
Zoe Roux 2020-05-19 02:49:47 +02:00
parent 8ce6e1cc1b
commit 500ba56943
9 changed files with 154 additions and 100 deletions

View File

@ -3,4 +3,9 @@ using System;
namespace Kyoo.Models.Attributes namespace Kyoo.Models.Attributes
{ {
public class NotMergableAttribute : Attribute { } public class NotMergableAttribute : Attribute { }
public interface IOnMerge
{
void OnMerge(object merged);
}
} }

View File

@ -15,22 +15,22 @@ namespace Kyoo.Models
public IEnumerable<ProviderID> Providers public IEnumerable<ProviderID> Providers
{ {
get => ProviderLinks.Select(x => x.Provider); get => ProviderLinks?.Select(x => x.Provider);
set => ProviderLinks = value.Select(x => new ProviderLink(x, this)); set => ProviderLinks = value.Select(x => new ProviderLink(x, this)).ToList();
} }
[NotMergable] [JsonIgnore] public virtual IEnumerable<ProviderLink> ProviderLinks { get; set; } [NotMergable] [JsonIgnore] public virtual IEnumerable<ProviderLink> ProviderLinks { get; set; }
[NotMergable] [JsonIgnore] public virtual IEnumerable<LibraryLink> Links { get; set; } [NotMergable] [JsonIgnore] public virtual IEnumerable<LibraryLink> Links { get; set; }
[JsonIgnore] public IEnumerable<Show> Shows [JsonIgnore] public IEnumerable<Show> 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( set => Links = Utility.MergeLists(
value?.Select(x => new LibraryLink(this, x)), value?.Select(x => new LibraryLink(this, x)),
Links?.Where(x => x.Show == null)); Links?.Where(x => x.Show == null));
} }
[JsonIgnore] public IEnumerable<Collection> Collections [JsonIgnore] public IEnumerable<Collection> 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( set => Links = Utility.MergeLists(
value?.Select(x => new LibraryLink(this, x)), value?.Select(x => new LibraryLink(this, x)),
Links?.Where(x => x.Collection == null)); Links?.Where(x => x.Collection == null));

View File

@ -5,7 +5,7 @@ using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Show public class Show : IOnMerge
{ {
[JsonIgnore] public long ID { get; set; } [JsonIgnore] public long ID { get; set; }
@ -102,6 +102,25 @@ namespace Kyoo.Models
{ {
return ExternalIDs?.FirstOrDefault(x => x.Provider.Name == provider)?.DataID; 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 } public enum Status { Finished, Airing, Planned }

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json; using System.Collections.Generic;
using Newtonsoft.Json;
namespace Kyoo.Models namespace Kyoo.Models
{ {
@ -8,6 +9,8 @@ namespace Kyoo.Models
public string Slug { get; set; } public string Slug { get; set; }
public string Name { get; set; } public string Name { get; set; }
public virtual IEnumerable<Show> Shows { get; set; }
public Studio() { } public Studio() { }
public Studio(string name) public Studio(string name)

View File

@ -124,6 +124,8 @@ namespace Kyoo
} }
} }
if (first is IOnMerge)
((IOnMerge)first).OnMerge(second);
return first; return first;
} }

View File

@ -1,9 +1,9 @@
using System; using System;
using System.Collections; using System.Collections;
using Kyoo.Models;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Models.Exceptions; using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.ChangeTracking;
@ -184,27 +184,6 @@ namespace Kyoo.Controllers
ValidateNewEntry(_database.Entry(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) public void RegisterShowLinks(Library library, Collection collection, Show show)
{ {
if (collection != null) if (collection != null)
@ -239,43 +218,6 @@ namespace Kyoo.Controllers
throw; 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 #endregion
#region Edit #region Edit
@ -476,30 +418,82 @@ namespace Kyoo.Controllers
#endregion #endregion
#region ValidateValue #region ValidateValue
private T Validate<T>(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>(T obj, EntityState? skipOtherState) where T : class
{ {
if (obj == null) if (obj == null)
return 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; return obj;
switch(obj) switch(obj)
{ {
case ProviderLink link: case ProviderLink link:
link.Provider = ValidateLink(link.Provider); link.Provider = ValidateLink(link.Provider, skipOtherState);
link.Library = ValidateLink(link.Library); link.Library = ValidateLink(link.Library, skipOtherState);
_database.Entry(link).State = EntityState.Added;
return obj; return obj;
case GenreLink link: case GenreLink link:
link.Show = ValidateLink(link.Show); link.Show = ValidateLink(link.Show, skipOtherState);
link.Genre = ValidateLink(link.Genre); link.Genre = ValidateLink(link.Genre, skipOtherState);
_database.Entry(link).State = EntityState.Added;
return obj; return obj;
case PeopleLink link: case PeopleLink link:
link.Show = ValidateLink(link.Show); link.Show = ValidateLink(link.Show, skipOtherState);
link.People = ValidateLink(link.People); link.People = ValidateLink(link.People, skipOtherState);
_database.Entry(link).State = EntityState.Added;
return obj; return obj;
} }
@ -509,27 +503,27 @@ namespace Kyoo.Controllers
Collection collection => GetCollection(collection.Slug) ?? collection, Collection collection => GetCollection(collection.Slug) ?? collection,
Show show => GetShow(show.Slug) ?? show, Show show => GetShow(show.Slug) ?? show,
Season season => GetSeason(season.Show.Slug, season.SeasonNumber) ?? season, 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, Studio studio => GetStudio(studio.Slug) ?? studio,
People people => GetPeople(people.Slug) ?? people, People people => GetPeople(people.Slug) ?? people,
Genre genre => GetGenre(genre.Slug) ?? genre, Genre genre => GetGenre(genre.Slug) ?? genre,
ProviderID provider => GetProvider(provider.Name) ?? provider, ProviderID provider => GetProvider(provider.Name) ?? provider,
IEnumerable<dynamic> list => Utility.RunGenericMethod(this, "ValidateList", IEnumerable<dynamic> list => Utility.RunGenericMethod(this, "ValidateList",
Utility.GetEnumerableType(list), new [] {list}), Utility.GetEnumerableType(list), new object[] {list, skipOtherState}),
_ => obj _ => obj
}); });
} }
public IEnumerable<T> ValidateList<T>(IEnumerable<T> list) where T : class public IEnumerable<T> ValidateList<T>(IEnumerable<T> list, EntityState? skipOtherState) where T : class
{ {
return list.Select(x => return list.Select(x =>
{ {
T tmp = Validate(x); T tmp = Validate(x, skipOtherState);
if (tmp != x) if (tmp != x)
_database.Entry(x).State = EntityState.Detached; _database.Entry(x).State = EntityState.Detached;
return tmp ?? x; 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) private static object GetSlug(object obj)
@ -556,9 +550,9 @@ namespace Kyoo.Controllers
}; };
} }
private T ValidateLink<T>(T oldValue) where T : class private T ValidateLink<T>(T oldValue, EntityState? skipOtherState) where T : class
{ {
T newValue = Validate(oldValue); T newValue = Validate(oldValue, skipOtherState);
if (!ReferenceEquals(oldValue, newValue)) if (!ReferenceEquals(oldValue, newValue))
_database.Entry(oldValue).State = EntityState.Detached; _database.Entry(oldValue).State = EntityState.Detached;
return newValue; return newValue;

View File

@ -32,13 +32,17 @@ namespace Kyoo.Controllers
ret = Utility.Merge(ret, await providerCall(provider)); ret = Utility.Merge(ret, await providerCall(provider));
} catch (Exception ex) } 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; return ret;
} }
private async Task<IEnumerable<T>> GetMetadata<T>(Func<IMetadataProvider, Task<IEnumerable<T>>> providerCall, Library library, string what) private async Task<List<T>> GetMetadata<T>(
Func<IMetadataProvider, Task<IEnumerable<T>>> providerCall,
Library library,
string what)
{ {
List<T> ret = new List<T>(); List<T> ret = new List<T>();
@ -54,7 +58,8 @@ namespace Kyoo.Controllers
ret.AddRange(await providerCall(provider) ?? new List<T>()); ret.AddRange(await providerCall(provider) ?? new List<T>());
} catch (Exception ex) } 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; return ret;
@ -62,7 +67,10 @@ namespace Kyoo.Controllers
public async Task<Collection> GetCollectionFromName(string name, Library library) public async Task<Collection> 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.Name ??= name;
collection.Slug ??= Utility.ToSlug(name); collection.Slug ??= Utility.ToSlug(name);
return collection; return collection;
@ -85,12 +93,17 @@ namespace Kyoo.Controllers
show.Slug = Utility.ToSlug(showName); show.Slug = Utility.ToSlug(showName);
show.Title ??= showName; show.Title ??= showName;
show.IsMovie = isMovie; 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; return show;
} }
public async Task<IEnumerable<Show>> SearchShows(string showName, bool isMovie, Library library) public async Task<IEnumerable<Show>> SearchShows(string showName, bool isMovie, Library library)
{ {
IEnumerable<Show> shows = await GetMetadata(provider => provider.SearchShows(showName, isMovie), library, $"the show {showName}"); IEnumerable<Show> shows = await GetMetadata(
provider => provider.SearchShows(showName, isMovie),
library,
$"the show {showName}");
return shows.Select(show => return shows.Select(show =>
{ {
show.Slug = Utility.ToSlug(showName); show.Slug = Utility.ToSlug(showName);
@ -102,16 +115,27 @@ namespace Kyoo.Controllers
public async Task<Season> GetSeason(Show show, long seasonNumber, Library library) public async Task<Season> 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.Show = show;
season.SeasonNumber = season.SeasonNumber == -1 ? seasonNumber : season.SeasonNumber; season.SeasonNumber = season.SeasonNumber == -1 ? seasonNumber : season.SeasonNumber;
season.Title ??= $"Season {season.SeasonNumber}"; season.Title ??= $"Season {season.SeasonNumber}";
return season; return season;
} }
public async Task<Episode> GetEpisode(Show show, string episodePath, long seasonNumber, long episodeNumber, long absoluteNumber, Library library) public async Task<Episode> 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.Show = show;
episode.Path = episodePath; episode.Path = episodePath;
episode.SeasonNumber = episode.SeasonNumber != -1 ? episode.SeasonNumber : seasonNumber; episode.SeasonNumber = episode.SeasonNumber != -1 ? episode.SeasonNumber : seasonNumber;
@ -122,8 +146,17 @@ namespace Kyoo.Controllers
public async Task<IEnumerable<PeopleLink>> GetPeople(Show show, Library library) public async Task<IEnumerable<PeopleLink>> GetPeople(Show show, Library library)
{ {
IEnumerable<PeopleLink> people = await GetMetadata(provider => provider.GetPeople(show), library, $"a cast member of {show.Title}"); List<PeopleLink> people = await GetMetadata(
return people; 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();
} }
} }
} }

View File

@ -52,9 +52,9 @@ namespace Kyoo
services.AddDbContext<DatabaseContext>(options => services.AddDbContext<DatabaseContext>(options =>
{ {
options.UseLazyLoadingProxies() options.UseLazyLoadingProxies()
.UseSqlite(_configuration.GetConnectionString("Database")); .UseSqlite(_configuration.GetConnectionString("Database"))
//.EnableSensitiveDataLogging(); .EnableSensitiveDataLogging()
//.UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole()));
}); });
services.AddDbContext<IdentityDatabase>(options => services.AddDbContext<IdentityDatabase>(options =>

View File

@ -174,9 +174,7 @@ namespace Kyoo.Controllers
return show; return show;
show = await _metadataProvider.SearchShow(showTitle, isMovie, library); show = await _metadataProvider.SearchShow(showTitle, isMovie, library);
show.Path = showPath; show.Path = showPath;
show.People = (await _metadataProvider.GetPeople(show, library)) show.People = await _metadataProvider.GetPeople(show, library);
.GroupBy(x => x.Slug)
.Select(x => x.First());
await _thumbnailsManager.Validate(show.People); await _thumbnailsManager.Validate(show.People);
await _thumbnailsManager.Validate(show); await _thumbnailsManager.Validate(show);
return show; return show;