diff --git a/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs b/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs
index def9a466..bb7c2b05 100644
--- a/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs
+++ b/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs
@@ -35,6 +35,8 @@ namespace Kyoo.Abstractions.Models.Attributes
public string? On { get; set; }
+ public string? Projected { get; set; }
+
///
/// Create a new .
///
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs
index dd280c87..c8cc1e65 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs
@@ -126,8 +126,20 @@ namespace Kyoo.Abstractions.Models
///
/// The number of episodes in this season.
///
- [Projectable(UseMemberBody = nameof(_EpisodesCount))]
+ [Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)]
[NotMapped]
+ [LoadableRelation(
+ // language=PostgreSQL
+ Projected = """
+ (
+ select
+ count(*)::int
+ from
+ episodes as e
+ where
+ e.season_id = id) as episode_count
+ """
+ )]
public int EpisodesCount { get; set; }
private int _EpisodesCount => Episodes!.Count;
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
index 2fdcfaa2..d6dd84e2 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
@@ -19,6 +19,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using EntityFrameworkCore.Projectables;
using Kyoo.Abstractions.Controllers;
@@ -111,6 +112,7 @@ namespace Kyoo.Abstractions.Models
public string? Trailer { get; set; }
[SerializeIgnore]
+ [Column("start_air")]
public DateTime? AirDate => StartAir;
///
diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs
index 8b7c49e9..87218ae2 100644
--- a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs
+++ b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs
@@ -25,17 +25,28 @@ using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models.Utils;
-///
-/// The aditional fields to include in the result.
-///
-/// The type related to the new fields
-public class Include
+public class Include
{
///
/// The aditional fields to include in the result.
///
- public ICollection Metadatas { get; private init; } = ArraySegment.Empty;
+ public ICollection Metadatas { get; init; } = ArraySegment.Empty;
+ public abstract record Metadata(string Name);
+
+ public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(Name);
+
+ public record CustomRelation(string Name, Type type, string Sql, string? On, Type Declaring) : Metadata(Name);
+
+ public record ProjectedRelation(string Name, string Sql) : Metadata(Name);
+}
+
+///
+/// The aditional fields to include in the result.
+///
+/// The type related to the new fields
+public class Include : Include
+{
///
/// The aditional fields names to include in the result.
///
@@ -79,16 +90,12 @@ public class Include
// }
if (attr.Sql != null)
return new CustomRelation(prop.Name, prop.PropertyType, attr.Sql, attr.On, prop.DeclaringType!);
+ if (attr.Projected != null)
+ return new ProjectedRelation(prop.Name, attr.Projected);
throw new NotImplementedException();
})
.Distinct();
}).ToArray()
};
}
-
- public abstract record Metadata(string Name);
-
- public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(Name);
-
- public record CustomRelation(string Name, Type type, string Sql, string? On, Type Declaring) : Metadata(Name);
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
index 4fa4c90b..e7d07ce2 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
@@ -32,6 +32,7 @@ using InterpolatedSql.Dapper;
using InterpolatedSql.SqlBuilders;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
+using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils;
@@ -103,7 +104,7 @@ namespace Kyoo.Core.Controllers
IEnumerable keys = config
.Where(x => key == "id" || x.Value.GetProperty(key) != null)
- .Select(x => $"{x.Key}.{key.ToSnakeCase()}");
+ .Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute()?.Name ?? key.ToSnakeCase()}");
return $"coalesce({string.Join(", ", keys)})";
}
@@ -140,12 +141,12 @@ namespace Kyoo.Core.Controllers
relation++;
switch (metadata)
{
- case Include.SingleRelation(var name, var type, var rid):
+ case Include.SingleRelation(var name, var type, var rid):
string tableName = type.GetCustomAttribute()?.Name ?? $"{type.Name.ToSnakeCase()}s";
retConfig.Add($"r{relation}", type);
join.AppendLine($"left join {tableName} as r{relation} on r{relation}.id = {_Property(rid, config)}");
break;
- case Include.CustomRelation(var name, var type, var sql, var on, var declaring):
+ case Include.CustomRelation(var name, var type, var sql, var on, var declaring):
string owner = config.First(x => x.Value == declaring).Key;
string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty;
sql = sql.Replace("\"this\"", owner);
@@ -153,6 +154,8 @@ namespace Kyoo.Core.Controllers
retConfig.Add($"r{relation}", type);
join.AppendLine($"left join{lateral} ({sql}) as r{relation} on r{relation}.{on}");
break;
+ case Include.ProjectedRelation:
+ continue;
default:
throw new NotImplementedException();
}
@@ -174,6 +177,17 @@ namespace Kyoo.Core.Controllers
return (retConfig, join.ToString(), Map);
}
+ public static string ExpendProjections(string? prefix, Include include)
+ {
+ prefix = prefix != null ? $"{prefix}." : string.Empty;
+ IEnumerable projections = include.Metadatas
+ .Select(x => x is Include.ProjectedRelation(var name, var sql) ? sql : null!)
+ .Where(x => x != null)
+ .Select(x => x.Replace("\"this\".", prefix));
+ string projStr = string.Join(string.Empty, projections.Select(x => $", {x}"));
+ return $"{prefix}*" + projStr;
+ }
+
public async Task> GetAll(
Expression>? where = null,
Sort? sort = null,
@@ -191,7 +205,7 @@ namespace Kyoo.Core.Controllers
// language=PostgreSQL
IDapperSqlCommand query = _database.SqlBuilder($"""
select
- s.*,
+ {ExpendProjections("s", include):raw},
m.*,
c.*
{string.Join(string.Empty, includeConfig.Select(x => $", {x.Key}.*")):raw}
@@ -199,12 +213,12 @@ namespace Kyoo.Core.Controllers
shows as s
full outer join (
select
- *
+ {ExpendProjections(null, include):raw}
from
movies) as m on false
- full outer join (
+ full outer join (
select
- *
+ {ExpendProjections(null, include):raw}
from
collections) as c on false
{includeJoin:raw}
diff --git a/back/src/Kyoo.Postgresql/DatabaseContext.cs b/back/src/Kyoo.Postgresql/DatabaseContext.cs
index 52a8464d..b6345ac0 100644
--- a/back/src/Kyoo.Postgresql/DatabaseContext.cs
+++ b/back/src/Kyoo.Postgresql/DatabaseContext.cs
@@ -245,7 +245,8 @@ namespace Kyoo.Postgresql
base.OnModelCreating(modelBuilder);
modelBuilder.Entity()
- .Ignore(x => x.FirstEpisode);
+ .Ignore(x => x.FirstEpisode)
+ .Ignore(x => x.AirDate);
modelBuilder.Entity()
.Ignore(x => x.PreviousEpisode)
.Ignore(x => x.NextEpisode);
diff --git a/front/packages/ui/src/details/season.tsx b/front/packages/ui/src/details/season.tsx
index 5b8b76d0..8f36aae9 100644
--- a/front/packages/ui/src/details/season.tsx
+++ b/front/packages/ui/src/details/season.tsx
@@ -94,6 +94,7 @@ SeasonHeader.query = (slug: string): QueryIdentifier =>
params: {
// Fetch all seasons at one, there won't be hundred of thems anyways.
limit: 0,
+ fields: ["episodesCount"],
},
infinite: {
value: true,