Update csharpier

This commit is contained in:
Zoe Roux 2024-02-03 14:51:37 +01:00
parent 042cc018cb
commit a5638203a6
46 changed files with 811 additions and 958 deletions

View File

@ -9,7 +9,7 @@
] ]
}, },
"csharpier": { "csharpier": {
"version": "0.26.4", "version": "0.27.2",
"commands": [ "commands": [
"dotnet-csharpier" "dotnet-csharpier"
] ]

View File

@ -181,37 +181,35 @@ namespace Kyoo.Abstractions.Models
[LoadableRelation( [LoadableRelation(
// language=PostgreSQL // language=PostgreSQL
Sql = """ Sql = """
select select
pe.* -- Episode as pe pe.* -- Episode as pe
from from
episodes as "pe" episodes as "pe"
where where
pe.show_id = "this".show_id pe.show_id = "this".show_id
and (pe.absolute_number < "this".absolute_number and (pe.absolute_number < "this".absolute_number
or pe.season_number < "this".season_number or pe.season_number < "this".season_number
or (pe.season_number = "this".season_number or (pe.season_number = "this".season_number
and e.episode_number < "this".episode_number)) and e.episode_number < "this".episode_number))
order by order by
pe.absolute_number desc nulls last, pe.absolute_number desc nulls last,
pe.season_number desc, pe.season_number desc,
pe.episode_number desc pe.episode_number desc
limit 1 limit 1
""" """
)] )]
public Episode? PreviousEpisode { get; set; } public Episode? PreviousEpisode { get; set; }
private Episode? _PreviousEpisode => private Episode? _PreviousEpisode =>
Show! Show!
.Episodes! .Episodes!.OrderBy(x => x.AbsoluteNumber == null)
.OrderBy(x => x.AbsoluteNumber == null)
.ThenByDescending(x => x.AbsoluteNumber) .ThenByDescending(x => x.AbsoluteNumber)
.ThenByDescending(x => x.SeasonNumber) .ThenByDescending(x => x.SeasonNumber)
.ThenByDescending(x => x.EpisodeNumber) .ThenByDescending(x => x.EpisodeNumber)
.FirstOrDefault( .FirstOrDefault(x =>
x => x.AbsoluteNumber < AbsoluteNumber
x.AbsoluteNumber < AbsoluteNumber || x.SeasonNumber < SeasonNumber
|| x.SeasonNumber < SeasonNumber || (x.SeasonNumber == SeasonNumber && x.EpisodeNumber < EpisodeNumber)
|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber < EpisodeNumber)
); );
/// <summary> /// <summary>
@ -221,36 +219,34 @@ namespace Kyoo.Abstractions.Models
[LoadableRelation( [LoadableRelation(
// language=PostgreSQL // language=PostgreSQL
Sql = """ Sql = """
select select
ne.* -- Episode as ne ne.* -- Episode as ne
from from
episodes as "ne" episodes as "ne"
where where
ne.show_id = "this".show_id ne.show_id = "this".show_id
and (ne.absolute_number > "this".absolute_number and (ne.absolute_number > "this".absolute_number
or ne.season_number > "this".season_number or ne.season_number > "this".season_number
or (ne.season_number = "this".season_number or (ne.season_number = "this".season_number
and e.episode_number > "this".episode_number)) and e.episode_number > "this".episode_number))
order by order by
ne.absolute_number, ne.absolute_number,
ne.season_number, ne.season_number,
ne.episode_number ne.episode_number
limit 1 limit 1
""" """
)] )]
public Episode? NextEpisode { get; set; } public Episode? NextEpisode { get; set; }
private Episode? _NextEpisode => private Episode? _NextEpisode =>
Show! Show!
.Episodes! .Episodes!.OrderBy(x => x.AbsoluteNumber)
.OrderBy(x => x.AbsoluteNumber)
.ThenBy(x => x.SeasonNumber) .ThenBy(x => x.SeasonNumber)
.ThenBy(x => x.EpisodeNumber) .ThenBy(x => x.EpisodeNumber)
.FirstOrDefault( .FirstOrDefault(x =>
x => x.AbsoluteNumber > AbsoluteNumber
x.AbsoluteNumber > AbsoluteNumber || x.SeasonNumber > SeasonNumber
|| x.SeasonNumber > SeasonNumber || (x.SeasonNumber == SeasonNumber && x.EpisodeNumber > EpisodeNumber)
|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber > EpisodeNumber)
); );
[SerializeIgnore] [SerializeIgnore]

View File

@ -135,14 +135,14 @@ namespace Kyoo.Abstractions.Models
[LoadableRelation( [LoadableRelation(
// language=PostgreSQL // language=PostgreSQL
Projected = """ Projected = """
( (
select select
count(*)::int count(*)::int
from from
episodes as e episodes as e
where where
e.season_id = id) as episodes_count e.season_id = id) as episodes_count
""" """
)] )]
public int EpisodesCount { get; set; } public int EpisodesCount { get; set; }

View File

