Validating with a reflection based methodé

This commit is contained in:
Zoe Roux 2020-05-09 21:34:00 +02:00
parent 7f0302f832
commit bd15d425df
11 changed files with 175 additions and 150 deletions

View File

@ -16,6 +16,7 @@ namespace Kyoo.Controllers
Genre GetGenre(string slug); Genre GetGenre(string slug);
Studio GetStudio(string slug); Studio GetStudio(string slug);
People GetPeople(string slug); People GetPeople(string slug);
ProviderID GetProvider(string name);
// Get all // Get all
IEnumerable<Library> GetLibraries(); IEnumerable<Library> GetLibraries();
@ -41,8 +42,8 @@ namespace Kyoo.Controllers
IEnumerable<Episode> GetEpisodes(string showSlug, long seasonNumber); IEnumerable<Episode> GetEpisodes(string showSlug, long seasonNumber);
//Register values //Register values
Task Register(object obj); void Register(object obj);
Task RegisterShowLinks(Library library, Collection collection, Show show); void RegisterShowLinks(Library library, Collection collection, Show show);
Task SaveChanges(); Task SaveChanges();
// Edit values // Edit values

View File

@ -4,7 +4,7 @@ using System.Linq;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Collection : IMergable<Collection> public class Collection
{ {
[JsonIgnore] public long ID { get; set; } [JsonIgnore] public long ID { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
@ -36,19 +36,5 @@ namespace Kyoo.Models
IsCollection = true IsCollection = true
}; };
} }
public Collection Merge(Collection collection)
{
if (collection == null)
return this;
if (ID == -1)
ID = collection.ID;
Slug ??= collection.Slug;
Name ??= collection.Name;
Poster ??= collection.Poster;
Overview ??= collection.Overview;
ImgPrimary ??= collection.ImgPrimary;
return this;
}
} }
} }

View File

@ -4,7 +4,7 @@ using System.Collections.Generic;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Episode : IMergable<Episode> public class Episode
{ {
[JsonIgnore] public long ID { get; set; } [JsonIgnore] public long ID { get; set; }
[JsonIgnore] public long ShowID { get; set; } [JsonIgnore] public long ShowID { get; set; }
@ -40,12 +40,7 @@ namespace Kyoo.Models
} }
public Episode() public Episode() { }
{
SeasonNumber = -1;
EpisodeNumber = -1;
AbsoluteNumber = -1;
}
public Episode(long seasonNumber, public Episode(long seasonNumber,
long episodeNumber, long episodeNumber,
@ -99,33 +94,5 @@ namespace Kyoo.Models
{ {
return showSlug + "-s" + seasonNumber + "e" + episodeNumber; return showSlug + "-s" + seasonNumber + "e" + episodeNumber;
} }
public Episode Merge(Episode other)
{
if (other == null)
return this;
if (ID == -1)
ID = other.ID;
if (ShowID == -1)
ShowID = other.ShowID;
if (SeasonID == -1)
SeasonID = other.SeasonID;
if (SeasonNumber == -1)
SeasonNumber = other.SeasonNumber;
if (EpisodeNumber == -1)
EpisodeNumber = other.EpisodeNumber;
if (AbsoluteNumber == -1)
AbsoluteNumber = other.AbsoluteNumber;
Path ??= other.Path;
Title ??= other.Title;
Overview ??= other.Overview;
ReleaseDate ??= other.ReleaseDate;
if (Runtime == -1)
Runtime = other.Runtime;
ImgPrimary ??= other.ImgPrimary;
ExternalIDs = Utility.MergeLists(ExternalIDs, other.ExternalIDs,
(x, y) => x.Provider.Name == y.Provider.Name);
return this;
}
} }
} }

View File

@ -4,7 +4,7 @@ using Newtonsoft.Json;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class People : IMergable<People> public class People
{ {
public long ID { get; set; } public long ID { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
@ -23,17 +23,5 @@ namespace Kyoo.Models
ImgPrimary = imgPrimary; ImgPrimary = imgPrimary;
ExternalIDs = externalIDs; ExternalIDs = externalIDs;
} }
public People Merge(People other)
{
if (other == null)
return this;
Slug ??= other.Slug;
Name ??= other.Name;
ImgPrimary ??= other.ImgPrimary;
ExternalIDs = Utility.MergeLists(ExternalIDs, other.ExternalIDs,
(x, y) => x.Provider.Name == y.Provider.Name);
return this;
}
} }
} }

