diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index 5ecf4495..091315c6 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -16,6 +16,7 @@ namespace Kyoo.Controllers Genre GetGenre(string slug); Studio GetStudio(string slug); People GetPeople(string slug); + ProviderID GetProvider(string name); // Get all IEnumerable GetLibraries(); @@ -41,8 +42,8 @@ namespace Kyoo.Controllers IEnumerable GetEpisodes(string showSlug, long seasonNumber); //Register values - Task Register(object obj); - Task RegisterShowLinks(Library library, Collection collection, Show show); + void Register(object obj); + void RegisterShowLinks(Library library, Collection collection, Show show); Task SaveChanges(); // Edit values diff --git a/Kyoo.Common/Models/Collection.cs b/Kyoo.Common/Models/Collection.cs index 830d0150..ce6ba4c9 100644 --- a/Kyoo.Common/Models/Collection.cs +++ b/Kyoo.Common/Models/Collection.cs @@ -4,7 +4,7 @@ using System.Linq; namespace Kyoo.Models { - public class Collection : IMergable + public class Collection { [JsonIgnore] public long ID { get; set; } public string Slug { get; set; } @@ -36,19 +36,5 @@ namespace Kyoo.Models 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; - } } } diff --git a/Kyoo.Common/Models/Episode.cs b/Kyoo.Common/Models/Episode.cs index 6f9118df..ada5a1ea 100644 --- a/Kyoo.Common/Models/Episode.cs +++ b/Kyoo.Common/Models/Episode.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Kyoo.Models { - public class Episode : IMergable + public class Episode { [JsonIgnore] public long ID { get; set; } [JsonIgnore] public long ShowID { get; set; } @@ -40,12 +40,7 @@ namespace Kyoo.Models } - public Episode() - { - SeasonNumber = -1; - EpisodeNumber = -1; - AbsoluteNumber = -1; - } + public Episode() { } public Episode(long seasonNumber, long episodeNumber, @@ -99,33 +94,5 @@ namespace Kyoo.Models { 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; - } } } diff --git a/Kyoo.Common/Models/People.cs b/Kyoo.Common/Models/People.cs index 5e8235a9..8dc17f9d 100644 --- a/Kyoo.Common/Models/People.cs +++ b/Kyoo.Common/Models/People.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Kyoo.Models { - public class People : IMergable + public class People { public long ID { get; set; } public string Slug { get; set; } @@ -23,17 +23,5 @@ namespace Kyoo.Models ImgPrimary = imgPrimary; 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; - } } } diff --git a/Kyoo.Common/Models/Season.cs b/Kyoo.Common/Models/Season.cs index 4db7f166..0f306349 100644 --- a/Kyoo.Common/Models/Season.cs +++ b/Kyoo.Common/Models/Season.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Kyoo.Models { - public class Season : IMergable + public class Season { [JsonIgnore] public long ID { get; set; } [JsonIgnore] public long ShowID { get; set; } @@ -39,22 +39,5 @@ namespace Kyoo.Models ImgPrimary = imgPrimary; 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; - } } } diff --git a/Kyoo.Common/Models/Show.cs b/Kyoo.Common/Models/Show.cs index 89cc8ae4..8d66e7c7 100644 --- a/Kyoo.Common/Models/Show.cs +++ b/Kyoo.Common/Models/Show.cs @@ -4,7 +4,7 @@ using System.Linq; namespace Kyoo.Models { - public class Show : IMergable + public class Show { [JsonIgnore] public long ID { get; set; } @@ -101,32 +101,6 @@ namespace Kyoo.Models { 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 } diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index 8a52fee2..19faf52f 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -1,5 +1,7 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -9,11 +11,6 @@ using Kyoo.Models; namespace Kyoo { - public interface IMergable - { - public T Merge(T other); - } - public static class Utility { public static string ToSlug(string str) @@ -59,7 +56,9 @@ namespace Kyoo } } - public static IEnumerable MergeLists(IEnumerable first, IEnumerable second, Func isEqual = null) + public static IEnumerable MergeLists(IEnumerable first, + IEnumerable second, + Func isEqual = null) { if (first == null) return second; @@ -91,6 +90,29 @@ namespace Kyoo return first; } + public static T Merge(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)oldValue, (IEnumerable)newValue); + } + } + + return first; + } + public static T Nullify(T obj) { Type type = typeof(T); @@ -107,5 +129,23 @@ namespace Kyoo return obj; } + + public static object RunGenericMethod([NotNull] object instance, + [NotNull] string methodName, + [NotNull] Type type, + IEnumerable 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()); + } + } } \ No newline at end of file diff --git a/Kyoo/Controllers/LibraryManager.cs b/Kyoo/Controllers/LibraryManager.cs index d29aee5c..830dffd0 100644 --- a/Kyoo/Controllers/LibraryManager.cs +++ b/Kyoo/Controllers/LibraryManager.cs @@ -1,15 +1,18 @@ using System; +using System.Collections; using Kyoo.Models; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; namespace Kyoo.Controllers { public class LibraryManager : ILibraryManager { + private const int MaxSaveRetry = 3; private readonly DatabaseContext _database; @@ -65,6 +68,12 @@ namespace Kyoo.Controllers { return _database.Peoples.FirstOrDefault(people => people.Slug == slug); } + + public ProviderID GetProvider(string name) + { + return _database.Providers.FirstOrDefault(x => x.Name == name); + } + #endregion #region GetAll @@ -168,15 +177,14 @@ namespace Kyoo.Controllers #endregion #region Register - public Task Register(object obj) + public void Register(object obj) { if (obj == null) - return Task.CompletedTask; - _database.Entry(obj).State = EntityState.Added; - return _database.SaveChangesAsync(); + return; + _database.Add(obj); } - public Task RegisterShowLinks(Library library, Collection collection, Show show) + public void RegisterShowLinks(Library library, Collection collection, Show show) { if (collection != null) { @@ -188,13 +196,48 @@ namespace Kyoo.Controllers else _database.LibraryLinks.AddIfNotExist(new LibraryLink {Library = library, Show = show}, x => x.Library == library && x.Collection == null && x.Show == show); - - return _database.SaveChangesAsync(); } - + 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 @@ -396,6 +439,54 @@ namespace Kyoo.Controllers #endregion #region ValidateValue + + private T Validate(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 list => Utility.RunGenericMethod(this, "ValidateList", + list.GetType().GetGenericArguments().First(), new [] {list}), + _ => obj + }); + } + + public IEnumerable ValidateList(IEnumerable list) where T : class + { + return list.Select(Validate).Where(x => x != null); + } + public Library Validate(Library library) { if (library == null) @@ -403,8 +494,6 @@ namespace Kyoo.Controllers library.Providers = library.Providers.Select(x => { x.Provider = _database.Providers.FirstOrDefault(y => y.Name == x.Name); - if (x.Provider != null) - x.ProviderID = x.Provider.ID; return x; }).Where(x => x.Provider != null).ToList(); return library; @@ -414,8 +503,7 @@ namespace Kyoo.Controllers { if (collection == null) return null; - if (collection.Slug == null) - collection.Slug = Utility.ToSlug(collection.Name); + collection.Slug ??= Utility.ToSlug(collection.Name); return collection; } @@ -429,7 +517,6 @@ namespace Kyoo.Controllers show.GenreLinks = show.GenreLinks?.Select(x => { x.Genre = Validate(x.Genre); - x.GenreID = x.Genre.ID; return x; }).ToList(); @@ -438,7 +525,6 @@ namespace Kyoo.Controllers .Select(x => { x.People = Validate(x.People); - x.PeopleID = x.People.ID; return x; }).ToList(); show.ExternalIDs = Validate(show.ExternalIDs); @@ -470,8 +556,7 @@ namespace Kyoo.Controllers { if (studio == 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; } @@ -479,8 +564,7 @@ namespace Kyoo.Controllers { if (people == 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); if (old != null) return old; @@ -500,7 +584,6 @@ namespace Kyoo.Controllers return ids?.Select(x => { x.Provider = _database.Providers.FirstOrDefault(y => y.Name == x.Provider.Name) ?? x.Provider; - x.ProviderID = x.Provider.ID; return x; }).GroupBy(x => x.Provider.Name).Select(x => x.First()).ToList(); } diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs index 2751c793..3281c907 100644 --- a/Kyoo/Controllers/ProviderManager.cs +++ b/Kyoo/Controllers/ProviderManager.cs @@ -16,7 +16,7 @@ namespace Kyoo.Controllers } private async Task GetMetadata(Func> providerCall, Library library, string what) - where T : IMergable, new() + where T : new() { T ret = new T(); @@ -29,8 +29,9 @@ namespace Kyoo.Controllers { try { - ret = ret.Merge(await providerCall(provider)); - } catch (Exception ex) { + 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}"); } } @@ -51,7 +52,8 @@ namespace Kyoo.Controllers try { ret.AddRange(await providerCall(provider) ?? new List()); - } catch (Exception ex) { + } catch (Exception ex) + { Console.Error.WriteLine($"\tThe provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}"); } } diff --git a/Kyoo/Controllers/Transcoder/TranscoderAPI.cs b/Kyoo/Controllers/Transcoder/TranscoderAPI.cs index 48cbc9ec..57140b6c 100644 --- a/Kyoo/Controllers/Transcoder/TranscoderAPI.cs +++ b/Kyoo/Controllers/Transcoder/TranscoderAPI.cs @@ -60,7 +60,6 @@ namespace Kyoo.Controllers.TranscoderLink tracks = new Track[0]; free(ptr); - Console.WriteLine($"\t{tracks.Length} tracks got at: {path}"); } public static void ExtractSubtitles(string path, string outPath, out Track[] tracks) @@ -89,7 +88,6 @@ namespace Kyoo.Controllers.TranscoderLink tracks = new Track[0]; free(ptr); - Console.WriteLine($"\t{tracks.Count(x => x.Type == StreamType.Subtitle)} subtitles got at: {path}"); } } } diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 19570b88..ce3fcd4d 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -107,13 +107,15 @@ namespace Kyoo.Controllers Console.Error.WriteLine($"Permission denied: can't access library's directory at {path}. (library slug: {library.Slug})"); 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)) - return Task.CompletedTask; + continue; + // return Task.CompletedTask; 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; Show show = await GetShow(libraryManager, showName, showPath, isMovie, library); if (isMovie) - await libraryManager.Register(await GetMovie(show, path)); + libraryManager.Register(await GetMovie(show, path)); else { Season season = await GetSeason(libraryManager, show, seasonNumber, library); Episode episode = await GetEpisode(libraryManager, show, season, episodeNumber, absoluteNumber, path, library); - await libraryManager.Register(episode); + libraryManager.Register(episode); } if (collection != null) - await libraryManager.Register(collection); - await libraryManager.RegisterShowLinks(library, collection, show); + libraryManager.Register(collection); + libraryManager.RegisterShowLinks(library, collection, show); + await libraryManager.SaveChanges(); } private async Task GetCollection(ILibraryManager libraryManager, string collectionName, Library library)