diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index 6ed573dc..4e8b2440 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -109,6 +109,8 @@ namespace Kyoo.Controllers public interface IShowRepository : IRepository { Task AddShowLink(int showID, int? libraryID, int? collectionID); + + Task GetSlug(int showID); } public interface ISeasonRepository : IRepository diff --git a/Kyoo.Common/Models/Attributes/ComposedSlug.cs b/Kyoo.Common/Models/Attributes/ComposedSlug.cs deleted file mode 100644 index 4595d5a4..00000000 --- a/Kyoo.Common/Models/Attributes/ComposedSlug.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System; - -namespace Kyoo.Models.Attributes -{ - [AttributeUsage(AttributeTargets.Class)] - public class ComposedSlugAttribute : Attribute { } -} \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 42863a5d..4d45c397 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -5,11 +5,11 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { - [ComposedSlug] public class Episode : IResource, IOnMerge { public int ID { get; set; } - public string Slug => Show != null ? GetSlug(Show.Slug, SeasonNumber, EpisodeNumber) : ID.ToString(); + public string Slug => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber); + [SerializeIgnore] public string ShowSlug { private get; set; } [SerializeIgnore] public int ShowID { get; set; } [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } [SerializeIgnore] public int? SeasonID { get; set; } @@ -30,9 +30,7 @@ namespace Kyoo.Models [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } [LoadableRelation] public virtual ICollection Tracks { get; set; } - - public string ShowTitle => Show?.Title; - + public Episode() { } @@ -78,6 +76,8 @@ namespace Kyoo.Models public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber) { + if (showSlug == null) + throw new ArgumentException("Show's slug is null. Can't find episode's slug."); if (seasonNumber == -1) return showSlug; return $"{showSlug}-s{seasonNumber}e{episodeNumber}"; diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index adba3b9f..8691e5f3 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -4,15 +4,16 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { - [ComposedSlug] public class Season : IResource { public int ID { get; set; } + public string Slug => $"{ShowSlug}-s{SeasonNumber}"; [SerializeIgnore] public int ShowID { get; set; } + [SerializeIgnore] public string ShowSlug { private get; set; } + [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } public int SeasonNumber { get; set; } = -1; - public string Slug => Show != null ? $"{Show.Slug}-s{SeasonNumber}" : ID.ToString(); public string Title { get; set; } public string Overview { get; set; } public int? Year { get; set; } @@ -21,7 +22,6 @@ namespace Kyoo.Models public string Thumb => $"/api/seasons/{Slug}/thumb"; [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } [LoadableRelation] public virtual ICollection Episodes { get; set; } public Season() { } diff --git a/Kyoo.Common/Models/WatchItem.cs b/Kyoo.Common/Models/WatchItem.cs index 0fe617da..f552efce 100644 --- a/Kyoo.Common/Models/WatchItem.cs +++ b/Kyoo.Common/Models/WatchItem.cs @@ -25,7 +25,7 @@ namespace Kyoo.Models public class WatchItem { - public readonly int EpisodeID = -1; + public readonly int EpisodeID; public string ShowTitle; public string ShowSlug; @@ -41,9 +41,9 @@ namespace Kyoo.Models public string Container; public Track Video; - public IEnumerable Audios; - public IEnumerable Subtitles; - public IEnumerable Chapters; + public ICollection Audios; + public ICollection Subtitles; + public ICollection Chapters; public WatchItem() { } @@ -78,8 +78,8 @@ namespace Kyoo.Models DateTime? releaseDate, string path, Track video, - IEnumerable audios, - IEnumerable subtitles) + ICollection audios, + ICollection subtitles) : this(episodeID, showTitle, showSlug, seasonNumber, episodeNumber, title, releaseDate, path) { Video = video; @@ -120,8 +120,8 @@ namespace Kyoo.Models ep.ReleaseDate, ep.Path, ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video), - ep.Tracks.Where(x => x.Type == StreamType.Audio), - ep.Tracks.Where(x => x.Type == StreamType.Subtitle)) + ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(), + ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray()) { IsMovie = show.IsMovie, PreviousEpisode = previous, @@ -130,7 +130,7 @@ namespace Kyoo.Models }; } - private static async Task> GetChapters(string episodePath) + private static async Task> GetChapters(string episodePath) { string path = PathIO.Combine( PathIO.GetDirectoryName(episodePath)!, diff --git a/Kyoo.CommonAPI/JsonSerializer.cs b/Kyoo.CommonAPI/JsonSerializer.cs index 71f9c6e6..69d818ae 100644 --- a/Kyoo.CommonAPI/JsonSerializer.cs +++ b/Kyoo.CommonAPI/JsonSerializer.cs @@ -1,5 +1,7 @@ using System; +using System.Collections; using System.Reflection; +using Kyoo.Models; using Kyoo.Models.Attributes; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -40,8 +42,13 @@ namespace Kyoo.Controllers protected override JsonContract CreateContract(Type objectType) { JsonContract contract = base.CreateContract(objectType); - contract.OnSerializingCallbacks.Add((_, _) => _depth++); - contract.OnSerializedCallbacks.Add((_, _) => _depth--); + if (Utility.GetGenericDefinition(objectType, typeof(Page<>)) == null + && !objectType.IsAssignableTo(typeof(IEnumerable))) + { + contract.OnSerializingCallbacks.Add((_, _) => _depth++); + contract.OnSerializedCallbacks.Add((_, _) => _depth--); + } + return contract; } } diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index 0e47c5b3..d5c7f790 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -209,7 +209,7 @@ namespace Kyoo.Controllers { if (string.IsNullOrEmpty(resource.Slug)) throw new ArgumentException("Resource can't have null as a slug."); - if (int.TryParse(resource.Slug, out int _) && typeof(T).GetCustomAttribute() == null) + if (int.TryParse(resource.Slug, out int _)) { try { diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index c4552d63..ab1c1df3 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -5,7 +5,6 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; -using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers @@ -15,13 +14,16 @@ namespace Kyoo.Controllers private bool _disposed; private readonly DatabaseContext _database; private readonly IProviderRepository _providers; + private readonly IShowRepository _shows; protected override Expression> DefaultSort => x => x.EpisodeNumber; - public EpisodeRepository(DatabaseContext database, IProviderRepository providers) : base(database) + public EpisodeRepository(DatabaseContext database, IProviderRepository providers, IShowRepository shows) + : base(database) { _database = database; _providers = providers; + _shows = shows; } @@ -32,6 +34,7 @@ namespace Kyoo.Controllers _disposed = true; _database.Dispose(); _providers.Dispose(); + _shows.Dispose(); GC.SuppressFinalize(this); } @@ -42,6 +45,15 @@ namespace Kyoo.Controllers _disposed = true; await _database.DisposeAsync(); await _providers.DisposeAsync(); + await _shows.DisposeAsync(); + } + + public override async Task Get(int id) + { + Episode ret = await base.Get(id); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + return ret; } public override Task Get(string slug) @@ -54,45 +66,81 @@ namespace Kyoo.Controllers int.Parse(match.Groups["season"].Value), int.Parse(match.Groups["episode"].Value)); } - - public Task Get(string showSlug, int seasonNumber, int episodeNumber) + + public override async Task Get(Expression> predicate) { - return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug - && x.SeasonNumber == seasonNumber - && x.EpisodeNumber == episodeNumber); + Episode ret = await base.Get(predicate); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + return ret; } - public Task Get(int showID, int seasonNumber, int episodeNumber) + public async Task Get(string showSlug, int seasonNumber, int episodeNumber) { - return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID - && x.SeasonNumber == seasonNumber - && x.EpisodeNumber == episodeNumber); + Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + && x.SeasonNumber == seasonNumber + && x.EpisodeNumber == episodeNumber); + if (ret != null) + ret.ShowSlug = showSlug; + return ret; } - public Task Get(int seasonID, int episodeNumber) + public async Task Get(int showID, int seasonNumber, int episodeNumber) { - return _database.Episodes.FirstOrDefaultAsync(x => x.SeasonID == seasonID - && x.EpisodeNumber == episodeNumber); + Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + && x.SeasonNumber == seasonNumber + && x.EpisodeNumber == episodeNumber); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(showID); + return ret; } - public Task GetAbsolute(int showID, int absoluteNumber) + public async Task Get(int seasonID, int episodeNumber) { - return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID - && x.AbsoluteNumber == absoluteNumber); + Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.SeasonID == seasonID + && x.EpisodeNumber == episodeNumber); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + return ret; } - public Task GetAbsolute(string showSlug, int absoluteNumber) + public async Task GetAbsolute(int showID, int absoluteNumber) { - return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug - && x.AbsoluteNumber == absoluteNumber); + Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + && x.AbsoluteNumber == absoluteNumber); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(showID); + return ret; + } + + public async Task GetAbsolute(string showSlug, int absoluteNumber) + { + Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + && x.AbsoluteNumber == absoluteNumber); + if (ret != null) + ret.ShowSlug = showSlug; + return ret; } public override async Task> Search(string query) { - return await _database.Episodes + List episodes = await _database.Episodes .Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) .Take(20) .ToListAsync(); + foreach (Episode episode in episodes) + episode.ShowSlug = await _shows.GetSlug(episode.ShowID); + return episodes; + } + + public override async Task> GetAll(Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection episodes = await base.GetAll(where, sort, limit); + foreach (Episode episode in episodes) + episode.ShowSlug = await _shows.GetSlug(episode.ShowID); + return episodes; } public override async Task Create(Episode obj) diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index 0ca44fdd..6fa7a298 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -5,7 +5,6 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; -using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -16,17 +15,20 @@ namespace Kyoo.Controllers private bool _disposed; private readonly DatabaseContext _database; private readonly IProviderRepository _providers; + private readonly IShowRepository _shows; private readonly Lazy _episodes; protected override Expression> DefaultSort => x => x.SeasonNumber; public SeasonRepository(DatabaseContext database, IProviderRepository providers, + IShowRepository shows, IServiceProvider services) : base(database) { _database = database; _providers = providers; + _shows = shows; _episodes = new Lazy(services.GetRequiredService); } @@ -38,6 +40,7 @@ namespace Kyoo.Controllers _disposed = true; _database.Dispose(); _providers.Dispose(); + _shows.Dispose(); if (_episodes.IsValueCreated) _episodes.Value.Dispose(); GC.SuppressFinalize(this); @@ -50,10 +53,27 @@ namespace Kyoo.Controllers _disposed = true; await _database.DisposeAsync(); await _providers.DisposeAsync(); + await _shows.DisposeAsync(); if (_episodes.IsValueCreated) await _episodes.Value.DisposeAsync(); } + public override async Task Get(int id) + { + Season ret = await base.Get(id); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + return ret; + } + + public override async Task Get(Expression> predicate) + { + Season ret = await base.Get(predicate); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + return ret; + } + public override Task Get(string slug) { Match match = Regex.Match(slug, @"(?.*)-s(?\d*)"); @@ -63,24 +83,43 @@ namespace Kyoo.Controllers return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value)); } - public Task Get(int showID, int seasonNumber) + public async Task Get(int showID, int seasonNumber) { - return _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID - && x.SeasonNumber == seasonNumber); + Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID + && x.SeasonNumber == seasonNumber); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(showID); + return ret; } - public Task Get(string showSlug, int seasonNumber) + public async Task Get(string showSlug, int seasonNumber) { - return _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug && x.SeasonNumber == seasonNumber); + if (ret != null) + ret.ShowSlug = showSlug; + return ret; } public override async Task> Search(string query) { - return await _database.Seasons + List seasons = await _database.Seasons .Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) .Take(20) .ToListAsync(); + foreach (Season season in seasons) + season.ShowSlug = await _shows.GetSlug(season.ShowID); + return seasons; + } + + public override async Task> GetAll(Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection seasons = await base.GetAll(where, sort, limit); + foreach (Season season in seasons) + season.ShowSlug = await _shows.GetSlug(season.ShowID); + return seasons; } public override async Task Create(Season obj) diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index b7e29563..cc0b2749 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -151,6 +151,13 @@ namespace Kyoo.Controllers } } + public Task GetSlug(int showID) + { + return _database.Shows.Where(x => x.ID == showID) + .Select(x => x.Slug) + .FirstOrDefaultAsync(); + } + public override async Task Delete(Show obj) { if (obj == null) diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index 1008dab7..413b202f 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -82,7 +82,7 @@ namespace Kyoo.Controllers { if (obj.EpisodeID <= 0) { - obj.EpisodeID = obj.Episode?.ID ?? -1; + obj.EpisodeID = obj.Episode?.ID ?? 0; if (obj.EpisodeID <= 0) throw new InvalidOperationException($"Can't store a track not related to any episode (episodeID: {obj.EpisodeID})."); }