@ -170,17 +170,17 @@ namespace Kyoo.Abstractions.Models
[LoadableRelation( [LoadableRelation(
// language=PostgreSQL // language=PostgreSQL
Sql = """ Sql = """
select
fe.* -- Episode as fe
from (
select select
e.*, fe.* -- Episode as fe
row_number() over (partition by e.show_id order by e.absolute_number, e.season_number, e.episode_number) as number from (
from select
episodes as e) as "fe" e.*,
where row_number() over (partition by e.show_id order by e.absolute_number, e.season_number, e.episode_number) as number
fe.number <= 1 from
""", episodes as e) as "fe"
where
fe.number <= 1
""",
On = "show_id = \"this\".id" On = "show_id = \"this\".id"
)] )]
public Episode? FirstEpisode { get; set; } public Episode? FirstEpisode { get; set; }
@ -200,14 +200,14 @@ namespace Kyoo.Abstractions.Models
[LoadableRelation( [LoadableRelation(
// language=PostgreSQL // language=PostgreSQL
Projected = """ Projected = """
( (
select select
count(*)::int count(*)::int
from from
episodes as e episodes as e
where where
e.show_id = "this".id) as episodes_count e.show_id = "this".id) as episodes_count
""" """
)] )]
public int EpisodesCount { get; set; } public int EpisodesCount { get; set; }

View File

@ -205,8 +205,7 @@ public abstract record Filter<T> : Filter
if (type.IsEnum) if (type.IsEnum)
{ {
return Parse return Parse
.LetterOrDigit .LetterOrDigit.Many()
.Many()
.Text() .Text()
.Then(x => .Then(x =>
{ {
@ -259,14 +258,11 @@ public abstract record Filter<T> : Filter
} }
PropertyInfo? propInfo = types PropertyInfo? propInfo = types
.Select( .Select(x =>
x => x.GetProperty(
x.GetProperty( prop,
prop, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
BindingFlags.IgnoreCase )
| BindingFlags.Public
| BindingFlags.Instance
)
) )
.FirstOrDefault(); .FirstOrDefault();
if (propInfo == null) if (propInfo == null)

View File

@ -62,17 +62,14 @@ public class Include<T> : Include
.SelectMany(key => .SelectMany(key =>
{ {
var relations = types var relations = types
.Select( .Select(x =>
x => x.GetProperty(
x.GetProperty( key,
key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
BindingFlags.IgnoreCase )!
| BindingFlags.Public
| BindingFlags.Instance
)!
) )
.Select( .Select(prop =>
prop => (prop, attr: prop?.GetCustomAttribute<LoadableRelationAttribute>()!) (prop, attr: prop?.GetCustomAttribute<LoadableRelationAttribute>()!)
) )
.Where(x => x.prop != null && x.attr != null) .Where(x => x.prop != null && x.attr != null)
.ToList(); .ToList();

View File

@ -120,12 +120,11 @@ namespace Kyoo.Abstractions.Controllers
Type[] types = Type[] types =
typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) }; typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
PropertyInfo? property = types PropertyInfo? property = types
.Select( .Select(x =>
x => x.GetProperty(
x.GetProperty( key,
key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance )
)
) )
.FirstOrDefault(x => x != null); .FirstOrDefault(x => x != null);
if (property == null) if (property == null)

View File

@ -60,8 +60,8 @@ namespace Kyoo.Utils
hasChanged = false; hasChanged = false;
if (second == null) if (second == null)
return first; return first;
hasChanged = second.Any( hasChanged = second.Any(x =>
x => !first.ContainsKey(x.Key) || x.Value?.Equals(first[x.Key]) == false !first.ContainsKey(x.Key) || x.Value?.Equals(first[x.Key]) == false
); );
foreach ((T key, T2 value) in first) foreach ((T key, T2 value) in first)
second.TryAdd(key, value); second.TryAdd(key, value);
@ -98,10 +98,9 @@ namespace Kyoo.Utils
Type type = typeof(T); Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties() IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where( .Where(x =>
x => x is { CanRead: true, CanWrite: true }
x is { CanRead: true, CanWrite: true } && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null
); );
if (where != null) if (where != null)

View File

@ -206,8 +206,8 @@ namespace Kyoo.Utils
: type.GetInheritanceTree(); : type.GetInheritanceTree();
return types return types
.Prepend(type) .Prepend(type)
.FirstOrDefault( .FirstOrDefault(x =>
x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType x.IsGenericType && x.GetGenericTypeDefinition() == genericType
); );
} }

View File

@ -229,11 +229,10 @@ namespace Kyoo.Authentication
private AuthenticateResult _ApiKeyCheck(ActionContext context) private AuthenticateResult _ApiKeyCheck(ActionContext context)
{ {
if ( if (
!context !context.HttpContext.Request.Headers.TryGetValue(
.HttpContext "X-API-Key",
.Request out StringValues apiKey
.Headers )
.TryGetValue("X-API-Key", out StringValues apiKey)
) )
return AuthenticateResult.NoResult(); return AuthenticateResult.NoResult();
if (!_options.ApiKeys.Contains<string>(apiKey!)) if (!_options.ApiKeys.Contains<string>(apiKey!))
@ -262,9 +261,9 @@ namespace Kyoo.Authentication
private async Task<AuthenticateResult> _JwtCheck(ActionContext context) private async Task<AuthenticateResult> _JwtCheck(ActionContext context)
{ {
AuthenticateResult ret = await context AuthenticateResult ret = await context.HttpContext.AuthenticateAsync(
.HttpContext JwtBearerDefaults.AuthenticationScheme
.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); );
// Change the failure message to make the API nice to use. // Change the failure message to make the API nice to use.
if (ret.Failure != null) if (ret.Failure != null)
return AuthenticateResult.Fail( return AuthenticateResult.Fail(

View File

@ -40,10 +40,8 @@ namespace Kyoo.Authentication.Models
get get
{ {
return Enum.GetNames<Group>() return Enum.GetNames<Group>()
.SelectMany( .SelectMany(group =>
group => Enum.GetNames<Kind>().Select(kind => $"{group}.{kind}".ToLowerInvariant())
Enum.GetNames<Kind>()
.Select(kind => $"{group}.{kind}".ToLowerInvariant())
) )
.ToArray(); .ToArray();
} }

View File

@ -65,9 +65,8 @@ public static class DapperHelper
.Where(x => !x.Key.StartsWith('_')) .Where(x => !x.Key.StartsWith('_'))
// If first char is lower, assume manual sql instead of reflection. // If first char is lower, assume manual sql instead of reflection.
.Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null) .Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null)
.Select( .Select(x =>
x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}"
$"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}"
) )
.ToArray(); .ToArray();
if (keys.Length == 1) if (keys.Length == 1)
@ -149,8 +148,7 @@ public static class DapperHelper
T Map(T item, IEnumerable<object?> relations) T Map(T item, IEnumerable<object?> relations)
{ {
IEnumerable<string> metadatas = include IEnumerable<string> metadatas = include
.Metadatas .Metadatas.Where(x => x is not Include.ProjectedRelation)
.Where(x => x is not Include.ProjectedRelation)
.Select(x => x.Name); .Select(x => x.Name);
foreach ((string name, object? value) in metadatas.Zip(relations)) foreach ((string name, object? value) in metadatas.Zip(relations))
{ {
@ -179,26 +177,24 @@ public static class DapperHelper
'\n', '\n',
config config
.Skip(1) .Skip(1)
.Select( .Select(x =>
x => $"when {x.Key}.id is not null then '{x.Value.Name.ToLowerInvariant()}'"
$"when {x.Key}.id is not null then '{x.Value.Name.ToLowerInvariant()}'"
) )
); );
return $""" return $"""
case case
{cases:raw} {cases:raw}
else '{config.First().Value.Name.ToLowerInvariant():raw}' else '{config.First().Value.Name.ToLowerInvariant():raw}'
end {op} end {op}
"""; """;
} }
IEnumerable<string> properties = config IEnumerable<string> properties = config
.Where(x => !x.Key.StartsWith('_')) .Where(x => !x.Key.StartsWith('_'))
// If first char is lower, assume manual sql instead of reflection. // If first char is lower, assume manual sql instead of reflection.
.Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null) .Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null)
.Select( .Select(x =>
x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}"
$"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}"
); );
FormattableString ret = $"{properties.First():raw} {op}"; FormattableString ret = $"{properties.First():raw} {op}";
@ -245,8 +241,7 @@ public static class DapperHelper
public static string ExpendProjections(Type type, string? prefix, Include include) public static string ExpendProjections(Type type, string? prefix, Include include)
{ {
IEnumerable<string> projections = include IEnumerable<string> projections = include
.Metadatas .Metadatas.Select(x => (x as Include.ProjectedRelation)!)
.Select(x => (x as Include.ProjectedRelation)!)
.Where(x => x != null) .Where(x => x != null)
.Where(x => type.GetProperty(x.Name) != null) .Where(x => type.GetProperty(x.Name) != null)
.Select(x => x.Sql.Replace("\"this\".", prefix)); .Select(x => x.Sql.Replace("\"this\".", prefix));
@ -336,8 +331,8 @@ public static class DapperHelper
{ {
string posterProj = string.Join( string posterProj = string.Join(
", ", ", ",
new[] { "poster", "thumbnail", "logo" }.Select( new[] { "poster", "thumbnail", "logo" }.Select(x =>
x => $"{prefix}{x}_source as source, {prefix}{x}_blurhash as blurhash" $"{prefix}{x}_source as source, {prefix}{x}_blurhash as blurhash"
) )
); );
projection = string.IsNullOrEmpty(projection) projection = string.IsNullOrEmpty(projection)

View File

@ -47,12 +47,10 @@ namespace Kyoo.Core.Controllers
IRepository<Show>.OnEdited += async (show) => IRepository<Show>.OnEdited += async (show) =>
{ {
await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope(); await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope();
DatabaseContext database = scope DatabaseContext database =
.ServiceProvider scope.ServiceProvider.GetRequiredService<DatabaseContext>();
.GetRequiredService<DatabaseContext>();
List<Episode> episodes = await database List<Episode> episodes = await database
.Episodes .Episodes.AsTracking()
.AsTracking()
.Where(x => x.ShowId == show.Id) .Where(x => x.ShowId == show.Id)
.ToListAsync(); .ToListAsync();
foreach (Episode ep in episodes) foreach (Episode ep in episodes)
@ -96,19 +94,14 @@ namespace Kyoo.Core.Controllers
protected override Task<Episode?> GetDuplicated(Episode item) protected override Task<Episode?> GetDuplicated(Episode item)
{ {
if (item is { SeasonNumber: not null, EpisodeNumber: not null }) if (item is { SeasonNumber: not null, EpisodeNumber: not null })
return _database return _database.Episodes.FirstOrDefaultAsync(x =>
.Episodes x.ShowId == item.ShowId
.FirstOrDefaultAsync( && x.SeasonNumber == item.SeasonNumber
x => && x.EpisodeNumber == item.EpisodeNumber
x.ShowId == item.ShowId
&& x.SeasonNumber == item.SeasonNumber
&& x.EpisodeNumber == item.EpisodeNumber
);
return _database
.Episodes
.FirstOrDefaultAsync(
x => x.ShowId == item.ShowId && x.AbsoluteNumber == item.AbsoluteNumber
); );
return _database.Episodes.FirstOrDefaultAsync(x =>
x.ShowId == item.ShowId && x.AbsoluteNumber == item.AbsoluteNumber
);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -140,11 +133,9 @@ namespace Kyoo.Core.Controllers
} }
if (resource.SeasonId == null && resource.SeasonNumber != null) if (resource.SeasonId == null && resource.SeasonNumber != null)
{ {
resource.Season = await _database resource.Season = await _database.Seasons.FirstOrDefaultAsync(x =>
.Seasons x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber
.FirstOrDefaultAsync( );
x => x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber
);
} }
} }
@ -152,8 +143,7 @@ namespace Kyoo.Core.Controllers
public override async Task Delete(Episode obj) public override async Task Delete(Episode obj)
{ {
int epCount = await _database int epCount = await _database
.Episodes .Episodes.Where(x => x.ShowId == obj.ShowId)
.Where(x => x.ShowId == obj.ShowId)
.Take(2) .Take(2)
.CountAsync(); .CountAsync();
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;

View File

@ -35,29 +35,29 @@ namespace Kyoo.Core.Controllers
// language=PostgreSQL // language=PostgreSQL
protected override FormattableString Sql => protected override FormattableString Sql =>
$""" $"""
select
s.*, -- Show as s
m.*,
c.*
/* includes */
from
shows as s
full outer join (
select select
* -- Movie s.*, -- Show as s
m.*,
c.*
/* includes */
from from
movies) as m on false shows as s
full outer join( full outer join (
select select
c.* -- Collection as c * -- Movie
from from
collections as c movies) as m on false
left join link_collection_show as ls on ls.collection_id = c.id full outer join(
left join link_collection_movie as lm on lm.collection_id = c.id select
group by c.id c.* -- Collection as c
having count(*) > 1 from
) as c on false collections as c
"""; left join link_collection_show as ls on ls.collection_id = c.id
left join link_collection_movie as lm on lm.collection_id = c.id
group by c.id
having count(*) > 1
) as c on false
""";
protected override Dictionary<string, Type> Config => protected override Dictionary<string, Type> Config =>
new() new()

View File

@ -32,19 +32,19 @@ namespace Kyoo.Core.Controllers
// language=PostgreSQL // language=PostgreSQL
protected override FormattableString Sql => protected override FormattableString Sql =>
$""" $"""
select
e.*, -- Episode as e
m.*
/* includes */
from
episodes as e
full outer join (
select select
* -- Movie e.*, -- Episode as e
m.*
/* includes */
from from
movies episodes as e
) as m on false full outer join (
"""; select
* -- Movie
from
movies
) as m on false
""";
protected override Dictionary<string, Type> Config => protected override Dictionary<string, Type> Config =>
new() { { "e", typeof(Episode) }, { "m", typeof(Movie) }, }; new() { { "e", typeof(Episode) }, { "m", typeof(Movie) }, };

View File

@ -47,12 +47,10 @@ namespace Kyoo.Core.Controllers
IRepository<Show>.OnEdited += async (show) => IRepository<Show>.OnEdited += async (show) =>
{ {
await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope(); await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope();
DatabaseContext database = scope DatabaseContext database =
.ServiceProvider scope.ServiceProvider.GetRequiredService<DatabaseContext>();
.GetRequiredService<DatabaseContext>();
List<Season> seasons = await database List<Season> seasons = await database
.Seasons .Seasons.AsTracking()
.AsTracking()
.Where(x => x.ShowId == show.Id) .Where(x => x.ShowId == show.Id)
.ToListAsync(); .ToListAsync();
foreach (Season season in seasons) foreach (Season season in seasons)
@ -77,11 +75,9 @@ namespace Kyoo.Core.Controllers
protected override Task<Season?> GetDuplicated(Season item) protected override Task<Season?> GetDuplicated(Season item)
{ {
return _database return _database.Seasons.FirstOrDefaultAsync(x =>
.Seasons x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber
.FirstOrDefaultAsync( );
x => x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber
);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@ -66,11 +66,10 @@ public class WatchStatusRepository : IWatchStatusRepository
{ {
await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope(); await using AsyncServiceScope scope = CoreModule.Services.CreateAsyncScope();
DatabaseContext db = scope.ServiceProvider.GetRequiredService<DatabaseContext>(); DatabaseContext db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
WatchStatusRepository repo = scope WatchStatusRepository repo =
.ServiceProvider scope.ServiceProvider.GetRequiredService<WatchStatusRepository>();
.GetRequiredService<WatchStatusRepository>(); List<Guid> users = await db
List<Guid> users = await db.ShowWatchStatus .ShowWatchStatus.IgnoreQueryFilters()
.IgnoreQueryFilters()
.Where(x => x.ShowId == ep.ShowId && x.Status == WatchStatus.Completed) .Where(x => x.ShowId == ep.ShowId && x.Status == WatchStatus.Completed)
.Select(x => x.UserId) .Select(x => x.UserId)
.ToListAsync(); .ToListAsync();
@ -95,41 +94,41 @@ public class WatchStatusRepository : IWatchStatusRepository
// language=PostgreSQL // language=PostgreSQL
protected FormattableString Sql => protected FormattableString Sql =>
$""" $"""
select
s.*,
swe.*, -- Episode as swe
m.*
/* includes */
from (
select select
s.*, -- Show as s s.*,
sw.*, swe.*, -- Episode as swe
sw.added_date as order, m.*
sw.status as watch_status /* includes */
from from (
shows as s select
inner join show_watch_status as sw on sw.show_id = s.id s.*, -- Show as s
and sw.user_id = [current_user]) as s sw.*,
full outer join ( sw.added_date as order,
select sw.status as watch_status
m.*, -- Movie as m from
mw.*, shows as s
mw.added_date as order, inner join show_watch_status as sw on sw.show_id = s.id
mw.status as watch_status and sw.user_id = [current_user]) as s
from full outer join (
movies as m select
inner join movie_watch_status as mw on mw.movie_id = m.id m.*, -- Movie as m
and mw.user_id = [current_user]) as m on false mw.*,
left join episodes as swe on swe.id = s.next_episode_id mw.added_date as order,
/* includesJoin */ mw.status as watch_status
where from
(coalesce(s.watch_status, m.watch_status) = 'watching'::watch_status movies as m
or coalesce(s.watch_status, m.watch_status) = 'planned'::watch_status) inner join movie_watch_status as mw on mw.movie_id = m.id
/* where */ and mw.user_id = [current_user]) as m on false
order by left join episodes as swe on swe.id = s.next_episode_id
coalesce(s.order, m.order) desc, /* includesJoin */
coalesce(s.id, m.id) asc where
"""; (coalesce(s.watch_status, m.watch_status) = 'watching'::watch_status
or coalesce(s.watch_status, m.watch_status) = 'planned'::watch_status)
/* where */
order by
coalesce(s.order, m.order) desc,
coalesce(s.id, m.id) asc
""";
protected Dictionary<string, Type> Config => protected Dictionary<string, Type> Config =>
new() new()
@ -189,8 +188,7 @@ public class WatchStatusRepository : IWatchStatusRepository
{ {
if (include != null) if (include != null)
include.Metadatas = include include.Metadatas = include
.Metadatas .Metadatas.Where(x => x.Name != nameof(Show.WatchStatus))
.Where(x => x.Name != nameof(Show.WatchStatus))
.ToList(); .ToList();
// We can't use the generic after id hanler since the sort depends on a relation. // We can't use the generic after id hanler since the sort depends on a relation.
@ -226,9 +224,9 @@ public class WatchStatusRepository : IWatchStatusRepository
/// <inheritdoc /> /// <inheritdoc />
public Task<MovieWatchStatus?> GetMovieStatus(Guid movieId, Guid userId) public Task<MovieWatchStatus?> GetMovieStatus(Guid movieId, Guid userId)
{ {
return _database return _database.MovieWatchStatus.FirstOrDefaultAsync(x =>
.MovieWatchStatus x.MovieId == movieId && x.UserId == userId
.FirstOrDefaultAsync(x => x.MovieId == movieId && x.UserId == userId); );
} }
/// <inheritdoc /> /// <inheritdoc />
@ -277,8 +275,7 @@ public class WatchStatusRepository : IWatchStatusRepository
PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null, PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
}; };
await _database await _database
.MovieWatchStatus .MovieWatchStatus.Upsert(ret)
.Upsert(ret)
.UpdateIf(x => status != Watching || x.Status != Completed) .UpdateIf(x => status != Watching || x.Status != Completed)
.RunAsync(); .RunAsync();
return ret; return ret;
@ -288,17 +285,16 @@ public class WatchStatusRepository : IWatchStatusRepository
public async Task DeleteMovieStatus(Guid movieId, Guid userId) public async Task DeleteMovieStatus(Guid movieId, Guid userId)
{ {
await _database await _database
.MovieWatchStatus .MovieWatchStatus.Where(x => x.MovieId == movieId && x.UserId == userId)
.Where(x => x.MovieId == movieId && x.UserId == userId)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<ShowWatchStatus?> GetShowStatus(Guid showId, Guid userId) public Task<ShowWatchStatus?> GetShowStatus(Guid showId, Guid userId)
{ {
return _database return _database.ShowWatchStatus.FirstOrDefaultAsync(x =>
.ShowWatchStatus x.ShowId == showId && x.UserId == userId
.FirstOrDefaultAsync(x => x.ShowId == showId && x.UserId == userId); );
} }
/// <inheritdoc /> /// <inheritdoc />
@ -315,12 +311,9 @@ public class WatchStatusRepository : IWatchStatusRepository
int unseenEpisodeCount = int unseenEpisodeCount =
status != WatchStatus.Completed status != WatchStatus.Completed
? await _database ? await _database
.Episodes .Episodes.Where(x => x.ShowId == showId)
.Where(x => x.ShowId == showId) .Where(x =>
.Where( x.Watched!.First(x => x.UserId == userId)!.Status != WatchStatus.Completed
x =>
x.Watched!.First(x => x.UserId == userId)!.Status
!= WatchStatus.Completed
) )
.CountAsync() .CountAsync()
: 0; : 0;
@ -332,57 +325,47 @@ public class WatchStatusRepository : IWatchStatusRepository
if (status == WatchStatus.Watching) if (status == WatchStatus.Watching)
{ {
var cursor = await _database var cursor = await _database
.Episodes .Episodes.IgnoreQueryFilters()
.IgnoreQueryFilters()
.Where(x => x.ShowId == showId) .Where(x => x.ShowId == showId)
.OrderByDescending(x => x.AbsoluteNumber) .OrderByDescending(x => x.AbsoluteNumber)
.ThenByDescending(x => x.SeasonNumber) .ThenByDescending(x => x.SeasonNumber)
.ThenByDescending(x => x.EpisodeNumber) .ThenByDescending(x => x.EpisodeNumber)
.Select( .Select(x => new
x => {
new x.Id,
{ x.AbsoluteNumber,
x.Id, x.SeasonNumber,
x.AbsoluteNumber, x.EpisodeNumber,
x.SeasonNumber, Status = x.Watched!.First(x => x.UserId == userId)
x.EpisodeNumber, })
Status = x.Watched!.First(x => x.UserId == userId) .FirstOrDefaultAsync(x =>
} x.Status.Status == WatchStatus.Completed
) || x.Status.Status == WatchStatus.Watching
.FirstOrDefaultAsync(
x =>
x.Status.Status == WatchStatus.Completed
|| x.Status.Status == WatchStatus.Watching
); );
cursorWatchStatus = cursor?.Status; cursorWatchStatus = cursor?.Status;
nextEpisodeId = nextEpisodeId =
cursor?.Status.Status == WatchStatus.Watching cursor?.Status.Status == WatchStatus.Watching
? cursor.Id ? cursor.Id
: await _database : await _database
.Episodes .Episodes.IgnoreQueryFilters()
.IgnoreQueryFilters()
.Where(x => x.ShowId == showId) .Where(x => x.ShowId == showId)
.OrderBy(x => x.AbsoluteNumber) .OrderBy(x => x.AbsoluteNumber)
.ThenBy(x => x.SeasonNumber) .ThenBy(x => x.SeasonNumber)
.ThenBy(x => x.EpisodeNumber) .ThenBy(x => x.EpisodeNumber)
.Where( .Where(x =>
x => cursor == null
cursor == null || x.AbsoluteNumber > cursor.AbsoluteNumber
|| x.AbsoluteNumber > cursor.AbsoluteNumber || x.SeasonNumber > cursor.SeasonNumber
|| x.SeasonNumber > cursor.SeasonNumber || (
|| ( x.SeasonNumber == cursor.SeasonNumber
x.SeasonNumber == cursor.SeasonNumber && x.EpisodeNumber > cursor.EpisodeNumber
&& x.EpisodeNumber > cursor.EpisodeNumber )
)
)
.Select(
x =>
new
{
x.Id,
Status = x.Watched!.FirstOrDefault(x => x.UserId == userId)
}
) )
.Select(x => new
{
x.Id,
Status = x.Watched!.FirstOrDefault(x => x.UserId == userId)
})
.Where(x => x.Status == null || x.Status.Status != WatchStatus.Completed) .Where(x => x.Status == null || x.Status.Status != WatchStatus.Completed)
// The as Guid? is here to add the nullability status of the queryable. // The as Guid? is here to add the nullability status of the queryable.
// Without this, FirstOrDefault returns new Guid() when no result is found (which is 16 0s and invalid in sql). // Without this, FirstOrDefault returns new Guid() when no result is found (which is 16 0s and invalid in sql).
@ -392,24 +375,19 @@ public class WatchStatusRepository : IWatchStatusRepository
else if (status == WatchStatus.Completed) else if (status == WatchStatus.Completed)
{ {
List<Guid> episodes = await _database List<Guid> episodes = await _database
.Episodes .Episodes.Where(x => x.ShowId == showId)
.Where(x => x.ShowId == showId)
.Select(x => x.Id) .Select(x => x.Id)
.ToListAsync(); .ToListAsync();
await _database await _database
.EpisodeWatchStatus .EpisodeWatchStatus.UpsertRange(
.UpsertRange( episodes.Select(episodeId => new EpisodeWatchStatus
episodes.Select( {
episodeId => UserId = userId,
new EpisodeWatchStatus EpisodeId = episodeId,
{ Status = WatchStatus.Completed,
UserId = userId, AddedDate = DateTime.UtcNow,
EpisodeId = episodeId, PlayedDate = DateTime.UtcNow
Status = WatchStatus.Completed, })
AddedDate = DateTime.UtcNow,
PlayedDate = DateTime.UtcNow
}
)
) )
.UpdateIf(x => x.Status == Watching || x.Status == Planned) .UpdateIf(x => x.Status == Watching || x.Status == Planned)
.RunAsync(); .RunAsync();
@ -435,8 +413,7 @@ public class WatchStatusRepository : IWatchStatusRepository
PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null, PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
}; };
await _database await _database
.ShowWatchStatus .ShowWatchStatus.Upsert(ret)
.Upsert(ret)
.UpdateIf(x => status != Watching || x.Status != Completed || newEpisode) .UpdateIf(x => status != Watching || x.Status != Completed || newEpisode)
.RunAsync(); .RunAsync();
return ret; return ret;
@ -446,22 +423,20 @@ public class WatchStatusRepository : IWatchStatusRepository
public async Task DeleteShowStatus(Guid showId, Guid userId) public async Task DeleteShowStatus(Guid showId, Guid userId)
{ {
await _database await _database
.ShowWatchStatus .ShowWatchStatus.IgnoreAutoIncludes()
.IgnoreAutoIncludes()
.Where(x => x.ShowId == showId && x.UserId == userId) .Where(x => x.ShowId == showId && x.UserId == userId)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
await _database await _database
.EpisodeWatchStatus .EpisodeWatchStatus.Where(x => x.Episode.ShowId == showId && x.UserId == userId)
.Where(x => x.Episode.ShowId == showId && x.UserId == userId)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<EpisodeWatchStatus?> GetEpisodeStatus(Guid episodeId, Guid userId) public Task<EpisodeWatchStatus?> GetEpisodeStatus(Guid episodeId, Guid userId)
{ {
return _database return _database.EpisodeWatchStatus.FirstOrDefaultAsync(x =>
.EpisodeWatchStatus x.EpisodeId == episodeId && x.UserId == userId
.FirstOrDefaultAsync(x => x.EpisodeId == episodeId && x.UserId == userId); );
} }
/// <inheritdoc /> /// <inheritdoc />
@ -510,8 +485,7 @@ public class WatchStatusRepository : IWatchStatusRepository
PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null, PlayedDate = status == WatchStatus.Completed ? DateTime.UtcNow : null,
}; };
await _database await _database
.EpisodeWatchStatus .EpisodeWatchStatus.Upsert(ret)
.Upsert(ret)
.UpdateIf(x => status != Watching || x.Status != Completed) .UpdateIf(x => status != Watching || x.Status != Completed)
.RunAsync(); .RunAsync();
await SetShowStatus(episode.ShowId, userId, WatchStatus.Watching); await SetShowStatus(episode.ShowId, userId, WatchStatus.Watching);
@ -522,8 +496,7 @@ public class WatchStatusRepository : IWatchStatusRepository
public async Task DeleteEpisodeStatus(Guid episodeId, Guid userId) public async Task DeleteEpisodeStatus(Guid episodeId, Guid userId)
{ {
await _database await _database
.EpisodeWatchStatus .EpisodeWatchStatus.Where(x => x.EpisodeId == episodeId && x.UserId == userId)
.Where(x => x.EpisodeId == episodeId && x.UserId == userId)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
} }
} }

View File

@ -212,14 +212,13 @@ namespace Kyoo.Core.Controllers
{ {
IEnumerable<string> images = new[] { "poster", "thumbnail", "logo" } IEnumerable<string> images = new[] { "poster", "thumbnail", "logo" }
.SelectMany(x => _GetBaseImagePath(item, x)) .SelectMany(x => _GetBaseImagePath(item, x))
.SelectMany( .SelectMany(x =>
x => new[]
new[] {
{ ImageQuality.High.ToString().ToLowerInvariant(),
ImageQuality.High.ToString().ToLowerInvariant(), ImageQuality.Medium.ToString().ToLowerInvariant(),
ImageQuality.Medium.ToString().ToLowerInvariant(), ImageQuality.Low.ToString().ToLowerInvariant(),
ImageQuality.Low.ToString().ToLowerInvariant(), }.Select(quality => $"{x}.{quality}.webp")
}.Select(quality => $"{x}.{quality}.webp")
); );
foreach (string image in images) foreach (string image in images)

View File

@ -105,8 +105,8 @@ namespace Kyoo.Core
options.SuppressMapClientErrors = true; options.SuppressMapClientErrors = true;
options.InvalidModelStateResponseFactory = ctx => options.InvalidModelStateResponseFactory = ctx =>
{ {
string[] errors = ctx.ModelState string[] errors = ctx
.SelectMany(x => x.Value!.Errors) .ModelState.SelectMany(x => x.Value!.Errors)
.Select(x => x.ErrorMessage) .Select(x => x.ErrorMessage)
.ToArray(); .ToArray();
return new BadRequestObjectResult(new RequestError(errors)); return new BadRequestObjectResult(new RequestError(errors));

View File

@ -45,13 +45,11 @@ namespace Kyoo.Core.Api
protected Page<TResult> Page<TResult>(ICollection<TResult> resources, int limit) protected Page<TResult> Page<TResult>(ICollection<TResult> resources, int limit)
where TResult : IResource where TResult : IResource
{ {
Dictionary<string, string> query = Request Dictionary<string, string> query = Request.Query.ToDictionary(
.Query x => x.Key,
.ToDictionary( x => x.Value.ToString(),
x => x.Key, StringComparer.InvariantCultureIgnoreCase
x => x.Value.ToString(), );
StringComparer.InvariantCultureIgnoreCase
);
// If the query was sorted randomly, add the seed to the url to get reproducible links (next,prev,first...) // If the query was sorted randomly, add the seed to the url to get reproducible links (next,prev,first...)
if (query.ContainsKey("sortBy")) if (query.ContainsKey("sortBy"))
@ -66,13 +64,11 @@ namespace Kyoo.Core.Api
protected SearchPage<TResult> SearchPage<TResult>(SearchPage<TResult>.SearchResult result) protected SearchPage<TResult> SearchPage<TResult>(SearchPage<TResult>.SearchResult result)
where TResult : IResource where TResult : IResource
{ {
Dictionary<string, string> query = Request Dictionary<string, string> query = Request.Query.ToDictionary(
.Query x => x.Key,
.ToDictionary( x => x.Value.ToString(),
x => x.Key, StringComparer.InvariantCultureIgnoreCase
x => x.Value.ToString(), );
StringComparer.InvariantCultureIgnoreCase
);
string self = Request.Path + query.ToQueryString(); string self = Request.Path + query.ToQueryString();
string? previous = null; string? previous = null;

View File

@ -28,14 +28,13 @@ public class FilterBinder : IModelBinder
{ {
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)
{ {
ValueProviderResult fields = bindingContext ValueProviderResult fields = bindingContext.ValueProvider.GetValue(
.ValueProvider bindingContext.FieldName
.GetValue(bindingContext.FieldName); );
try try
{ {
object? filter = bindingContext object? filter = bindingContext
.ModelType .ModelType.GetMethod(nameof(Filter<object>.From))!
.GetMethod(nameof(Filter<object>.From))!
.Invoke(null, new object?[] { fields.FirstValue }); .Invoke(null, new object?[] { fields.FirstValue });
bindingContext.Result = ModelBindingResult.Success(filter); bindingContext.Result = ModelBindingResult.Success(filter);
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -31,14 +31,13 @@ public class IncludeBinder : IModelBinder
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)
{ {
ValueProviderResult fields = bindingContext ValueProviderResult fields = bindingContext.ValueProvider.GetValue(
.ValueProvider bindingContext.FieldName
.GetValue(bindingContext.FieldName); );
try try
{ {
object include = bindingContext object include = bindingContext
.ModelType .ModelType.GetMethod(nameof(Include<object>.From))!
.GetMethod(nameof(Include<object>.From))!
.Invoke(null, new object?[] { fields.FirstValue })!; .Invoke(null, new object?[] { fields.FirstValue })!;
bindingContext.Result = ModelBindingResult.Success(include); bindingContext.Result = ModelBindingResult.Success(include);
bindingContext.HttpContext.Items["fields"] = ((dynamic)include).Fields; bindingContext.HttpContext.Items["fields"] = ((dynamic)include).Fields;

View File

@ -32,9 +32,9 @@ public class SortBinder : IModelBinder
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)
{ {
ValueProviderResult sortBy = bindingContext ValueProviderResult sortBy = bindingContext.ValueProvider.GetValue(
.ValueProvider bindingContext.FieldName
.GetValue(bindingContext.FieldName); );
uint seed = BitConverter.ToUInt32( uint seed = BitConverter.ToUInt32(
BitConverter.GetBytes(_rng.Next(int.MinValue, int.MaxValue)), BitConverter.GetBytes(_rng.Next(int.MinValue, int.MaxValue)),
0 0
@ -42,8 +42,7 @@ public class SortBinder : IModelBinder
try try
{ {
object sort = bindingContext object sort = bindingContext
.ModelType .ModelType.GetMethod(nameof(Sort<Movie>.From))!
.GetMethod(nameof(Sort<Movie>.From))!
.Invoke(null, new object?[] { sortBy.FirstValue, seed })!; .Invoke(null, new object?[] { sortBy.FirstValue, seed })!;
bindingContext.Result = ModelBindingResult.Success(sort); bindingContext.Result = ModelBindingResult.Success(sort);
bindingContext.HttpContext.Items["seed"] = seed; bindingContext.HttpContext.Items["seed"] = seed;

View File

@ -85,17 +85,12 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Show> fields [FromQuery] Include<Show> fields
) )
{ {
ICollection<Show> resources = await _libraryManager ICollection<Show> resources = await _libraryManager.Shows.GetAll(
.Shows Filter.And(filter, identifier.Matcher<Show>(x => x.StudioId, x => x.Studio!.Slug)),
.GetAll( sortBy,
Filter.And( fields,
filter, pagination
identifier.Matcher<Show>(x => x.StudioId, x => x.Studio!.Slug) );
),
sortBy,
fields,
pagination
);
if ( if (
!resources.Any() !resources.Any()

View File

@ -200,17 +200,12 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Show>? fields [FromQuery] Include<Show>? fields
) )
{ {
ICollection<Show> resources = await _libraryManager ICollection<Show> resources = await _libraryManager.Shows.GetAll(
.Shows Filter.And(filter, identifier.IsContainedIn<Show, Collection>(x => x.Collections)),
.GetAll( sortBy == new Sort<Show>.Default() ? new Sort<Show>.By(x => x.AirDate) : sortBy,
Filter.And( fields,
filter, pagination
identifier.IsContainedIn<Show, Collection>(x => x.Collections) );
),
sortBy == new Sort<Show>.Default() ? new Sort<Show>.By(x => x.AirDate) : sortBy,
fields,
pagination
);
if ( if (
!resources.Any() !resources.Any()
@ -249,19 +244,12 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Movie>? fields [FromQuery] Include<Movie>? fields
) )
{ {
ICollection<Movie> resources = await _libraryManager ICollection<Movie> resources = await _libraryManager.Movies.GetAll(
.Movies Filter.And(filter, identifier.IsContainedIn<Movie, Collection>(x => x.Collections)),
.GetAll( sortBy == new Sort<Movie>.Default() ? new Sort<Movie>.By(x => x.AirDate) : sortBy,
Filter.And( fields,
filter, pagination
identifier.IsContainedIn<Movie, Collection>(x => x.Collections) );
),
sortBy == new Sort<Movie>.Default()
? new Sort<Movie>.By(x => x.AirDate)
: sortBy,
fields,
pagination
);
if ( if (
!resources.Any() !resources.Any()

View File

@ -77,9 +77,10 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Show> fields [FromQuery] Include<Show> fields
) )
{ {
return await _libraryManager return await _libraryManager.Shows.Get(
.Shows identifier.IsContainedIn<Show, Episode>(x => x.Episodes!),
.Get(identifier.IsContainedIn<Show, Episode>(x => x.Episodes!), fields); fields
);
} }
/// <summary> /// <summary>
@ -103,9 +104,10 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Season> fields [FromQuery] Include<Season> fields
) )
{ {
Season? ret = await _libraryManager Season? ret = await _libraryManager.Seasons.GetOrDefault(
.Seasons identifier.IsContainedIn<Season, Episode>(x => x.Episodes!),
.GetOrDefault(identifier.IsContainedIn<Season, Episode>(x => x.Episodes!), fields); fields
);
if (ret != null) if (ret != null)
return ret; return ret;
Episode? episode = await identifier.Match( Episode? episode = await identifier.Match(
@ -170,9 +172,13 @@ namespace Kyoo.Core.Api
id => Task.FromResult(id), id => Task.FromResult(id),
async slug => (await _libraryManager.Episodes.Get(slug)).Id async slug => (await _libraryManager.Episodes.Get(slug)).Id
); );
return await _libraryManager return await _libraryManager.WatchStatus.SetEpisodeStatus(
.WatchStatus id,
.SetEpisodeStatus(id, User.GetIdOrThrow(), status, watchedTime, percent); User.GetIdOrThrow(),
status,
watchedTime,
percent
);
} }
/// <summary> /// <summary>

View File

@ -113,9 +113,10 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Studio> fields [FromQuery] Include<Studio> fields
) )
{ {
return await _libraryManager return await _libraryManager.Studios.Get(
.Studios identifier.IsContainedIn<Studio, Movie>(x => x.Movies!),
.Get(identifier.IsContainedIn<Studio, Movie>(x => x.Movies!), fields); fields
);
} }
/// <summary> /// <summary>
@ -146,14 +147,12 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Collection> fields [FromQuery] Include<Collection> fields
) )
{ {
ICollection<Collection> resources = await _libraryManager ICollection<Collection> resources = await _libraryManager.Collections.GetAll(
.Collections Filter.And(filter, identifier.IsContainedIn<Collection, Movie>(x => x.Movies)),
.GetAll( sortBy,
Filter.And(filter, identifier.IsContainedIn<Collection, Movie>(x => x.Movies)), fields,
sortBy, pagination
fields, );
pagination
);
if ( if (
!resources.Any() !resources.Any()
@ -219,9 +218,13 @@ namespace Kyoo.Core.Api
id => Task.FromResult(id), id => Task.FromResult(id),
async slug => (await _libraryManager.Movies.Get(slug)).Id async slug => (await _libraryManager.Movies.Get(slug)).Id
); );
return await _libraryManager return await _libraryManager.WatchStatus.SetMovieStatus(
.WatchStatus id,
.SetMovieStatus(id, User.GetIdOrThrow(), status, watchedTime, percent); User.GetIdOrThrow(),
status,
watchedTime,
percent
);
} }
/// <summary> /// <summary>

View File

@ -86,17 +86,15 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Episode> fields [FromQuery] Include<Episode> fields
) )
{ {
ICollection<Episode> resources = await _libraryManager ICollection<Episode> resources = await _libraryManager.Episodes.GetAll(
.Episodes Filter.And(
.GetAll( filter,
Filter.And( identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season!.Slug)
filter, ),
identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season!.Slug) sortBy,
), fields,
sortBy, pagination
fields, );
pagination
);
if ( if (
!resources.Any() !resources.Any()
@ -125,9 +123,10 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Show> fields [FromQuery] Include<Show> fields
) )
{ {
Show? ret = await _libraryManager Show? ret = await _libraryManager.Shows.GetOrDefault(
.Shows identifier.IsContainedIn<Show, Season>(x => x.Seasons!),
.GetOrDefault(identifier.IsContainedIn<Show, Season>(x => x.Seasons!), fields); fields
);
if (ret == null) if (ret == null)
return NotFound(); return NotFound();
return ret; return ret;

View File

@ -88,17 +88,12 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Season> fields [FromQuery] Include<Season> fields
) )
{ {
ICollection<Season> resources = await _libraryManager ICollection<Season> resources = await _libraryManager.Seasons.GetAll(
.Seasons Filter.And(filter, identifier.Matcher<Season>(x => x.ShowId, x => x.Show!.Slug)),
.GetAll( sortBy,
Filter.And( fields,
filter, pagination
identifier.Matcher<Season>(x => x.ShowId, x => x.Show!.Slug) );
),
sortBy,
fields,
pagination
);
if ( if (
!resources.Any() !resources.Any()
@ -136,17 +131,12 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Episode> fields [FromQuery] Include<Episode> fields
) )
{ {
ICollection<Episode> resources = await _libraryManager ICollection<Episode> resources = await _libraryManager.Episodes.GetAll(
.Episodes Filter.And(filter, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show!.Slug)),
.GetAll( sortBy,
Filter.And( fields,
filter, pagination
identifier.Matcher<Episode>(x => x.ShowId, x => x.Show!.Slug) );
),
sortBy,
fields,
pagination
);
if ( if (
!resources.Any() !resources.Any()
@ -210,9 +200,10 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Studio> fields [FromQuery] Include<Studio> fields
) )
{ {
return await _libraryManager return await _libraryManager.Studios.Get(
.Studios identifier.IsContainedIn<Studio, Show>(x => x.Shows!),
.Get(identifier.IsContainedIn<Studio, Show>(x => x.Shows!), fields); fields
);
} }
/// <summary> /// <summary>
@ -243,14 +234,12 @@ namespace Kyoo.Core.Api
[FromQuery] Include<Collection> fields [FromQuery] Include<Collection> fields
) )
{ {
ICollection<Collection> resources = await _libraryManager ICollection<Collection> resources = await _libraryManager.Collections.GetAll(
.Collections Filter.And(filter, identifier.IsContainedIn<Collection, Show>(x => x.Shows!)),
.GetAll( sortBy,
Filter.And(filter, identifier.IsContainedIn<Collection, Show>(x => x.Shows!)), fields,
sortBy, pagination
fields, );
pagination
);
if ( if (
!resources.Any() !resources.Any()

View File

@ -35,14 +35,13 @@ namespace Kyoo.Core.Api
public class ProxyApi : Controller public class ProxyApi : Controller
{ {
private readonly HttpProxyOptions _proxyOptions = HttpProxyOptionsBuilder private readonly HttpProxyOptions _proxyOptions = HttpProxyOptionsBuilder
.Instance .Instance.WithHandleFailure(
.WithHandleFailure(
async (context, exception) => async (context, exception) =>
{ {
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context await context.Response.WriteAsJsonAsync(
.Response new RequestError("Service unavailable")
.WriteAsJsonAsync(new RequestError("Service unavailable")); );
} }
) )
.Build(); .Build();

View File

@ -150,25 +150,19 @@ namespace Kyoo.Host
.ConfigureAppConfiguration(x => _SetupConfig(x, args)) .ConfigureAppConfiguration(x => _SetupConfig(x, args))
.UseSerilog((host, services, builder) => _ConfigureLogging(builder)) .UseSerilog((host, services, builder) => _ConfigureLogging(builder))
.ConfigureServices(x => x.AddRouting()) .ConfigureServices(x => x.AddRouting())
.ConfigureWebHost( .ConfigureWebHost(x =>
x => x.UseKestrel(options =>
x.UseKestrel(options => {
{ options.AddServerHeader = false;
options.AddServerHeader = false; })
}) .UseIIS()
.UseIIS() .UseIISIntegration()
.UseIISIntegration() .UseUrls(
.UseUrls( Environment.GetEnvironmentVariable("KYOO_BIND_URL") ?? "http://*:5000"
Environment.GetEnvironmentVariable("KYOO_BIND_URL") )
?? "http://*:5000" .UseStartup(host =>
) PluginsStartup.FromWebHost(host, new LoggerFactory().AddSerilog())
.UseStartup( )
host =>
PluginsStartup.FromWebHost(
host,
new LoggerFactory().AddSerilog()
)
)
); );
} }
@ -196,20 +190,13 @@ namespace Kyoo.Host
"[{@t:HH:mm:ss} {@l:u3} {Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), 25} " "[{@t:HH:mm:ss} {@l:u3} {Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), 25} "
+ "({@i:D10})] {@m}{#if not EndsWith(@m, '\n')}\n{#end}{@x}"; + "({@i:D10})] {@m}{#if not EndsWith(@m, '\n')}\n{#end}{@x}";
builder builder
.MinimumLevel .MinimumLevel.Warning()
.Warning() .MinimumLevel.Override("Kyoo", LogEventLevel.Verbose)
.MinimumLevel .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Verbose)
.Override("Kyoo", LogEventLevel.Verbose) .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Fatal)
.MinimumLevel .WriteTo.Console(new ExpressionTemplate(template, theme: TemplateTheme.Code))
.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Verbose) .Enrich.WithThreadId()
.MinimumLevel .Enrich.FromLogContext();
.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Fatal)
.WriteTo
.Console(new ExpressionTemplate(template, theme: TemplateTheme.Code))
.Enrich
.WithThreadId()
.Enrich
.FromLogContext();
} }
} }
} }

View File

@ -127,8 +127,8 @@ namespace Kyoo.Host
.SelectMany(x => x.ConfigureSteps) .SelectMany(x => x.ConfigureSteps)
.OrderByDescending(x => x.Priority); .OrderByDescending(x => x.Priority);
using ILifetimeScope scope = container.BeginLifetimeScope( using ILifetimeScope scope = container.BeginLifetimeScope(x =>
x => x.RegisterInstance(app).SingleInstance().ExternallyOwned() x.RegisterInstance(app).SingleInstance().ExternallyOwned()
); );
IServiceProvider provider = scope.Resolve<IServiceProvider>(); IServiceProvider provider = scope.Resolve<IServiceProvider>();
foreach (IStartupAction step in steps) foreach (IStartupAction step in steps)

View File

@ -39,8 +39,10 @@ public class SearchManager : ISearchManager
Sort<T>.By @sortBy Sort<T>.By @sortBy
=> MeilisearchModule => MeilisearchModule
.IndexSettings[index] .IndexSettings[index]
.SortableAttributes .SortableAttributes.Contains(
.Contains(sortBy.Key, StringComparer.InvariantCultureIgnoreCase) sortBy.Key,
StringComparer.InvariantCultureIgnoreCase
)
? new[] ? new[]
{ {
$"{CamelCase.ConvertName(sortBy.Key)}:{(sortBy.Desendant ? "desc" : "asc")}" $"{CamelCase.ConvertName(sortBy.Key)}:{(sortBy.Desendant ? "desc" : "asc")}"

View File

@ -369,16 +369,13 @@ namespace Kyoo.Postgresql
modelBuilder.Entity<Season>().HasIndex(x => x.Slug).IsUnique(); modelBuilder.Entity<Season>().HasIndex(x => x.Slug).IsUnique();
modelBuilder modelBuilder
.Entity<Episode>() .Entity<Episode>()
.HasIndex( .HasIndex(x => new
x => {
new ShowID = x.ShowId,
{ x.SeasonNumber,
ShowID = x.ShowId, x.EpisodeNumber,
x.SeasonNumber, x.AbsoluteNumber
x.EpisodeNumber, })
x.AbsoluteNumber
}
)
.IsUnique(); .IsUnique();
modelBuilder.Entity<Episode>().HasIndex(x => x.Slug).IsUnique(); modelBuilder.Entity<Episode>().HasIndex(x => x.Slug).IsUnique();
modelBuilder.Entity<User>().HasIndex(x => x.Slug).IsUnique(); modelBuilder.Entity<User>().HasIndex(x => x.Slug).IsUnique();

View File

@ -41,42 +41,41 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "collections", name: "collections",
columns: table => columns: table => new
new {
{ id = table.Column<Guid>(type: "uuid", nullable: false),
id = table.Column<Guid>(type: "uuid", nullable: false), slug = table.Column<string>(
slug = table.Column<string>( type: "character varying(256)",
type: "character varying(256)", maxLength: 256,
maxLength: 256, nullable: false
nullable: false ),
), name = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: false), overview = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true), added_date = table.Column<DateTime>(
added_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: false,
nullable: false, defaultValueSql: "now() at time zone 'utc'"
defaultValueSql: "now() at time zone 'utc'" ),
), poster_source = table.Column<string>(type: "text", nullable: true),
poster_source = table.Column<string>(type: "text", nullable: true), poster_blurhash = table.Column<string>(
poster_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true), thumbnail_blurhash = table.Column<string>(
thumbnail_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), logo_source = table.Column<string>(type: "text", nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true), logo_blurhash = table.Column<string>(
logo_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), external_id = table.Column<string>(type: "json", nullable: false)
external_id = table.Column<string>(type: "json", nullable: false) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_collections", x => x.id); table.PrimaryKey("pk_collections", x => x.id);
@ -85,18 +84,17 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "studios", name: "studios",
columns: table => columns: table => new
new {
{ id = table.Column<Guid>(type: "uuid", nullable: false),
id = table.Column<Guid>(type: "uuid", nullable: false), slug = table.Column<string>(
slug = table.Column<string>( type: "character varying(256)",
type: "character varying(256)", maxLength: 256,
maxLength: 256, nullable: false
nullable: false ),
), name = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: false), external_id = table.Column<string>(type: "json", nullable: false)
external_id = table.Column<string>(type: "json", nullable: false) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_studios", x => x.id); table.PrimaryKey("pk_studios", x => x.id);
@ -105,31 +103,30 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "users", name: "users",
columns: table => columns: table => new
new {
{ id = table.Column<Guid>(type: "uuid", nullable: false),
id = table.Column<Guid>(type: "uuid", nullable: false), slug = table.Column<string>(
slug = table.Column<string>( type: "character varying(256)",
type: "character varying(256)", maxLength: 256,
maxLength: 256, nullable: false
nullable: false ),
), username = table.Column<string>(type: "text", nullable: false),
username = table.Column<string>(type: "text", nullable: false), email = table.Column<string>(type: "text", nullable: false),
email = table.Column<string>(type: "text", nullable: false), password = table.Column<string>(type: "text", nullable: false),
password = table.Column<string>(type: "text", nullable: false), permissions = table.Column<string[]>(type: "text[]", nullable: false),
permissions = table.Column<string[]>(type: "text[]", nullable: false), added_date = table.Column<DateTime>(
added_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: false,
nullable: false, defaultValueSql: "now() at time zone 'utc'"
defaultValueSql: "now() at time zone 'utc'" ),
), logo_source = table.Column<string>(type: "text", nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true), logo_blurhash = table.Column<string>(
logo_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true )
) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_users", x => x.id); table.PrimaryKey("pk_users", x => x.id);
@ -138,56 +135,55 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "movies", name: "movies",
columns: table => columns: table => new
new {
{ id = table.Column<Guid>(type: "uuid", nullable: false),
id = table.Column<Guid>(type: "uuid", nullable: false), slug = table.Column<string>(
slug = table.Column<string>( type: "character varying(256)",
type: "character varying(256)", maxLength: 256,
maxLength: 256, nullable: false
nullable: false ),
), name = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: false), tagline = table.Column<string>(type: "text", nullable: true),
tagline = table.Column<string>(type: "text", nullable: true), aliases = table.Column<string[]>(type: "text[]", nullable: false),
aliases = table.Column<string[]>(type: "text[]", nullable: false), path = table.Column<string>(type: "text", nullable: false),
path = table.Column<string>(type: "text", nullable: false), overview = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true), tags = table.Column<string[]>(type: "text[]", nullable: false),
tags = table.Column<string[]>(type: "text[]", nullable: false), genres = table.Column<Genre[]>(type: "genre[]", nullable: false),
genres = table.Column<Genre[]>(type: "genre[]", nullable: false), status = table.Column<Status>(type: "status", nullable: false),
status = table.Column<Status>(type: "status", nullable: false), rating = table.Column<int>(type: "integer", nullable: false),
rating = table.Column<int>(type: "integer", nullable: false), runtime = table.Column<int>(type: "integer", nullable: false),
runtime = table.Column<int>(type: "integer", nullable: false), air_date = table.Column<DateTime>(
air_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: true
nullable: true ),
), added_date = table.Column<DateTime>(
added_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: false,
nullable: false, defaultValueSql: "now() at time zone 'utc'"
defaultValueSql: "now() at time zone 'utc'" ),
), poster_source = table.Column<string>(type: "text", nullable: true),
poster_source = table.Column<string>(type: "text", nullable: true), poster_blurhash = table.Column<string>(
poster_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true), thumbnail_blurhash = table.Column<string>(
thumbnail_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), logo_source = table.Column<string>(type: "text", nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true), logo_blurhash = table.Column<string>(
logo_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), trailer = table.Column<string>(type: "text", nullable: true),
trailer = table.Column<string>(type: "text", nullable: true), external_id = table.Column<string>(type: "json", nullable: false),
external_id = table.Column<string>(type: "json", nullable: false), studio_id = table.Column<Guid>(type: "uuid", nullable: true)
studio_id = table.Column<Guid>(type: "uuid", nullable: true) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_movies", x => x.id); table.PrimaryKey("pk_movies", x => x.id);
@ -203,58 +199,57 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "shows", name: "shows",
columns: table => columns: table => new
new {
{ id = table.Column<Guid>(type: "uuid", nullable: false),
id = table.Column<Guid>(type: "uuid", nullable: false), slug = table.Column<string>(
slug = table.Column<string>( type: "character varying(256)",
type: "character varying(256)", maxLength: 256,
maxLength: 256, nullable: false
nullable: false ),
), name = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: false), tagline = table.Column<string>(type: "text", nullable: true),
tagline = table.Column<string>(type: "text", nullable: true), aliases = table.Column<List<string>>(type: "text[]", nullable: false),
aliases = table.Column<List<string>>(type: "text[]", nullable: false), overview = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true), tags = table.Column<List<string>>(type: "text[]", nullable: false),
tags = table.Column<List<string>>(type: "text[]", nullable: false), genres = table.Column<List<Genre>>(type: "genre[]", nullable: false),
genres = table.Column<List<Genre>>(type: "genre[]", nullable: false), status = table.Column<Status>(type: "status", nullable: false),
status = table.Column<Status>(type: "status", nullable: false), rating = table.Column<int>(type: "integer", nullable: false),
rating = table.Column<int>(type: "integer", nullable: false), start_air = table.Column<DateTime>(
start_air = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: true
nullable: true ),
), end_air = table.Column<DateTime>(
end_air = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: true
nullable: true ),
), added_date = table.Column<DateTime>(
added_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: false,
nullable: false, defaultValueSql: "now() at time zone 'utc'"
defaultValueSql: "now() at time zone 'utc'" ),
), poster_source = table.Column<string>(type: "text", nullable: true),
poster_source = table.Column<string>(type: "text", nullable: true), poster_blurhash = table.Column<string>(
poster_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true), thumbnail_blurhash = table.Column<string>(
thumbnail_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), logo_source = table.Column<string>(type: "text", nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true), logo_blurhash = table.Column<string>(
logo_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), trailer = table.Column<string>(type: "text", nullable: true),
trailer = table.Column<string>(type: "text", nullable: true), external_id = table.Column<string>(type: "json", nullable: false),
external_id = table.Column<string>(type: "json", nullable: false), studio_id = table.Column<Guid>(type: "uuid", nullable: true)
studio_id = table.Column<Guid>(type: "uuid", nullable: true) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_shows", x => x.id); table.PrimaryKey("pk_shows", x => x.id);
@ -270,12 +265,11 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "link_collection_movie", name: "link_collection_movie",
columns: table => columns: table => new
new {
{ collection_id = table.Column<Guid>(type: "uuid", nullable: false),
collection_id = table.Column<Guid>(type: "uuid", nullable: false), movie_id = table.Column<Guid>(type: "uuid", nullable: false)
movie_id = table.Column<Guid>(type: "uuid", nullable: false) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey( table.PrimaryKey(
@ -301,12 +295,11 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "link_collection_show", name: "link_collection_show",
columns: table => columns: table => new
new {
{ collection_id = table.Column<Guid>(type: "uuid", nullable: false),
collection_id = table.Column<Guid>(type: "uuid", nullable: false), show_id = table.Column<Guid>(type: "uuid", nullable: false)
show_id = table.Column<Guid>(type: "uuid", nullable: false) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey( table.PrimaryKey(
@ -332,52 +325,51 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "seasons", name: "seasons",
columns: table => columns: table => new
new {
{ id = table.Column<Guid>(type: "uuid", nullable: false),
id = table.Column<Guid>(type: "uuid", nullable: false), slug = table.Column<string>(
slug = table.Column<string>( type: "character varying(256)",
type: "character varying(256)", maxLength: 256,
maxLength: 256, nullable: false
nullable: false ),
), show_id = table.Column<Guid>(type: "uuid", nullable: false),
show_id = table.Column<Guid>(type: "uuid", nullable: false), season_number = table.Column<int>(type: "integer", nullable: false),
season_number = table.Column<int>(type: "integer", nullable: false), name = table.Column<string>(type: "text", nullable: true),
name = table.Column<string>(type: "text", nullable: true), overview = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true), start_date = table.Column<DateTime>(
start_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: true
nullable: true ),
), added_date = table.Column<DateTime>(
added_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: false,
nullable: false, defaultValueSql: "now() at time zone 'utc'"
defaultValueSql: "now() at time zone 'utc'" ),
), end_date = table.Column<DateTime>(
end_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: true
nullable: true ),
), poster_source = table.Column<string>(type: "text", nullable: true),
poster_source = table.Column<string>(type: "text", nullable: true), poster_blurhash = table.Column<string>(
poster_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true), thumbnail_blurhash = table.Column<string>(
thumbnail_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), logo_source = table.Column<string>(type: "text", nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true), logo_blurhash = table.Column<string>(
logo_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), external_id = table.Column<string>(type: "json", nullable: false)
external_id = table.Column<string>(type: "json", nullable: false) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_seasons", x => x.id); table.PrimaryKey("pk_seasons", x => x.id);
@ -393,53 +385,52 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "episodes", name: "episodes",
columns: table => columns: table => new
new {
{ id = table.Column<Guid>(type: "uuid", nullable: false),
id = table.Column<Guid>(type: "uuid", nullable: false), slug = table.Column<string>(
slug = table.Column<string>( type: "character varying(256)",
type: "character varying(256)", maxLength: 256,
maxLength: 256, nullable: false
nullable: false ),
), show_id = table.Column<Guid>(type: "uuid", nullable: false),
show_id = table.Column<Guid>(type: "uuid", nullable: false), season_id = table.Column<Guid>(type: "uuid", nullable: true),
season_id = table.Column<Guid>(type: "uuid", nullable: true), season_number = table.Column<int>(type: "integer", nullable: true),
season_number = table.Column<int>(type: "integer", nullable: true), episode_number = table.Column<int>(type: "integer", nullable: true),
episode_number = table.Column<int>(type: "integer", nullable: true), absolute_number = table.Column<int>(type: "integer", nullable: true),
absolute_number = table.Column<int>(type: "integer", nullable: true), path = table.Column<string>(type: "text", nullable: false),
path = table.Column<string>(type: "text", nullable: false), name = table.Column<string>(type: "text", nullable: true),
name = table.Column<string>(type: "text", nullable: true), overview = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true), runtime = table.Column<int>(type: "integer", nullable: false),
runtime = table.Column<int>(type: "integer", nullable: false), release_date = table.Column<DateTime>(
release_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: true
nullable: true ),
), added_date = table.Column<DateTime>(
added_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: false,
nullable: false, defaultValueSql: "now() at time zone 'utc'"
defaultValueSql: "now() at time zone 'utc'" ),
), poster_source = table.Column<string>(type: "text", nullable: true),
poster_source = table.Column<string>(type: "text", nullable: true), poster_blurhash = table.Column<string>(
poster_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true), thumbnail_blurhash = table.Column<string>(
thumbnail_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), logo_source = table.Column<string>(type: "text", nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true), logo_blurhash = table.Column<string>(
logo_blurhash = table.Column<string>( type: "character varying(32)",
type: "character varying(32)", maxLength: 32,
maxLength: 32, nullable: true
nullable: true ),
), external_id = table.Column<string>(type: "json", nullable: false)
external_id = table.Column<string>(type: "json", nullable: false) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_episodes", x => x.id); table.PrimaryKey("pk_episodes", x => x.id);

View File

@ -46,24 +46,23 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "episode_watch_status", name: "episode_watch_status",
columns: table => columns: table => new
new {
{ user_id = table.Column<Guid>(type: "uuid", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false), episode_id = table.Column<Guid>(type: "uuid", nullable: false),
episode_id = table.Column<Guid>(type: "uuid", nullable: false), added_date = table.Column<DateTime>(
added_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: false,
nullable: false, defaultValueSql: "now() at time zone 'utc'"
defaultValueSql: "now() at time zone 'utc'" ),
), played_date = table.Column<DateTime>(
played_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: true
nullable: true ),
), status = table.Column<WatchStatus>(type: "watch_status", nullable: false),
status = table.Column<WatchStatus>(type: "watch_status", nullable: false), watched_time = table.Column<int>(type: "integer", nullable: true),
watched_time = table.Column<int>(type: "integer", nullable: true), watched_percent = table.Column<int>(type: "integer", nullable: true)
watched_percent = table.Column<int>(type: "integer", nullable: true) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey( table.PrimaryKey(
@ -89,24 +88,23 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "movie_watch_status", name: "movie_watch_status",
columns: table => columns: table => new
new {
{ user_id = table.Column<Guid>(type: "uuid", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false), movie_id = table.Column<Guid>(type: "uuid", nullable: false),
movie_id = table.Column<Guid>(type: "uuid", nullable: false), added_date = table.Column<DateTime>(
added_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: false,
nullable: false, defaultValueSql: "now() at time zone 'utc'"
defaultValueSql: "now() at time zone 'utc'" ),
), played_date = table.Column<DateTime>(
played_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: true
nullable: true ),
), status = table.Column<WatchStatus>(type: "watch_status", nullable: false),
status = table.Column<WatchStatus>(type: "watch_status", nullable: false), watched_time = table.Column<int>(type: "integer", nullable: true),
watched_time = table.Column<int>(type: "integer", nullable: true), watched_percent = table.Column<int>(type: "integer", nullable: true)
watched_percent = table.Column<int>(type: "integer", nullable: true) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_movie_watch_status", x => new { x.user_id, x.movie_id }); table.PrimaryKey("pk_movie_watch_status", x => new { x.user_id, x.movie_id });
@ -129,26 +127,25 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "show_watch_status", name: "show_watch_status",
columns: table => columns: table => new
new {
{ user_id = table.Column<Guid>(type: "uuid", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false), show_id = table.Column<Guid>(type: "uuid", nullable: false),
show_id = table.Column<Guid>(type: "uuid", nullable: false), added_date = table.Column<DateTime>(
added_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: false,
nullable: false, defaultValueSql: "now() at time zone 'utc'"
defaultValueSql: "now() at time zone 'utc'" ),
), played_date = table.Column<DateTime>(
played_date = table.Column<DateTime>( type: "timestamp with time zone",
type: "timestamp with time zone", nullable: true
nullable: true ),
), status = table.Column<WatchStatus>(type: "watch_status", nullable: false),
status = table.Column<WatchStatus>(type: "watch_status", nullable: false), unseen_episodes_count = table.Column<int>(type: "integer", nullable: false),
unseen_episodes_count = table.Column<int>(type: "integer", nullable: false), next_episode_id = table.Column<Guid>(type: "uuid", nullable: true),
next_episode_id = table.Column<Guid>(type: "uuid", nullable: true), watched_time = table.Column<int>(type: "integer", nullable: true),
watched_time = table.Column<int>(type: "integer", nullable: true), watched_percent = table.Column<int>(type: "integer", nullable: true)
watched_percent = table.Column<int>(type: "integer", nullable: true) },
},
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_show_watch_status", x => new { x.user_id, x.show_id }); table.PrimaryKey("pk_show_watch_status", x => new { x.user_id, x.show_id });

View File

@ -99,17 +99,14 @@ namespace Kyoo.Postgresql
modelBuilder modelBuilder
.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5))!) .HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5))!)
.HasTranslation( .HasTranslation(args => new SqlFunctionExpression(
args => "md5",
new SqlFunctionExpression( args,
"md5", nullable: true,
args, argumentsPropagateNullability: new[] { false },
nullable: true, type: args[0].Type,
argumentsPropagateNullability: new[] { false }, typeMapping: args[0].TypeMapping
type: args[0].Type, ));
typeMapping: args[0].TypeMapping
)
);
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
} }

View File

@ -39,8 +39,7 @@ namespace Kyoo.Swagger
{ {
// We can't reorder items by assigning the sorted value to the Paths variable since it has no setter. // We can't reorder items by assigning the sorted value to the Paths variable since it has no setter.
List<KeyValuePair<string, OpenApiPathItem>> sorted = postProcess List<KeyValuePair<string, OpenApiPathItem>> sorted = postProcess
.Paths .Paths.OrderBy(x => x.Key)
.OrderBy(x => x.Key)
.ToList(); .ToList();
postProcess.Paths.Clear(); postProcess.Paths.Clear();
foreach ((string key, OpenApiPathItem value) in sorted) foreach ((string key, OpenApiPathItem value) in sorted)

View File

@ -21,10 +21,10 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Swagger.Models; using Kyoo.Swagger.Models;
using Namotion.Reflection;
using NSwag; using NSwag;
using NSwag.Generation.AspNetCore; using NSwag.Generation.AspNetCore;
using NSwag.Generation.Processors.Contexts; using NSwag.Generation.Processors.Contexts;
using Namotion.Reflection;
namespace Kyoo.Swagger namespace Kyoo.Swagger
{ {
@ -42,30 +42,25 @@ namespace Kyoo.Swagger
/// <returns>This always return <c>true</c> since it should not remove operations.</returns> /// <returns>This always return <c>true</c> since it should not remove operations.</returns>
public static bool OperationFilter(OperationProcessorContext context) public static bool OperationFilter(OperationProcessorContext context)
{ {
ApiDefinitionAttribute def = context ApiDefinitionAttribute def =
.ControllerType context.ControllerType.GetCustomAttribute<ApiDefinitionAttribute>();
.GetCustomAttribute<ApiDefinitionAttribute>();
string name = def?.Name ?? context.ControllerType.Name; string name = def?.Name ?? context.ControllerType.Name;
ApiDefinitionAttribute methodOverride = context ApiDefinitionAttribute methodOverride =
.MethodInfo context.MethodInfo.GetCustomAttribute<ApiDefinitionAttribute>();
.GetCustomAttribute<ApiDefinitionAttribute>();
if (methodOverride != null) if (methodOverride != null)
name = methodOverride.Name; name = methodOverride.Name;
context.OperationDescription.Operation.Tags.Add(name); context.OperationDescription.Operation.Tags.Add(name);
if (context.Document.Tags.All(x => x.Name != name)) if (context.Document.Tags.All(x => x.Name != name))
{ {
context context.Document.Tags.Add(
.Document new OpenApiTag
.Tags {
.Add( Name = name,
new OpenApiTag Description = context.ControllerType.GetXmlDocsSummary()
{ }
Name = name, );
Description = context.ControllerType.GetXmlDocsSummary()
}
);
} }
if (def?.Group == null) if (def?.Group == null)
@ -106,8 +101,7 @@ namespace Kyoo.Swagger
{ {
List<TagGroups> tagGroups = (List<TagGroups>)postProcess.ExtensionData["x-tagGroups"]; List<TagGroups> tagGroups = (List<TagGroups>)postProcess.ExtensionData["x-tagGroups"];
List<string> tagsWithoutGroup = postProcess List<string> tagsWithoutGroup = postProcess
.Tags .Tags.Select(x => x.Name)
.Select(x => x.Name)
.Where(x => tagGroups.SelectMany(y => y.Tags).All(y => y != x)) .Where(x => tagGroups.SelectMany(y => y.Tags).All(y => y != x))
.ToList(); .ToList();
if (tagsWithoutGroup.Any()) if (tagsWithoutGroup.Any())

View File

@ -49,8 +49,7 @@ namespace Kyoo.Swagger
foreach (ActionModel action in context.Result.Controllers.SelectMany(x => x.Actions)) foreach (ActionModel action in context.Result.Controllers.SelectMany(x => x.Actions))
{ {
IEnumerable<ProducesResponseTypeAttribute> responses = action IEnumerable<ProducesResponseTypeAttribute> responses = action
.Filters .Filters.OfType<ProducesResponseTypeAttribute>()
.OfType<ProducesResponseTypeAttribute>()
.Where(x => x.Type == typeof(ActionResult<>)); .Where(x => x.Type == typeof(ActionResult<>));
foreach (ProducesResponseTypeAttribute response in responses) foreach (ProducesResponseTypeAttribute response in responses)
{ {

View File

@ -17,8 +17,8 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic; using System.Collections.Generic;
using NSwag;
using Newtonsoft.Json; using Newtonsoft.Json;
using NSwag;
namespace Kyoo.Swagger.Models namespace Kyoo.Swagger.Models
{ {

View File

@ -39,8 +39,7 @@ namespace Kyoo.Swagger
context.OperationDescription.Operation.Security ??= context.OperationDescription.Operation.Security ??=
new List<OpenApiSecurityRequirement>(); new List<OpenApiSecurityRequirement>();
OpenApiSecurityRequirement perms = context OpenApiSecurityRequirement perms = context
.MethodInfo .MethodInfo.GetCustomAttributes<UserOnlyAttribute>()
.GetCustomAttributes<UserOnlyAttribute>()
.Aggregate( .Aggregate(
new OpenApiSecurityRequirement(), new OpenApiSecurityRequirement(),
(agg, _) => (agg, _) =>
@ -51,8 +50,7 @@ namespace Kyoo.Swagger
); );
perms = context perms = context
.MethodInfo .MethodInfo.GetCustomAttributes<PermissionAttribute>()
.GetCustomAttributes<PermissionAttribute>()
.Aggregate( .Aggregate(
perms, perms,
(agg, cur) => (agg, cur) =>
@ -64,14 +62,12 @@ namespace Kyoo.Swagger
} }
); );
PartialPermissionAttribute controller = context PartialPermissionAttribute controller =
.ControllerType context.ControllerType.GetCustomAttribute<PartialPermissionAttribute>();
.GetCustomAttribute<PartialPermissionAttribute>();
if (controller != null) if (controller != null)
{ {
perms = context perms = context
.MethodInfo .MethodInfo.GetCustomAttributes<PartialPermissionAttribute>()
.GetCustomAttributes<PartialPermissionAttribute>()
.Aggregate( .Aggregate(
perms, perms,
(agg, cur) => (agg, cur) =>

View File

@ -82,20 +82,16 @@ namespace Kyoo.Swagger
!= AlternativeRoute; != AlternativeRoute;
return true; return true;
}); });
document document.SchemaGenerator.Settings.TypeMappers.Add(
.SchemaGenerator new PrimitiveTypeMapper(
.Settings typeof(Identifier),
.TypeMappers x =>
.Add( {
new PrimitiveTypeMapper( x.IsNullableRaw = false;
typeof(Identifier), x.Type = JsonObjectType.String | JsonObjectType.Integer;
x => }
{ )
x.IsNullableRaw = false; );
x.Type = JsonObjectType.String | JsonObjectType.Integer;
}
)
);
document.AddSecurity( document.AddSecurity(
nameof(Kyoo), nameof(Kyoo),

View File

@ -54,17 +54,14 @@ namespace Kyoo.Tests.Database
{ {
Episode episode = await _repository.Get(1.AsGuid()); Episode episode = await _repository.Get(1.AsGuid());
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
await Repositories await Repositories.LibraryManager.Shows.Patch(
.LibraryManager episode.ShowId,
.Shows (x) =>
.Patch( {
episode.ShowId, x.Slug = "new-slug";
(x) => return x;
{ }
x.Slug = "new-slug"; );
return x;
}
);
episode = await _repository.Get(1.AsGuid()); episode = await _repository.Get(1.AsGuid());
Assert.Equal("new-slug-s1e1", episode.Slug); Assert.Equal("new-slug-s1e1", episode.Slug);
} }
@ -92,17 +89,14 @@ namespace Kyoo.Tests.Database
{ {
Episode episode = await _repository.Get(1.AsGuid()); Episode episode = await _repository.Get(1.AsGuid());
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
episode = await Repositories episode = await Repositories.LibraryManager.Episodes.Patch(
.LibraryManager episode.Id,
.Episodes (x) =>
.Patch( {
episode.Id, x.EpisodeNumber = 2;
(x) => return x;
{ }
x.EpisodeNumber = 2; );
return x;
}
);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
episode = await _repository.Get(1.AsGuid()); episode = await _repository.Get(1.AsGuid());
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
@ -143,17 +137,14 @@ namespace Kyoo.Tests.Database
public async Task SlugEditAbsoluteTest() public async Task SlugEditAbsoluteTest()
{ {
Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode()); Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
await Repositories await Repositories.LibraryManager.Shows.Patch(
.LibraryManager episode.ShowId,
.Shows (x) =>
.Patch( {
episode.ShowId, x.Slug = "new-slug";
(x) => return x;
{ }
x.Slug = "new-slug"; );
return x;
}
);
episode = await _repository.Get(2.AsGuid()); episode = await _repository.Get(2.AsGuid());
Assert.Equal($"new-slug-3", episode.Slug); Assert.Equal($"new-slug-3", episode.Slug);
} }

View File

@ -53,17 +53,14 @@ namespace Kyoo.Tests.Database
{ {
Season season = await _repository.Get(1.AsGuid()); Season season = await _repository.Get(1.AsGuid());
Assert.Equal("anohana-s1", season.Slug); Assert.Equal("anohana-s1", season.Slug);
await Repositories await Repositories.LibraryManager.Shows.Patch(
.LibraryManager season.ShowId,
.Shows (x) =>
.Patch( {
season.ShowId, x.Slug = "new-slug";
(x) => return x;
{ }
x.Slug = "new-slug"; );
return x;
}
);
season = await _repository.Get(1.AsGuid()); season = await _repository.Get(1.AsGuid());
Assert.Equal("new-slug-s1", season.Slug); Assert.Equal("new-slug-s1", season.Slug);
} }

View File

@ -10,6 +10,7 @@
combinePackages [ combinePackages [
sdk_7_0 sdk_7_0
aspnetcore_7_0 aspnetcore_7_0
runtime_6_0
]; ];
in in
pkgs.mkShell { pkgs.mkShell {