diff --git a/back/src/Kyoo.Abstractions/Models/News.cs b/back/src/Kyoo.Abstractions/Models/News.cs
index 6001aa84..9b03c681 100644
--- a/back/src/Kyoo.Abstractions/Models/News.cs
+++ b/back/src/Kyoo.Abstractions/Models/News.cs
@@ -16,180 +16,16 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see .
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
+using Kyoo.Abstractions.Controllers;
+using Kyoo.Abstractions.Models.Attributes;
-namespace Kyoo.Abstractions.Models
+namespace Kyoo.Abstractions.Models;
+
+///
+/// A show, a movie or a collection.
+///
+[OneOf(Types = new[] { typeof(Episode), typeof(Movie) })]
+public interface INews : IResource, IThumbnails, IMetadata, IAddedDate, IQuery
{
- ///
- /// The type of item, ether a show, a movie or a collection.
- ///
- public enum NewsKind
- {
- ///
- /// The is an .
- ///
- Episode,
-
- ///
- /// The is a Movie.
- ///
- Movie,
- }
-
- ///
- /// A new item
- ///
- public class News : IResource, IMetadata, IThumbnails, IAddedDate, IQuery
- {
- ///
- public int Id { get; set; }
-
- ///
- [MaxLength(256)]
- public string Slug { get; set; }
-
- ///
- /// The title of this show.
- ///
- public string? Name { get; set; }
-
- ///
- /// A catchphrase for this movie.
- ///
- public string? Tagline { get; set; }
-
- ///
- /// The list of alternative titles of this show.
- ///
- public string[] Aliases { get; set; } = Array.Empty();
-
- ///
- /// The path of the movie video file.
- ///
- public string Path { get; set; }
-
- ///
- /// The summary of this show.
- ///
- public string? Overview { get; set; }
-
- ///
- /// A list of tags that match this movie.
- ///
- public string[] Tags { get; set; } = Array.Empty();
-
- ///
- /// The list of genres (themes) this show has.
- ///
- public Genre[] Genres { get; set; } = Array.Empty();
-
- ///
- /// Is this show airing, not aired yet or finished?
- ///
- public Status? Status { get; set; }
-
- ///
- /// How well this item is rated? (from 0 to 100).
- ///
- public int? Rating { get; set; }
-
- ///
- /// How long is this movie or episode? (in minutes)
- ///
- public int Runtime { get; set; }
-
- ///
- /// The date this movie aired.
- ///
- public DateTime? AirDate { get; set; }
-
- ///
- /// The date this movie aired.
- ///
- public DateTime? ReleaseDate => AirDate;
-
- ///
- public DateTime AddedDate { get; set; }
-
- ///
- public Image? Poster { get; set; }
-
- ///
- public Image? Thumbnail { get; set; }
-
- ///
- public Image? Logo { get; set; }
-
- ///
- /// A video of a few minutes that tease the content.
- ///
- public string? Trailer { get; set; }
-
- ///
- public Dictionary ExternalId { get; set; } = new();
-
- ///
- /// The season in witch this episode is in.
- ///
- public int? SeasonNumber { get; set; }
-
- ///
- /// The number of this episode in it's season.
- ///
- public int? EpisodeNumber { get; set; }
-
- ///
- /// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
- ///
- public int? AbsoluteNumber { get; set; }
-
- ///
- /// A simple summary of informations about the show of this episode
- /// (this is specially useful since news can't have includes).
- ///
- public ShowInfo? Show { get; set; }
-
- ///
- /// Is the item a a movie or an episode?
- ///
- public NewsKind Kind { get; set; }
-
- ///
- /// Links to watch this movie.
- ///
- public VideoLinks Links => new()
- {
- Direct = $"/video/{Kind.ToString().ToLower()}/{Slug}/direct",
- Hls = $"/video/{Kind.ToString().ToLower()}/{Slug}/master.m3u8",
- };
-
- ///
- /// A simple summary of informations about the show of this episode
- /// (this is specially useful since news can't have includes).
- ///
- public class ShowInfo : IResource, IThumbnails
- {
- ///
- public int Id { get; set; }
-
- ///
- public string Slug { get; set; }
-
- ///
- /// The title of this show.
- ///
- public string Name { get; set; }
-
- ///
- public Image? Poster { get; set; }
-
- ///
- public Image? Thumbnail { get; set; }
-
- ///
- public Image? Logo { get; set; }
- }
- }
+ static Sort IQuery.DefaultSort => new Sort.By(nameof(AddedDate));
}
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
index 5bc83bd0..02d50909 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
@@ -31,7 +31,7 @@ namespace Kyoo.Abstractions.Models
///
/// A class to represent a single show's episode.
///
- public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate
+ public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, INews
{
// Use absolute numbers by default and fallback to season/episodes if it does not exists.
public static Sort DefaultSort => new Sort.Conglomerate(
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
index 73927e64..8f47fe95 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
@@ -29,7 +29,7 @@ namespace Kyoo.Abstractions.Models
///
/// A series or a movie.
///
- public class Movie : IQuery, IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem
+ public class Movie : IQuery, IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem, INews
{
public static Sort DefaultSort => new Sort.By(x => x.Name);
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
index fc9ca436..ebcde594 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
@@ -39,12 +39,12 @@ public static class DapperHelper
{
private static string _Property(string key, Dictionary config)
{
- if (config.Count == 1)
- return $"{config.First()}.{key.ToSnakeCase()}";
-
- IEnumerable keys = config
+ string[] keys = config
.Where(x => key == "id" || x.Value.GetProperty(key) != null)
- .Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute()?.Name ?? key.ToSnakeCase()}");
+ .Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute()?.Name ?? key.ToSnakeCase()}")
+ .ToArray();
+ if (keys.Length == 1)
+ return keys.First();
return $"coalesce({string.Join(", ", keys)})";
}
@@ -66,14 +66,16 @@ public static class DapperHelper
}
public static (
- Dictionary config,
+ string projection,
string join,
+ List types,
Func, T> map
) ProcessInclude(Include include, Dictionary config)
where T : class
{
int relation = 0;
- Dictionary retConfig = new();
+ List types = new();
+ StringBuilder projection = new();
StringBuilder join = new();
foreach (Include.Metadata metadata in include.Metadatas)
@@ -83,7 +85,8 @@ public static class DapperHelper
{
case Include.SingleRelation(var name, var type, var rid):
string tableName = type.GetCustomAttribute()?.Name ?? $"{type.Name.ToSnakeCase()}s";
- retConfig.Add($"r{relation}", type);
+ types.Add(type);
+ projection.AppendLine($", r{relation}.* -- {type.Name} as r{relation}");
join.Append($"\nleft 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):
@@ -91,7 +94,8 @@ public static class DapperHelper
string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty;
sql = sql.Replace("\"this\"", owner);
on = on?.Replace("\"this\"", owner);
- retConfig.Add($"r{relation}", type);
+ types.Add(type);
+ projection.AppendLine($", r{relation}.*");
join.Append($"\nleft join{lateral} ({sql}) as r{relation} on r{relation}.{on}");
break;
case Include.ProjectedRelation:
@@ -114,7 +118,7 @@ public static class DapperHelper
return item;
}
- return (retConfig, join.ToString(), Map);
+ return (projection.ToString(), join.ToString(), types, Map);
}
public static FormattableString ProcessFilter(Filter filter, Dictionary config)
@@ -187,9 +191,8 @@ public static class DapperHelper
// Include handling
include ??= new();
- var (includeConfig, includeJoin, mapIncludes) = ProcessInclude(include, config);
+ var (includeProjection, includeJoin, includeTypes, mapIncludes) = ProcessInclude(include, config);
query.AppendLiteral(includeJoin);
- string includeProjection = string.Join(string.Empty, includeConfig.Select(x => $", {x.Key}.*"));
query.Replace("/* includes */", $"{includeProjection:raw}", out bool replaced);
if (!replaced)
throw new ArgumentException("Missing '/* includes */' placeholder in top level sql select to support includes.");
@@ -210,9 +213,7 @@ public static class DapperHelper
// Build query and prepare to do the query/projections
IDapperSqlCommand cmd = query.Build();
string sql = cmd.Sql;
- List types = config.Select(x => x.Value)
- .Concat(includeConfig.Select(x => x.Value))
- .ToList();
+ List types = config.Select(x => x.Value).Concat(includeTypes).ToList();
// Expand projections on every types received.
sql = Regex.Replace(sql, @"(,?) -- (\w+)( as (\w+))?", (match) =>
@@ -296,6 +297,7 @@ public static class DapperHelper
query += ProcessFilter(filter, config);
IDapperSqlCommand cmd = query.Build();
+
// language=postgreSQL
string sql = $"select count(*) from ({cmd.Sql}) as query";
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs
index ba813113..d51c62cb 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs
@@ -18,53 +18,53 @@
using System;
using System.Collections.Generic;
-using System.Threading.Tasks;
-using Kyoo.Abstractions.Controllers;
+using System.Data.Common;
+using System.IO;
using Kyoo.Abstractions.Models;
-using Kyoo.Abstractions.Models.Utils;
-using Kyoo.Postgresql;
namespace Kyoo.Core.Controllers
{
///
/// A local repository to handle shows
///
- public class NewsRepository : LocalRepository
+ public class NewsRepository : DapperRepository
{
- public NewsRepository(DatabaseContext database, IThumbnailsManager thumbs)
- : base(database, thumbs)
+ // language=PostgreSQL
+ protected override FormattableString Sql => $"""
+ select
+ e.*, -- Episode as e
+ m.*
+ /* includes */
+ from
+ episodes as e
+ full outer join (
+ select
+ * -- Movie
+ from
+ movies
+ ) as m on false
+ """;
+
+ protected override Dictionary Config => new()
+ {
+ { "e", typeof(Episode) },
+ { "m", typeof(Movie) },
+ };
+
+ protected override INews Mapper(List