diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index 4e8b2440..b0b42cc6 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -30,7 +30,7 @@ namespace Kyoo.Controllers public Sort(Expression> key, bool descendant = false) { - Key = ExpressionRewrite.Rewrite>(key); + Key = key; Descendant = descendant; if (!Utility.IsPropertyExpression(Key)) @@ -54,8 +54,7 @@ namespace Kyoo.Controllers Key = property.Type.IsValueType ? Expression.Lambda>(Expression.Convert(property, typeof(object)), param) : Expression.Lambda>(property, param); - Key = ExpressionRewrite.Rewrite>(Key); - + Descendant = order switch { "desc" => true, @@ -64,11 +63,6 @@ namespace Kyoo.Controllers _ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.") }; } - - public Sort To() - { - return new(Key.Convert>(), Descendant); - } } public interface IRepository : IDisposable, IAsyncDisposable where T : class, IResource diff --git a/Kyoo.Common/ExpressionRewrite.cs b/Kyoo.Common/ExpressionRewrite.cs deleted file mode 100644 index 3bb5bc5c..00000000 --- a/Kyoo.Common/ExpressionRewrite.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace Kyoo -{ - public class ExpressionRewriteAttribute : Attribute - { - public string Link { get; } - public string Inner { get; } - - public ExpressionRewriteAttribute(string link, string inner = null) - { - Link = link; - Inner = inner; - } - } - - public class ExpressionRewrite : ExpressionVisitor - { - private string _inner; - private readonly List<(string inner, ParameterExpression param, ParameterExpression newParam)> _innerRewrites; - - private ExpressionRewrite() - { - _innerRewrites = new List<(string, ParameterExpression, ParameterExpression)>(); - } - - public static Expression Rewrite(Expression expression) - { - return new ExpressionRewrite().Visit(expression); - } - - public static Expression Rewrite(Expression expression) where T : Delegate - { - return (Expression)new ExpressionRewrite().Visit(expression); - } - - protected override Expression VisitMember(MemberExpression node) - { - (string inner, _, ParameterExpression p) = _innerRewrites.FirstOrDefault(x => x.param == node.Expression); - if (inner != null) - { - Expression param = inner.Split('.').Aggregate(p, Expression.Property); - node = Expression.Property(param, node.Member.Name); - } - - // Can't use node.Member directly because we want to support attribute override - MemberInfo member = node.Expression!.Type.GetProperty(node.Member.Name) ?? node.Member; - ExpressionRewriteAttribute attr = member!.GetCustomAttribute(); - if (attr == null) - return base.VisitMember(node); - - Expression property = attr.Link.Split('.').Aggregate(node.Expression, Expression.Property); - if (property is MemberExpression expr) - Visit(expr.Expression); - _inner = attr.Inner; - return property!; - } - - protected override Expression VisitLambda(Expression node) - { - (_, ParameterExpression oldParam, ParameterExpression param) = _innerRewrites - .FirstOrDefault(x => node.Parameters.Any(y => y == x.param)); - if (param == null) - return base.VisitLambda(node); - - ParameterExpression[] newParams = node.Parameters.Where(x => x != oldParam).Append(param).ToArray(); - return Expression.Lambda(Visit(node.Body)!, newParams); - } - - protected override Expression VisitMethodCall(MethodCallExpression node) - { - int count = node.Arguments.Count; - if (node.Object != null) - count++; - if (count != 2) - return base.VisitMethodCall(node); - - Expression instance = node.Object ?? node.Arguments.First(); - Expression argument = node.Object != null - ? node.Arguments.First() - : node.Arguments[1]; - - Type oldType = instance.Type; - instance = Visit(instance); - if (instance!.Type == oldType) - return base.VisitMethodCall(node); - - if (_inner != null && argument is LambdaExpression lambda) - { - // TODO this type handler will usually work with IEnumerable & others but won't work with everything. - Type type = oldType.GetGenericArguments().First(); - ParameterExpression oldParam = lambda.Parameters.FirstOrDefault(x => x.Type == type); - if (oldParam != null) - { - Type newType = instance.Type.GetGenericArguments().First(); - ParameterExpression newParam = Expression.Parameter(newType, oldParam.Name); - _innerRewrites.Add((_inner, oldParam, newParam)); - } - } - argument = Visit(argument); - - // TODO this method handler may not work for some methods (ex: method taking a Fun<> method won't have good generic arguments) - MethodInfo method = node.Method.IsGenericMethod - ? node.Method.GetGenericMethodDefinition().MakeGenericMethod(instance.Type.GetGenericArguments()) - : node.Method; - return node.Object != null - ? Expression.Call(instance, method!, argument) - : Expression.Call(null, method!, instance, argument!); - } - } -} \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 6d9f3b81..ec4d2919 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -58,6 +58,7 @@ namespace Kyoo.Models { public int ID { get; set; } [SerializeIgnore] public int EpisodeID { get; set; } + public int TrackIndex { get; set; } public bool IsDefault { get => isDefault; @@ -94,13 +95,17 @@ namespace Kyoo.Models { get { - // TODO other type of tracks should still have slugs. The slug should never be an ID. Maybe a und-number format. - if (Type != StreamType.Subtitle) - return null; - - string slug = string.IsNullOrEmpty(Language) - ? ID.ToString() - : $"{Episode.Slug}.{Language}{(IsForced ? "-forced" : "")}"; + string type = Type switch + { + StreamType.Subtitle => "", + StreamType.Video => "video.", + StreamType.Audio => "audio.", + StreamType.Font => "font.", + _ => "" + }; + string slug = $"{Episode.Slug}.{type}{Language}{(TrackIndex != 0 ? TrackIndex : "")}"; + if (IsForced) + slug += "-forced"; switch (Codec) { case "ass": @@ -144,6 +149,7 @@ namespace Kyoo.Models return mkvLanguage switch { "fre" => "fra", + null => "und", _ => mkvLanguage }; } diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index f672d598..0ee0c881 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -256,6 +256,42 @@ namespace Kyoo return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); } + public static IEnumerable Map([CanBeNull] this IEnumerable self, + [NotNull] Func mapper) + { + if (self == null) + yield break; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return mapper(enumerator.Current, index); + index++; + } + } + + public static async IAsyncEnumerable MapAsync([CanBeNull] this IEnumerable self, + [NotNull] Func> mapper) + { + if (self == null) + yield break; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return await mapper(enumerator.Current, index); + index++; + } + } + public static async IAsyncEnumerable SelectAsync([CanBeNull] this IEnumerable self, [NotNull] Func> mapper) { @@ -584,57 +620,5 @@ namespace Kyoo return true; return firstID == secondID; } - - public static Expression Convert([CanBeNull] this Expression expr) - where T : Delegate - { - Expression e = expr switch - { - null => null, - LambdaExpression lambda => new ExpressionConverter(lambda).VisitAndConvert(), - _ => throw new ArgumentException("Can't convert a non lambda.") - }; - - return ExpressionRewrite.Rewrite(e); - } - - private class ExpressionConverter : ExpressionVisitor - where TTo : Delegate - { - private readonly LambdaExpression _expression; - private readonly ParameterExpression[] _newParams; - - internal ExpressionConverter(LambdaExpression expression) - { - _expression = expression; - - Type[] paramTypes = typeof(TTo).GetGenericArguments()[..^1]; - if (paramTypes.Length != _expression.Parameters.Count) - throw new ArgumentException("Parameter count from internal and external lambda are not matched."); - - _newParams = new ParameterExpression[paramTypes.Length]; - for (int i = 0; i < paramTypes.Length; i++) - { - if (_expression.Parameters[i].Type == paramTypes[i]) - _newParams[i] = _expression.Parameters[i]; - else - _newParams[i] = Expression.Parameter(paramTypes[i], _expression.Parameters[i].Name); - } - } - - internal Expression VisitAndConvert() - { - Type returnType = _expression.Type.GetGenericArguments().Last(); - Expression body = _expression.ReturnType == returnType - ? Visit(_expression.Body) - : Expression.Convert(Visit(_expression.Body)!, returnType); - return Expression.Lambda(body!, _newParams); - } - - protected override Expression VisitParameter(ParameterExpression node) - { - return _newParams.FirstOrDefault(x => x.Name == node.Name) ?? node; - } - } } } \ No newline at end of file diff --git a/Kyoo.CommonAPI/ApiHelper.cs b/Kyoo.CommonAPI/ApiHelper.cs index 312ac908..d3d8d951 100644 --- a/Kyoo.CommonAPI/ApiHelper.cs +++ b/Kyoo.CommonAPI/ApiHelper.cs @@ -26,12 +26,7 @@ namespace Kyoo.CommonApi Expression> defaultWhere = null) { if (where == null || where.Count == 0) - { - if (defaultWhere == null) - return null; - Expression body = ExpressionRewrite.Rewrite(defaultWhere.Body); - return Expression.Lambda>(body, defaultWhere.Parameters.First()); - } + return defaultWhere; ParameterExpression param = defaultWhere?.Parameters.First() ?? Expression.Parameter(typeof(T)); Expression expression = defaultWhere?.Body; @@ -97,8 +92,7 @@ namespace Kyoo.CommonApi expression = condition; } - expression = ExpressionRewrite.Rewrite(expression); - return Expression.Lambda>(expression, param); + return Expression.Lambda>(expression!, param); } private static Expression ResourceEqual(Expression parameter, string value, bool notEqual = false) diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 162d03e3..c6b2e307 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -155,13 +155,7 @@ namespace Kyoo.Controllers _database.Entry(obj).State = EntityState.Added; obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added); await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists)."); - obj.Tracks = await obj.Tracks.SelectAsync(x => - { - x.Episode = obj; - x.EpisodeID = obj.ID; - return _tracks.CreateIfNotExists(x, true); - }).ToListAsync(); - return obj; + return await ValidateTracks(obj); } protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld) @@ -173,30 +167,41 @@ namespace Kyoo.Controllers { ICollection oldTracks = await _tracks.GetAll(x => x.EpisodeID == resource.ID); await _tracks.DeleteRange(oldTracks); - resource.Tracks = await changed.Tracks.SelectAsync(x => - { - x.Episode = resource; - x.EpisodeID = resource.ID; - return _tracks.Create(x); - }).ToListAsync(); + resource.Tracks = changed.Tracks; + await ValidateTracks(resource); } if (changed.ExternalIDs != null || resetOld) { await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); - resource.ExternalIDs = changed.ExternalIDs?.Select(x => - { - x.Provider = null; - return x; - }).ToList(); + resource.ExternalIDs = changed.ExternalIDs; } + + await Validate(resource); } + private async Task ValidateTracks(Episode resource) + { + resource.Tracks = await resource.Tracks.MapAsync((x, i) => + { + x.Episode = resource; + x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language + && x.IsForced == y.IsForced + && x.Codec == y.Codec + && x.Type == y.Type); + return _tracks.CreateIfNotExists(x, true); + }).ToListAsync(); + return resource; + } + protected override async Task Validate(Episode resource) { await base.Validate(resource); - await resource.ExternalIDs.ForEachAsync(async id => - id.Provider = await _providers.CreateIfNotExists(id.Provider, true)); + resource.ExternalIDs = resource.ExternalIDs?.Select(x => + { + x.Provider = null; + return x; + }).ToList(); } public async Task Delete(string showSlug, int seasonNumber, int episodeNumber) diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index 53b79936..1fdc8c6a 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -99,7 +99,8 @@ namespace Kyoo.Controllers protected override async Task Validate(Show resource) { await base.Validate(resource); - resource.Studio = await _studios.CreateIfNotExists(resource.Studio, true); + if (resource.Studio != null) + resource.Studio = await _studios.CreateIfNotExists(resource.Studio, true); resource.GenreLinks = await resource.Genres .SelectAsync(async x => Link.UCreate(resource, await _genres.CreateIfNotExists(x, true))) .ToListAsync(); diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index e97d430a..97b151f2 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -90,7 +90,7 @@ - + diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210316095337_Initial.Designer.cs b/Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.Designer.cs similarity index 99% rename from Kyoo/Models/DatabaseMigrations/Internal/20210316095337_Initial.Designer.cs rename to Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.Designer.cs index a53b427f..d62ff33f 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210316095337_Initial.Designer.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.Designer.cs @@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Models.DatabaseMigrations.Internal { [DbContext(typeof(DatabaseContext))] - [Migration("20210316095337_Initial")] + [Migration("20210317180448_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -491,6 +491,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Title") .HasColumnType("text"); + b.Property("TrackIndex") + .HasColumnType("integer"); + b.Property("Type") .HasColumnType("integer"); diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210316095337_Initial.cs b/Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.cs similarity index 99% rename from Kyoo/Models/DatabaseMigrations/Internal/20210316095337_Initial.cs rename to Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.cs index 4b007b7f..0593207e 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210316095337_Initial.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.cs @@ -397,6 +397,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), EpisodeID = table.Column(type: "integer", nullable: false), + TrackIndex = table.Column(type: "integer", nullable: false), IsDefault = table.Column(type: "boolean", nullable: false), IsForced = table.Column(type: "boolean", nullable: false), IsExternal = table.Column(type: "boolean", nullable: false), diff --git a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs index 50070827..9889f943 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs @@ -489,6 +489,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Title") .HasColumnType("text"); + b.Property("TrackIndex") + .HasColumnType("integer"); + b.Property("Type") .HasColumnType("integer"); diff --git a/Kyoo/Views/API/TrackApi.cs b/Kyoo/Views/API/TrackApi.cs new file mode 100644 index 00000000..689e904b --- /dev/null +++ b/Kyoo/Views/API/TrackApi.cs @@ -0,0 +1,56 @@ +using System.Linq; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/track")] + [Route("api/tracks")] + [ApiController] + public class TrackApi : CrudApi + { + private readonly ILibraryManager _libraryManager; + + public TrackApi(ILibraryManager libraryManager, IConfiguration configuration) + : base(libraryManager.TrackRepository, configuration) + { + _libraryManager = libraryManager; + } + + [HttpGet("{id:int}/episode")] + [Authorize(Policy = "Read")] + public async Task> GetEpisode(int id) + { + try + { + return await _libraryManager.GetEpisode(x => x.Tracks.Any(y => y.ID == id)); + } + catch (ItemNotFound) + { + return NotFound(); + } + } + + [HttpGet("{slug}/episode")] + [Authorize(Policy = "Read")] + public async Task> GetEpisode(string slug) + { + try + { + // TODO This won't work with the local repository implementation. + // TODO Implement something like this (a dotnet-ef's QueryCompilationContext): https://stackoverflow.com/questions/62687811/how-can-i-convert-a-custom-function-to-a-sql-expression-for-entity-framework-cor + return await _libraryManager.GetEpisode(x => x.Tracks.Any(y => y.Slug == slug)); + } + catch (ItemNotFound) + { + return NotFound(); + } + } + } +} \ No newline at end of file