View File

@ -3,7 +3,7 @@ using Newtonsoft.Json;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Season : IMergable<Season> public class Season
{ {
[JsonIgnore] public long ID { get; set; } [JsonIgnore] public long ID { get; set; }
[JsonIgnore] public long ShowID { get; set; } [JsonIgnore] public long ShowID { get; set; }
@ -39,22 +39,5 @@ namespace Kyoo.Models
ImgPrimary = imgPrimary; ImgPrimary = imgPrimary;
ExternalIDs = externalIDs; ExternalIDs = externalIDs;
} }
public Season Merge(Season other)
{
if (other == null)
return this;
if (ShowID == -1)
ShowID = other.ShowID;
if (SeasonNumber == -1)
SeasonNumber = other.SeasonNumber;
Title ??= other.Title;
Overview ??= other.Overview;
Year ??= other.Year;
ImgPrimary ??= other.ImgPrimary;
ExternalIDs = Utility.MergeLists(ExternalIDs, other.ExternalIDs,
(x, y) => x.Provider.Name == y.Provider.Name);
return this;
}
} }
} }

View File

@ -4,7 +4,7 @@ using System.Linq;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Show : IMergable<Show> public class Show
{ {
[JsonIgnore] public long ID { get; set; } [JsonIgnore] public long ID { get; set; }
@ -101,32 +101,6 @@ namespace Kyoo.Models
{ {
return ExternalIDs?.FirstOrDefault(x => x.Provider.Name == provider)?.DataID; return ExternalIDs?.FirstOrDefault(x => x.Provider.Name == provider)?.DataID;
} }
public Show Merge(Show other)
{
if (other == null)
return this;
if (ID == -1)
ID = other.ID;
Slug ??= other.Slug;
Title ??= other.Title;
Aliases = Utility.MergeLists(Aliases, other.Aliases).ToArray();
Genres = Utility.MergeLists(Genres, other.Genres,
(x, y) => x.Slug == y.Slug);
Path ??= other.Path;
Overview ??= other.Overview;
TrailerUrl ??= other.TrailerUrl;
Status ??= other.Status;
StartYear ??= other.StartYear;
EndYear ??= other.EndYear;
Poster ??= other.Poster;
Logo ??= other.Logo;
Backdrop ??= other.Backdrop;
Studio ??= other.Studio;
ExternalIDs = Utility.MergeLists(ExternalIDs, other.ExternalIDs,
(x, y) => x.Provider.Name == y.Provider.Name);
return this;
}
} }
public enum Status { Finished, Airing, Planned } public enum Status { Finished, Airing, Planned }

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -9,11 +11,6 @@ using Kyoo.Models;
namespace Kyoo namespace Kyoo
{ {
public interface IMergable<T>
{
public T Merge(T other);
}
public static class Utility public static class Utility
{ {
public static string ToSlug(string str) public static string ToSlug(string str)
@ -59,7 +56,9 @@ namespace Kyoo
} }
} }
public static IEnumerable<T> MergeLists<T>(IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> isEqual = null) public static IEnumerable<T> MergeLists<T>(IEnumerable<T> first,
IEnumerable<T> second,
Func<T, T, bool> isEqual = null)
{ {
if (first == null) if (first == null)
return second; return second;
@ -91,6 +90,29 @@ namespace Kyoo
return first; return first;
} }
public static T Merge<T>(T first, T second)
{
Type type = typeof(T);
foreach (PropertyInfo property in type.GetProperties().Where(x => x.CanRead && x.CanWrite))
{
object oldValue = property.GetValue(first);
object newValue = property.GetValue(second);
object defaultValue = property.PropertyType.IsValueType
? Activator.CreateInstance(property.PropertyType)
: null;
if (oldValue?.Equals(defaultValue) == true)
property.SetValue(first, newValue);
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)
&& property.PropertyType != typeof(string))
{
property.SetValue((IEnumerable<object>)oldValue, (IEnumerable<object>)newValue);
}
}
return first;
}
public static T Nullify<T>(T obj) public static T Nullify<T>(T obj)
{ {
Type type = typeof(T); Type type = typeof(T);
@ -107,5 +129,23 @@ namespace Kyoo
return obj; return obj;
} }
public static object RunGenericMethod([NotNull] object instance,
[NotNull] string methodName,
[NotNull] Type type,
IEnumerable<object> args)
{
if (instance == null)
throw new ArgumentNullException(nameof(instance));
if (methodName == null)
throw new ArgumentNullException(nameof(methodName));
if (type == null)
throw new ArgumentNullException(nameof(type));
MethodInfo method = instance.GetType().GetMethod(methodName);
if (method == null)
throw new NullReferenceException($"A method named {methodName} could not be found on {instance.GetType().FullName}");
return method.MakeGenericMethod(type).Invoke(instance, args?.ToArray());
}
} }
} }

