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 items) + { + if (items[0] is Episode episode && episode.Id != 0) + return episode; + if (items[1] is Movie movie && movie.Id != 0) + { + movie.Id = -movie.Id; + return movie; + } + throw new InvalidDataException(); + } + + public NewsRepository(DbConnection database) + : base(database) { } - - /// - public override Task> Search(string query, Include? include = default) - => throw new InvalidOperationException(); - - /// - public override Task Create(News obj) - => throw new InvalidOperationException(); - - /// - public override Task CreateIfNotExists(News obj) - => throw new InvalidOperationException(); - - /// - public override Task Edit(News edited) - => throw new InvalidOperationException(); - - /// - public override Task Patch(int id, Func> patch) - => throw new InvalidOperationException(); - - /// - public override Task Delete(int id) - => throw new InvalidOperationException(); - - /// - public override Task Delete(string slug) - => throw new InvalidOperationException(); - - /// - public override Task Delete(News obj) - => throw new InvalidOperationException(); } } diff --git a/back/src/Kyoo.Core/Views/Resources/NewsApi.cs b/back/src/Kyoo.Core/Views/Resources/NewsApi.cs index a16b9c23..ae4e53f6 100644 --- a/back/src/Kyoo.Core/Views/Resources/NewsApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/NewsApi.cs @@ -33,9 +33,9 @@ namespace Kyoo.Core.Api [ApiController] [PartialPermission("LibraryItem")] [ApiDefinition("News", Group = ResourcesGroup)] - public class NewsApi : CrudThumbsApi + public class NewsApi : CrudThumbsApi { - public NewsApi(IRepository news, IThumbnailsManager thumbs) + public NewsApi(IRepository news, IThumbnailsManager thumbs) : base(news, thumbs) { } } diff --git a/back/src/Kyoo.Postgresql/DatabaseContext.cs b/back/src/Kyoo.Postgresql/DatabaseContext.cs index 547d4b3c..d693a15b 100644 --- a/back/src/Kyoo.Postgresql/DatabaseContext.cs +++ b/back/src/Kyoo.Postgresql/DatabaseContext.cs @@ -92,14 +92,6 @@ namespace Kyoo.Postgresql /// public DbSet PeopleRoles { get; set; } - /// - /// The list of new items (episodes and movies). - /// - /// - /// This set is ready only, on most database this will be a view. - /// - public DbSet News { get; set; } - /// /// Add a many to many link between two resources. /// @@ -284,7 +276,6 @@ namespace Kyoo.Postgresql .WithMany("Users") .UsingEntity(x => x.ToTable(LinkName())); - _HasMetadata(modelBuilder); _HasMetadata(modelBuilder); _HasMetadata(modelBuilder); _HasMetadata(modelBuilder); @@ -293,7 +284,6 @@ namespace Kyoo.Postgresql _HasMetadata(modelBuilder); _HasMetadata(modelBuilder); - _HasImages(modelBuilder); _HasImages(modelBuilder); _HasImages(modelBuilder); _HasImages(modelBuilder); @@ -301,7 +291,6 @@ namespace Kyoo.Postgresql _HasImages(modelBuilder); _HasImages(modelBuilder); - _HasAddedDate(modelBuilder); _HasAddedDate(modelBuilder); _HasAddedDate(modelBuilder); _HasAddedDate(modelBuilder); @@ -347,14 +336,6 @@ namespace Kyoo.Postgresql modelBuilder.Entity() .Ignore(x => x.Links); - modelBuilder.Entity() - .Ignore(x => x.Links); - - var builder = modelBuilder.Entity() - .OwnsOne(x => x.Show); - builder.OwnsOne(x => x.Poster); - builder.OwnsOne(x => x.Thumbnail); - builder.OwnsOne(x => x.Logo); } /// diff --git a/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs index 95fff21a..125330b6 100644 --- a/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs +++ b/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs @@ -370,10 +370,6 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("genre[]") .HasColumnName("genres"); - b.Property("Kind") - .HasColumnType("news_kind") - .HasColumnName("kind"); - b.Property("Name") .HasColumnType("text") .HasColumnName("name"); diff --git a/back/src/Kyoo.Postgresql/Migrations/20231031212819_rating.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20231031212819_rating.Designer.cs index 68a97c4a..0fa05ad7 100644 --- a/back/src/Kyoo.Postgresql/Migrations/20231031212819_rating.Designer.cs +++ b/back/src/Kyoo.Postgresql/Migrations/20231031212819_rating.Designer.cs @@ -390,10 +390,6 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("genre[]") .HasColumnName("genres"); - b.Property("Kind") - .HasColumnType("news_kind") - .HasColumnName("kind"); - b.Property("Name") .HasColumnType("text") .HasColumnName("name"); diff --git a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 3973198b..7d79701c 100644 --- a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -387,10 +387,6 @@ namespace Kyoo.Postgresql.Migrations .HasColumnType("genre[]") .HasColumnName("genres"); - b.Property("Kind") - .HasColumnType("news_kind") - .HasColumnName("kind"); - b.Property("Name") .HasColumnType("text") .HasColumnName("name"); diff --git a/back/src/Kyoo.Postgresql/PostgresContext.cs b/back/src/Kyoo.Postgresql/PostgresContext.cs index 4a04b8c5..f7fbeb8f 100644 --- a/back/src/Kyoo.Postgresql/PostgresContext.cs +++ b/back/src/Kyoo.Postgresql/PostgresContext.cs @@ -18,11 +18,8 @@ using System; using System.Globalization; -using System.Linq.Expressions; -using System.Reflection; using EFCore.NamingConventions.Internal; using Kyoo.Abstractions.Models; -using Kyoo.Utils; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Npgsql; @@ -50,7 +47,6 @@ namespace Kyoo.Postgresql { NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); } /// @@ -103,7 +99,6 @@ namespace Kyoo.Postgresql { modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); - modelBuilder.HasPostgresEnum(); modelBuilder.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5))!) .HasTranslation(args => diff --git a/front/packages/models/src/resources/episode.ts b/front/packages/models/src/resources/episode.ts index 282bc75d..702b371e 100644 --- a/front/packages/models/src/resources/episode.ts +++ b/front/packages/models/src/resources/episode.ts @@ -37,7 +37,10 @@ export const EpisodeP = BaseEpisodeP.and( show: ShowP.optional(), }), -); +).transform((x) => { + if (x.show && !x.thumbnail && x.show.thumbnail) x.thumbnail = x.show.thumbnail; + return x; +}); /** * A class to represent a single show's episode. diff --git a/front/packages/models/src/resources/news.ts b/front/packages/models/src/resources/news.ts index 4dc8ef45..f27d21a4 100644 --- a/front/packages/models/src/resources/news.ts +++ b/front/packages/models/src/resources/news.ts @@ -20,9 +20,7 @@ import { z } from "zod"; import { MovieP } from "./movie"; -import { BaseEpisodeP } from "./episode.base"; -import { ResourceP } from "../traits/resource"; -import { withImages } from "../traits/images"; +import { EpisodeP } from "./episode"; /** * The type of item, ether a a movie or an episode. @@ -36,29 +34,7 @@ export const NewsP = z.union([ /* * Either an episode */ - BaseEpisodeP.and( - z.object({ - kind: z.literal(NewsKind.Episode), - show: withImages( - ResourceP.extend({ - name: z.string(), - }), - "shows", - ).transform((x) => { - if (!x.thumbnail && x.poster) { - x.thumbnail = { ...x.poster }; - if (x.thumbnail) { - x.thumbnail.low = x.thumbnail.high; - x.thumbnail.medium = x.thumbnail.high; - } - } - return x; - }), - }), - ).transform((x) => { - if (!x.thumbnail && x.show.thumbnail) x.thumbnail = x.show.thumbnail; - return x; - }), + EpisodeP.and(z.object({ kind: z.literal(NewsKind.Episode) })), /* * Or a Movie */ diff --git a/front/packages/ui/src/home/news.tsx b/front/packages/ui/src/home/news.tsx index 60d424cc..ee3907b0 100644 --- a/front/packages/ui/src/home/news.tsx +++ b/front/packages/ui/src/home/news.tsx @@ -92,5 +92,6 @@ NewsList.query = (): QueryIdentifier => ({ params: { // Limit the inital numbers of items limit: 10, + fields: ["show"], }, }); diff --git a/front/packages/ui/src/home/recommanded.tsx b/front/packages/ui/src/home/recommanded.tsx index da45841e..e6c987f7 100644 --- a/front/packages/ui/src/home/recommanded.tsx +++ b/front/packages/ui/src/home/recommanded.tsx @@ -149,13 +149,15 @@ export const ItemDetails = ({ minHeight: px(50), })} > - - {(genres || [...Array(3)])?.map((x, i) => ( - - {x ?? } - - ))} - + {(isLoading || genres) && ( + + {(genres || [...Array(3)])?.map((x, i) => ( + + {x ?? } + + ))} + + )} {playHref !== null && (