Removing usless calls, cleaning track's slugs, adding a track api, cleaning show & episode repository

This commit is contained in:
Zoe Roux 2021-03-17 19:56:02 +01:00
parent f028d369b9
commit 457dcd0db2
12 changed files with 145 additions and 213 deletions

View File

@ -30,7 +30,7 @@ namespace Kyoo.Controllers
public Sort(Expression<Func<T, object>> key, bool descendant = false)
{
Key = ExpressionRewrite.Rewrite<Func<T, object>>(key);
Key = key;
Descendant = descendant;
if (!Utility.IsPropertyExpression(Key))
@ -54,8 +54,7 @@ namespace Kyoo.Controllers
Key = property.Type.IsValueType
? Expression.Lambda<Func<T, object>>(Expression.Convert(property, typeof(object)), param)
: Expression.Lambda<Func<T, object>>(property, param);
Key = ExpressionRewrite.Rewrite<Func<T, object>>(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<TValue> To<TValue>()
{
return new(Key.Convert<Func<TValue, object>>(), Descendant);
}
}
public interface IRepository<T> : IDisposable, IAsyncDisposable where T : class, IResource

View File

@ -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<T> Rewrite<T>(Expression expression) where T : Delegate
{
return (Expression<T>)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<string, Expression>(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<ExpressionRewriteAttribute>();
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<T>(Expression<T> 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!);
}
}
}

View File

@ -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
};
}

View File

@ -256,6 +256,42 @@ namespace Kyoo
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
}
public static IEnumerable<T2> Map<T, T2>([CanBeNull] this IEnumerable<T> self,
[NotNull] Func<T, int, T2> mapper)
{
if (self == null)
yield break;
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0;
while (enumerator.MoveNext())
{
yield return mapper(enumerator.Current, index);
index++;
}
}
public static async IAsyncEnumerable<T2> MapAsync<T, T2>([CanBeNull] this IEnumerable<T> self,
[NotNull] Func<T, int, Task<T2>> mapper)
{
if (self == null)
yield break;
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0;
while (enumerator.MoveNext())
{
yield return await mapper(enumerator.Current, index);
index++;
}
}
public static async IAsyncEnumerable<T2> SelectAsync<T, T2>([CanBeNull] this IEnumerable<T> self,
[NotNull] Func<T, Task<T2>> mapper)
{
@ -584,57 +620,5 @@ namespace Kyoo
return true;
return firstID == secondID;
}
public static Expression<T> Convert<T>([CanBeNull] this Expression expr)
where T : Delegate
{
Expression<T> e = expr switch
{
null => null,
LambdaExpression lambda => new ExpressionConverter<T>(lambda).VisitAndConvert(),
_ => throw new ArgumentException("Can't convert a non lambda.")
};
return ExpressionRewrite.Rewrite<T>(e);
}
private class ExpressionConverter<TTo> : 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<TTo> VisitAndConvert()
{
Type returnType = _expression.Type.GetGenericArguments().Last();
Expression body = _expression.ReturnType == returnType
? Visit(_expression.Body)
: Expression.Convert(Visit(_expression.Body)!, returnType);
return Expression.Lambda<TTo>(body!, _newParams);
}
protected override Expression VisitParameter(ParameterExpression node)
{
return _newParams.FirstOrDefault(x => x.Name == node.Name) ?? node;
}
}
}
}

View File

@ -26,12 +26,7 @@ namespace Kyoo.CommonApi
Expression<Func<T, bool>> defaultWhere = null)
{
if (where == null || where.Count == 0)
{
if (defaultWhere == null)
return null;
Expression body = ExpressionRewrite.Rewrite(defaultWhere.Body);
return Expression.Lambda<Func<T, bool>>(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<Func<T, bool>>(expression, param);
return Expression.Lambda<Func<T, bool>>(expression!, param);
}
private static Expression ResourceEqual(Expression parameter, string value, bool notEqual = false)

View File

@ -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<Track> 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<Episode> 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)

View File

@ -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();

View File

@ -90,7 +90,7 @@
</Target>
<Target Name="Symlink views to output - Linux" AfterTargets="Build" Condition="$(Configuration) == 'Debug' And $(OS) == 'Unix'">
<Exec WorkingDirectory="$(OutputPath)" Command="ln -fs ../../../Views"/>
<Exec WorkingDirectory="$(OutputPath)" Command="ln -fs ../../../Views" />
</Target>
<Target Name="Compile the transcoder" BeforeTargets="BeforeBuild">

View File

@ -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<string>("Title")
.HasColumnType("text");
b.Property<int>("TrackIndex")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");

View File

@ -397,6 +397,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
EpisodeID = table.Column<int>(type: "integer", nullable: false),
TrackIndex = table.Column<int>(type: "integer", nullable: false),
IsDefault = table.Column<bool>(type: "boolean", nullable: false),
IsForced = table.Column<bool>(type: "boolean", nullable: false),
IsExternal = table.Column<bool>(type: "boolean", nullable: false),

View File

@ -489,6 +489,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<string>("Title")
.HasColumnType("text");
b.Property<int>("TrackIndex")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");

View File

@ -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<Track>
{
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<ActionResult<Episode>> 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<ActionResult<Episode>> 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();
}
}
}
}