View File

@ -1,15 +1,18 @@
using System; using System;
using System.Collections;
using Kyoo.Models; 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.Exceptions; using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class LibraryManager : ILibraryManager public class LibraryManager : ILibraryManager
{ {
private const int MaxSaveRetry = 3;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
@ -65,6 +68,12 @@ namespace Kyoo.Controllers
{ {
return _database.Peoples.FirstOrDefault(people => people.Slug == slug); return _database.Peoples.FirstOrDefault(people => people.Slug == slug);
} }
public ProviderID GetProvider(string name)
{
return _database.Providers.FirstOrDefault(x => x.Name == name);
}
#endregion #endregion
#region GetAll #region GetAll
@ -168,15 +177,14 @@ namespace Kyoo.Controllers
#endregion #endregion
#region Register #region Register
public Task Register(object obj) public void Register(object obj)
{ {
if (obj == null) if (obj == null)
return Task.CompletedTask; return;
_database.Entry(obj).State = EntityState.Added; _database.Add(obj);
return _database.SaveChangesAsync();
} }
public Task RegisterShowLinks(Library library, Collection collection, Show show) public void RegisterShowLinks(Library library, Collection collection, Show show)
{ {
if (collection != null) if (collection != null)
{ {
@ -188,13 +196,48 @@ namespace Kyoo.Controllers
else else
_database.LibraryLinks.AddIfNotExist(new LibraryLink {Library = library, Show = show}, _database.LibraryLinks.AddIfNotExist(new LibraryLink {Library = library, Show = show},
x => x.Library == library && x.Collection == null && x.Show == show); x => x.Library == library && x.Collection == null && x.Show == show);
return _database.SaveChangesAsync();
} }
public Task SaveChanges() public Task SaveChanges()
{ {
return _database.SaveChangesAsync(); return SaveChanges(0);
}
private async Task SaveChanges(int retryCount)
{
ValidateChanges();
try
{
await _database.SaveChangesAsync();
}
catch (DbUpdateException)
{
ValidateChanges();
if (retryCount < MaxSaveRetry)
await SaveChanges(retryCount + 1);
else
throw;
}
}
private void ValidateChanges()
{
foreach (EntityEntry sourceEntry in _database.ChangeTracker.Entries())
{
if (sourceEntry.State != EntityState.Added && sourceEntry.State != EntityState.Modified)
continue;
foreach (NavigationEntry navigation in sourceEntry.Navigations)
{
if (navigation.IsModified == false)
continue;
object value = navigation.Metadata.PropertyInfo.GetValue(sourceEntry.Entity);
if (value == null)
continue;
navigation.Metadata.PropertyInfo.SetValue(sourceEntry.Entity, Validate(value));
}
}
} }
#endregion #endregion
@ -396,6 +439,54 @@ namespace Kyoo.Controllers
#endregion #endregion
#region ValidateValue #region ValidateValue
private T Validate<T>(T obj) where T : class
{
if (obj == null)
return null;
if (!(obj is IEnumerable) && _database.Entry(obj).IsKeySet)
return obj;
switch(obj)
{
case ProviderLink link:
link.Provider = Validate(link.Provider);
link.Library = Validate(link.Library);
return obj;
case GenreLink link:
link.Show = Validate(link.Show);
link.Genre = Validate(link.Genre);
return obj;
case PeopleLink link:
link.Show = Validate(link.Show);
link.People = Validate(link.People);
return obj;
}
return (T)(obj switch
{
Library library => GetLibrary(library.Slug) ?? library,
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,
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<dynamic> list => Utility.RunGenericMethod(this, "ValidateList",
list.GetType().GetGenericArguments().First(), new [] {list}),
_ => obj
});
}
public IEnumerable<T> ValidateList<T>(IEnumerable<T> list) where T : class
{
return list.Select(Validate).Where(x => x != null);
}
public Library Validate(Library library) public Library Validate(Library library)
{ {
if (library == null) if (library == null)
@ -403,8 +494,6 @@ namespace Kyoo.Controllers
library.Providers = library.Providers.Select(x => library.Providers = library.Providers.Select(x =>
{ {
x.Provider = _database.Providers.FirstOrDefault(y => y.Name == x.Name); x.Provider = _database.Providers.FirstOrDefault(y => y.Name == x.Name);
if (x.Provider != null)
x.ProviderID = x.Provider.ID;
return x; return x;
}).Where(x => x.Provider != null).ToList(); }).Where(x => x.Provider != null).ToList();
return library; return library;
@ -414,8 +503,7 @@ namespace Kyoo.Controllers
{ {
if (collection == null) if (collection == null)
return null; return null;
if (collection.Slug == null) collection.Slug ??= Utility.ToSlug(collection.Name);
collection.Slug = Utility.ToSlug(collection.Name);
return collection; return collection;
} }
@ -429,7 +517,6 @@ namespace Kyoo.Controllers
show.GenreLinks = show.GenreLinks?.Select(x => show.GenreLinks = show.GenreLinks?.Select(x =>
{ {
x.Genre = Validate(x.Genre); x.Genre = Validate(x.Genre);
x.GenreID = x.Genre.ID;
return x; return x;
}).ToList(); }).ToList();
@ -438,7 +525,6 @@ namespace Kyoo.Controllers
.Select(x => .Select(x =>
{ {
x.People = Validate(x.People); x.People = Validate(x.People);
x.PeopleID = x.People.ID;
return x; return x;
}).ToList(); }).ToList();
show.ExternalIDs = Validate(show.ExternalIDs); show.ExternalIDs = Validate(show.ExternalIDs);
@ -470,8 +556,7 @@ namespace Kyoo.Controllers
{ {
if (studio == null) if (studio == null)
return null; return null;
if (studio.Slug == null) studio.Slug ??= Utility.ToSlug(studio.Name);
studio.Slug = Utility.ToSlug(studio.Name);
return _database.Studios.FirstOrDefault(x => x.Slug == studio.Slug) ?? studio; return _database.Studios.FirstOrDefault(x => x.Slug == studio.Slug) ?? studio;
} }
@ -479,8 +564,7 @@ namespace Kyoo.Controllers
{ {
if (people == null) if (people == null)
return null; return null;
if (people.Slug == null) people.Slug ??= Utility.ToSlug(people.Name);
people.Slug = Utility.ToSlug(people.Name);
People old = _database.Peoples.FirstOrDefault(y => y.Slug == people.Slug); People old = _database.Peoples.FirstOrDefault(y => y.Slug == people.Slug);
if (old != null) if (old != null)
return old; return old;
@ -500,7 +584,6 @@ namespace Kyoo.Controllers
return ids?.Select(x => return ids?.Select(x =>
{ {
x.Provider = _database.Providers.FirstOrDefault(y => y.Name == x.Provider.Name) ?? x.Provider; x.Provider = _database.Providers.FirstOrDefault(y => y.Name == x.Provider.Name) ?? x.Provider;
x.ProviderID = x.Provider.ID;
return x; return x;
}).GroupBy(x => x.Provider.Name).Select(x => x.First()).ToList(); }).GroupBy(x => x.Provider.Name).Select(x => x.First()).ToList();
} }

View File

@ -16,7 +16,7 @@ namespace Kyoo.Controllers
} }
private async Task<T> GetMetadata<T>(Func<IMetadataProvider, Task<T>> providerCall, Library library, string what) private async Task<T> GetMetadata<T>(Func<IMetadataProvider, Task<T>> providerCall, Library library, string what)
where T : IMergable<T>, new() where T : new()
{ {
T ret = new T(); T ret = new T();
@ -29,8 +29,9 @@ namespace Kyoo.Controllers
{ {
try try
{ {
ret = ret.Merge(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}"); Console.Error.WriteLine($"\tThe provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}");
} }
} }
@ -51,7 +52,8 @@ namespace Kyoo.Controllers
try try
{ {
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}"); Console.Error.WriteLine($"\tThe provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}");
} }
} }

View File

@ -60,7 +60,6 @@ namespace Kyoo.Controllers.TranscoderLink
tracks = new Track[0]; tracks = new Track[0];
free(ptr); free(ptr);
Console.WriteLine($"\t{tracks.Length} tracks got at: {path}");
} }
public static void ExtractSubtitles(string path, string outPath, out Track[] tracks) public static void ExtractSubtitles(string path, string outPath, out Track[] tracks)
@ -89,7 +88,6 @@ namespace Kyoo.Controllers.TranscoderLink
tracks = new Track[0]; tracks = new Track[0];
free(ptr); free(ptr);
Console.WriteLine($"\t{tracks.Count(x => x.Type == StreamType.Subtitle)} subtitles got at: {path}");
} }
} }
} }

