diff --git a/Kyoo.Common/Models/Attributes/ComputedAttribute.cs b/Kyoo.Common/Models/Attributes/ComputedAttribute.cs new file mode 100644 index 00000000..b7f07048 --- /dev/null +++ b/Kyoo.Common/Models/Attributes/ComputedAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Kyoo.Models.Attributes +{ + /// + /// An attribute to inform that the property is computed automatically and can't be assigned manually. + /// + [AttributeUsage(AttributeTargets.Property)] + public class ComputedAttribute : NotMergeableAttribute { } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index e5a677d4..307ab115 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -17,12 +17,17 @@ namespace Kyoo.Models public int ID { get; set; } /// - public string Slug + [Computed] public string Slug { - get => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber, AbsoluteNumber); + get + { + if (ShowSlug == null && Show == null) + return GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber); + return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber); + } [UsedImplicitly] private set { - Match match = Regex.Match(value, @"(?.*)-s(?\d*)e(?\d*)"); + Match match = Regex.Match(value, @"(?.+)-s(?\d+)e(?\d+)"); if (match.Success) { @@ -45,7 +50,7 @@ namespace Kyoo.Models } } } - + /// /// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed. /// diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index 142b7e24..e3440670 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -16,12 +16,17 @@ namespace Kyoo.Models public int ID { get; set; } /// - public string Slug + [Computed] public string Slug { - get => $"{ShowSlug}-s{SeasonNumber}"; + get + { + if (ShowSlug == null && Show == null) + return $"{ShowID}-s{SeasonNumber}"; + return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}"; + } [UsedImplicitly] private set { - Match match = Regex.Match(value, @"(?.*)-s(?\d*)"); + Match match = Regex.Match(value ?? "", @"(?.+)-s(?\d+)"); if (!match.Success) throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); @@ -29,7 +34,7 @@ namespace Kyoo.Models SeasonNumber = int.Parse(match.Groups["season"].Value); } } - + /// /// The slug of the Show that contain this episode. If this is not set, this season is ill-formed. /// diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 21aa9d63..e0d543c2 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -29,7 +29,7 @@ namespace Kyoo.Models public int ID { get; set; } /// - public string Slug + [Computed] public string Slug { get { diff --git a/Kyoo.Common/Utility/Merger.cs b/Kyoo.Common/Utility/Merger.cs index 047b5b09..d6378ca9 100644 --- a/Kyoo.Common/Utility/Merger.cs +++ b/Kyoo.Common/Utility/Merger.cs @@ -79,7 +79,7 @@ namespace Kyoo Type type = typeof(T); IEnumerable properties = type.GetProperties() - .Where(x => x.CanRead && x.CanWrite + .Where(x => x.CanRead && x.CanWrite && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); if (where != null) diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index a2b7eeb2..2e425415 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -330,6 +330,16 @@ namespace Kyoo modelBuilder.Entity() .Property(x => x.Slug) .ValueGeneratedOnAddOrUpdate(); + + modelBuilder.Entity() + .Property(x => x.EpisodeNumber) + .HasDefaultValue(-1); + modelBuilder.Entity() + .Property(x => x.SeasonNumber) + .HasDefaultValue(-1); + modelBuilder.Entity() + .Property(x => x.AbsoluteNumber) + .HasDefaultValue(-1); } /// diff --git a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj index f6add6b4..14d40203 100644 --- a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj +++ b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj @@ -14,6 +14,7 @@ + diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index 9c23c774..8ab23443 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -257,6 +257,8 @@ namespace Kyoo.Controllers /// You can throw this if the resource is illegal and should not be saved. protected virtual Task Validate(T resource) { + if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute() != null) + return Task.CompletedTask; if (string.IsNullOrEmpty(resource.Slug)) throw new ArgumentException("Resource can't have null as a slug."); if (int.TryParse(resource.Slug, out int _)) diff --git a/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs b/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs index 2dce467d..48bdb52a 100644 --- a/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs +++ b/Kyoo.SqLite/Migrations/20210621175342_Triggers.cs @@ -18,10 +18,26 @@ namespace Kyoo.SqLite.Migrations UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber WHERE ID == new.ID; END"); + + migrationBuilder.Sql(@" + CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW + BEGIN + UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber || 'e' || EpisodeNumber + WHERE ID == new.ID; + END"); + migrationBuilder.Sql(@" + CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF EpisodeNumber, SeasonNumber, ShowID ON Episodes FOR EACH ROW + BEGIN + UPDATE Episodes SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber || 'e' || EpisodeNumber + WHERE ID == new.ID; + END"); + + migrationBuilder.Sql(@" CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW BEGIN UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID; + UPDATE Episodes SET Slug = new.Slug || '-s' || SeasonNumber || 'e' || EpisodeNumber WHERE ShowID = new.ID; END;"); } @@ -29,6 +45,8 @@ namespace Kyoo.SqLite.Migrations { migrationBuilder.Sql("DROP TRIGGER SeasonSlugInsert;"); migrationBuilder.Sql("DROP TRIGGER SeasonSlugUpdate;"); + migrationBuilder.Sql("DROP TRIGGER EpisodeSlugInsert;"); + migrationBuilder.Sql("DROP TRIGGER EpisodeSlugUpdate;"); migrationBuilder.Sql("DROP TRIGGER ShowSlugUpdate;"); } } diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs index a7b3d743..d3603567 100644 --- a/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs +++ b/Kyoo.Tests/Library/SpecificTests/EpisodeTest.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; +using Kyoo.Controllers; using Kyoo.Models; using Xunit; @@ -25,8 +27,70 @@ namespace Kyoo.Tests.Library public abstract class AEpisodeTests : RepositoryTests { - protected AEpisodeTests(RepositoryActivator repositories) - : base(repositories) - { } + private readonly IEpisodeRepository _repository; + + protected AEpisodeTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = repositories.LibraryManager.EpisodeRepository; + } + + [Fact] + public async Task SlugEditTest() + { + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); + Show show = new() + { + ID = episode.ShowID, + Slug = "new-slug" + }; + await Repositories.LibraryManager.ShowRepository.Edit(show, false); + episode = await _repository.Get(1); + Assert.Equal("new-slug-s1e1", episode.Slug); + } + + [Fact] + public async Task SeasonNumberEditTest() + { + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); + await _repository.Edit(new Episode + { + ID = 1, + SeasonNumber = 2 + }, false); + episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); + } + + [Fact] + public async Task EpisodeNumberEditTest() + { + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); + await _repository.Edit(new Episode + { + ID = 1, + EpisodeNumber = 2 + }, false); + episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); + } + + [Fact] + public async Task EpisodeCreationSlugTest() + { + Episode season = await _repository.Create(new Episode + { + ShowID = TestSample.Get().ID, + SeasonNumber = 2, + EpisodeNumber = 4 + }); + Assert.Equal($"{TestSample.Get().Slug}-s2e4", season.Slug); + } + + + // TODO absolute numbering tests } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 5a01bd44..278e31b5 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -145,7 +145,7 @@ namespace Kyoo.Controllers /// The parameter is returned. private async Task ValidateTracks(Episode resource) { - resource.Tracks = await resource.Tracks.MapAsync((x, i) => + resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.MapAsync((x, i) => { x.Episode = resource; x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language @@ -153,7 +153,7 @@ namespace Kyoo.Controllers && x.Codec == y.Codec && x.Type == y.Type); return _tracks.Create(x); - }).ToListAsync(); + }).ToListAsync()); return resource; } @@ -161,13 +161,12 @@ namespace Kyoo.Controllers protected override async Task Validate(Episode resource) { await base.Validate(resource); - resource.ExternalIDs = await resource.ExternalIDs.SelectAsync(async x => + await resource.ExternalIDs.ForEachAsync(async x => { x.Second = await _providers.CreateIfNotExists(x.Second); x.SecondID = x.Second.ID; _database.Entry(x.Second).State = EntityState.Detached; - return x; - }).ToListAsync(); + }); } ///