View File

@ -107,13 +107,15 @@ namespace Kyoo.Controllers
Console.Error.WriteLine($"Permission denied: can't access library's directory at {path}. (library slug: {library.Slug})"); Console.Error.WriteLine($"Permission denied: can't access library's directory at {path}. (library slug: {library.Slug})");
continue; continue;
} }
await Task.WhenAll(files.Select(file => // await Task.WhenAll(files.Select(file =>
foreach (var file in files)
{ {
if (!IsVideo(file) || libraryManager.GetEpisodes().Any(x => x.Path == file)) if (!IsVideo(file) || libraryManager.GetEpisodes().Any(x => x.Path == file))
return Task.CompletedTask; continue;
// return Task.CompletedTask;
string relativePath = file.Substring(path.Length); string relativePath = file.Substring(path.Length);
return RegisterFile(file, relativePath, library, cancellationToken); await RegisterFile(file, relativePath, library, cancellationToken);
})); }//));
} }
} }
@ -142,16 +144,17 @@ namespace Kyoo.Controllers
bool isMovie = seasonNumber == -1 && episodeNumber == -1 && absoluteNumber == -1; bool isMovie = seasonNumber == -1 && episodeNumber == -1 && absoluteNumber == -1;
Show show = await GetShow(libraryManager, showName, showPath, isMovie, library); Show show = await GetShow(libraryManager, showName, showPath, isMovie, library);
if (isMovie) if (isMovie)
await libraryManager.Register(await GetMovie(show, path)); libraryManager.Register(await GetMovie(show, path));
else else
{ {
Season season = await GetSeason(libraryManager, show, seasonNumber, library); Season season = await GetSeason(libraryManager, show, seasonNumber, library);
Episode episode = await GetEpisode(libraryManager, show, season, episodeNumber, absoluteNumber, path, library); Episode episode = await GetEpisode(libraryManager, show, season, episodeNumber, absoluteNumber, path, library);
await libraryManager.Register(episode); libraryManager.Register(episode);
} }
if (collection != null) if (collection != null)
await libraryManager.Register(collection); libraryManager.Register(collection);
await libraryManager.RegisterShowLinks(library, collection, show); libraryManager.RegisterShowLinks(library, collection, show);
await libraryManager.SaveChanges();
} }
private async Task<Collection> GetCollection(ILibraryManager libraryManager, string collectionName, Library library) private async Task<Collection> GetCollection(ILibraryManager libraryManager, string collectionName, Library library)