diff --git a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs index df4e0205..d553ed61 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs @@ -54,12 +54,11 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query) { - return (await Sort( + return await Sort( _database.Collections - .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) - .Take(20) - ).ToListAsync()) - .Select(SetBackingImageSelf).ToList(); + .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) + .Take(20) + ).ToListAsync(); } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index 15d9904a..9ae11a87 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -77,7 +77,7 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query) { - List ret = await Sort( + return await Sort( _database.Episodes .Include(x => x.Show) .Where(x => x.EpisodeNumber != null || x.AbsoluteNumber != null) @@ -85,12 +85,6 @@ namespace Kyoo.Core.Controllers ) .Take(20) .ToListAsync(); - foreach (Episode ep in ret) - { - ep.Show!.Episodes = null; - SetBackingImage(ep); - } - return ret; } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs index c3495fcc..43829c9f 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -54,14 +54,12 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query) { - return (await Sort( + return await Sort( _database.LibraryItems .Where(_database.Like(x => x.Name, $"%{query}%")) ) .Take(20) - .ToListAsync()) - .Select(SetBackingImageSelf) - .ToList(); + .ToListAsync(); } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs index 7133bb63..2b8d2a98 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs @@ -278,28 +278,6 @@ namespace Kyoo.Core.Controllers return query; } - protected void SetBackingImage(T? obj) - { - if (obj is not IThumbnails thumbs) - return; - string type = obj is LibraryItem item - ? item.Kind.ToString().ToLowerInvariant() - : typeof(T).Name.ToLowerInvariant(); - - if (thumbs.Poster != null) - thumbs.Poster.Path = $"/{type}/{obj.Slug}/poster"; - if (thumbs.Thumbnail != null) - thumbs.Thumbnail.Path = $"/{type}/{obj.Slug}/thumbnail"; - if (thumbs.Logo != null) - thumbs.Logo.Path = $"/{type}/{obj.Slug}/logo"; - } - - protected T SetBackingImageSelf(T obj) - { - SetBackingImage(obj); - return obj; - } - /// /// Get a resource from it's ID and make the instance track it. /// @@ -309,7 +287,6 @@ namespace Kyoo.Core.Controllers protected virtual async Task GetWithTracking(int id) { T? ret = await Database.Set().AsTracking().FirstOrDefaultAsync(x => x.Id == id); - SetBackingImage(ret); if (ret == null) throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}"); return ret; @@ -346,8 +323,7 @@ namespace Kyoo.Core.Controllers public virtual Task GetOrDefault(int id, Include? include = default) { return AddIncludes(Database.Set(), include) - .FirstOrDefaultAsync(x => x.Id == id) - .Then(SetBackingImage); + .FirstOrDefaultAsync(x => x.Id == id); } /// @@ -357,12 +333,10 @@ namespace Kyoo.Core.Controllers { return AddIncludes(Database.Set(), include) .OrderBy(x => EF.Functions.Random()) - .FirstOrDefaultAsync() - .Then(SetBackingImage); + .FirstOrDefaultAsync(); } return AddIncludes(Database.Set(), include) - .FirstOrDefaultAsync(x => x.Slug == slug) - .Then(SetBackingImage); + .FirstOrDefaultAsync(x => x.Slug == slug); } /// @@ -374,21 +348,19 @@ namespace Kyoo.Core.Controllers AddIncludes(Database.Set(), include), sortBy ) - .FirstOrDefaultAsync(where) - .Then(SetBackingImage); + .FirstOrDefaultAsync(where); } /// public abstract Task> Search(string query); /// - public virtual async Task> GetAll(Expression>? where = null, + public virtual Task> GetAll(Expression>? where = null, Sort? sort = default, Pagination? limit = default, Include? include = default) { - return (await ApplyFilters(Database.Set(), where, sort, limit, include)) - .Select(SetBackingImageSelf).ToList(); + return ApplyFilters(Database.Set(), where, sort, limit, include); } /// @@ -447,7 +419,6 @@ namespace Kyoo.Core.Controllers if (thumbs.Logo != null) Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry!.State = EntityState.Added; } - SetBackingImage(obj); return obj; } @@ -499,7 +470,6 @@ namespace Kyoo.Core.Controllers await EditRelations(old, edited); await Database.SaveChangesAsync(); OnEdited?.Invoke(old); - SetBackingImage(old); return old; } finally @@ -523,7 +493,6 @@ namespace Kyoo.Core.Controllers await Database.SaveChangesAsync(); OnEdited?.Invoke(resource); - SetBackingImage(resource); return resource; } finally diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs index 4daf2b7d..c3bf697c 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs @@ -71,14 +71,12 @@ namespace Kyoo.Core.Controllers public override async Task> Search(string query) { query = $"%{query}%"; - return (await Sort( - _database.Movies + return await Sort( + _database.Movies .Where(_database.Like(x => x.Name + " " + x.Slug, query)) ) .Take(20) - .ToListAsync()) - .Select(SetBackingImageSelf) - .ToList(); + .ToListAsync(); } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs index e01890bb..25a4e2e5 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/PeopleRepository.cs @@ -65,14 +65,12 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query) { - return (await Sort( + return await Sort( _database.People .Where(_database.Like(x => x.Name, $"%{query}%")) ) .Take(20) - .ToListAsync()) - .Select(SetBackingImageSelf) - .ToList(); + .ToListAsync(); } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index 6fa81f7d..9025853a 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -71,14 +71,12 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query) { - return (await Sort( + return await Sort( _database.Seasons .Where(_database.Like(x => x.Name!, $"%{query}%")) ) .Take(20) - .ToListAsync()) - .Select(SetBackingImageSelf) - .ToList(); + .ToListAsync(); } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs index 1090b744..367174ee 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs @@ -71,15 +71,12 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query) { - query = $"%{query}%"; - return (await Sort( + return await Sort( _database.Shows - .Where(_database.Like(x => x.Name + " " + x.Slug, query)) + .Where(_database.Like(x => x.Name + " " + x.Slug, $"%{query}%")) ) .Take(20) - .ToListAsync()) - .Select(SetBackingImageSelf) - .ToList(); + .ToListAsync(); } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs index aad3a591..1153d9be 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs @@ -54,14 +54,12 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query) { - return (await Sort( + return await Sort( _database.Studios .Where(_database.Like(x => x.Name, $"%{query}%")) ) .Take(20) - .ToListAsync()) - .Select(SetBackingImageSelf) - .ToList(); + .ToListAsync(); } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs index 6f050b9e..4efaf09f 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs @@ -53,14 +53,12 @@ namespace Kyoo.Core.Controllers /// public override async Task> Search(string query) { - return (await Sort( + return await Sort( _database.Users .Where(_database.Like(x => x.Username, $"%{query}%")) ) .Take(20) - .ToListAsync()) - .Select(SetBackingImageSelf) - .ToList(); + .ToListAsync(); } /// diff --git a/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs b/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs index eeb71cc9..76aacdea 100644 --- a/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs +++ b/back/src/Kyoo.Core/Views/Helper/Serializers/JsonSerializerContract.cs @@ -59,6 +59,8 @@ namespace Kyoo.Core.Api property.ShouldSerialize = _ => { ICollection fields = (ICollection)_httpContextAccessor.HttpContext!.Items["fields"]!; + if (fields == null) + return false; return fields.Contains(member.Name); }; } diff --git a/front/packages/models/src/resources/collection.ts b/front/packages/models/src/resources/collection.ts index 548b88ca..2e7c1cc1 100644 --- a/front/packages/models/src/resources/collection.ts +++ b/front/packages/models/src/resources/collection.ts @@ -19,10 +19,10 @@ */ import { z } from "zod"; -import { ImagesP, ResourceP } from "../traits"; +import { withImages, ResourceP } from "../traits"; -export const CollectionP = ResourceP.merge(ImagesP) - .extend({ +export const CollectionP = withImages( + ResourceP.extend({ /** * The title of this collection. */ @@ -31,11 +31,12 @@ export const CollectionP = ResourceP.merge(ImagesP) * The summary of this show. */ overview: z.string().nullable(), - }) - .transform((x) => ({ - ...x, - href: `/collection/${x.slug}`, - })); + }), + "collections", +).transform((x) => ({ + ...x, + href: `/collection/${x.slug}`, +})); /** * A class representing collections of show or movies. diff --git a/front/packages/models/src/resources/episode.ts b/front/packages/models/src/resources/episode.ts index 3bfaba11..ae9bc1b4 100644 --- a/front/packages/models/src/resources/episode.ts +++ b/front/packages/models/src/resources/episode.ts @@ -20,72 +20,77 @@ import { z } from "zod"; import { zdate } from "../utils"; -import { ImagesP, imageFn } from "../traits"; +import { withImages, imageFn } from "../traits"; import { ResourceP } from "../traits/resource"; import { ShowP } from "./show"; -const BaseEpisodeP = ResourceP.merge(ImagesP).extend({ - /** - * The season in witch this episode is in. - */ - seasonNumber: z.number().nullable(), - - /** - * The number of this episode in it's season. - */ - episodeNumber: z.number().nullable(), - - /** - * The absolute number of this episode. It's an episode number that is not reset to 1 after a new - * season. - */ - absoluteNumber: z.number().nullable(), - - /** - * The title of this episode. - */ - name: z.string().nullable(), - - /** - * The overview of this episode. - */ - overview: z.string().nullable(), - - /** - * The release date of this episode. It can be null if unknown. - */ - releaseDate: zdate().nullable(), - - /** - * The links to see a movie or an episode. - */ - links: z.object({ +const BaseEpisodeP = withImages( + ResourceP.extend({ /** - * The direct link to the unprocessed video (pristine quality). + * The season in witch this episode is in. */ - direct: z.string().transform(imageFn), + seasonNumber: z.number().nullable(), /** - * The link to an HLS master playlist containing all qualities available for this video. + * The number of this episode in it's season. */ - hls: z.string().transform(imageFn), + episodeNumber: z.number().nullable(), + + /** + * The absolute number of this episode. It's an episode number that is not reset to 1 after a new + * season. + */ + absoluteNumber: z.number().nullable(), + + /** + * The title of this episode. + */ + name: z.string().nullable(), + + /** + * The overview of this episode. + */ + overview: z.string().nullable(), + + /** + * The release date of this episode. It can be null if unknown. + */ + releaseDate: zdate().nullable(), + + /** + * The links to see a movie or an episode. + */ + links: z.object({ + /** + * The direct link to the unprocessed video (pristine quality). + */ + direct: z.string().transform(imageFn), + + /** + * The link to an HLS master playlist containing all qualities available for this video. + */ + hls: z.string().transform(imageFn), + }), }), -}); + "episodes", +); -export const EpisodeP = BaseEpisodeP.extend({ - /** - * The episode that come before this one if you follow usual watch orders. If this is the first - * episode or this is a movie, it will be null. - */ - previousEpisode: BaseEpisodeP.nullable().optional(), - /** - * The episode that come after this one if you follow usual watch orders. If this is the last - * aired episode or this is a movie, it will be null. - */ - nextEpisode: BaseEpisodeP.nullable().optional(), +export const EpisodeP = BaseEpisodeP.and( + z.object({ + /** + * The episode that come before this one if you follow usual watch orders. If this is the first + * episode, it will be null. + */ + previousEpisode: BaseEpisodeP.nullable().optional(), + /** + * The episode that come after this one if you follow usual watch orders. If this is the last + * aired episode, it will be null. + */ + nextEpisode: BaseEpisodeP.nullable().optional(), - show: ShowP.optional(), -}); + show: ShowP.optional(), + }), +); /** * A class to represent a single show's episode. diff --git a/front/packages/models/src/resources/movie.ts b/front/packages/models/src/resources/movie.ts index e3643af4..fdd31656 100644 --- a/front/packages/models/src/resources/movie.ts +++ b/front/packages/models/src/resources/movie.ts @@ -20,13 +20,13 @@ import { z } from "zod"; import { zdate } from "../utils"; -import { ImagesP, ResourceP, imageFn } from "../traits"; +import { withImages, ResourceP, imageFn } from "../traits"; import { Genre } from "./genre"; import { StudioP } from "./studio"; import { Status } from "./show"; -export const MovieP = ResourceP.merge(ImagesP) - .extend({ +export const MovieP = withImages( + ResourceP.extend({ /** * The title of this movie. */ @@ -82,7 +82,9 @@ export const MovieP = ResourceP.merge(ImagesP) */ hls: z.string().transform(imageFn), }), - }) + }), + "movies", +) .transform((x) => { if (!x.thumbnail && x.poster) { x.thumbnail = { ...x.poster }; diff --git a/front/packages/models/src/resources/person.ts b/front/packages/models/src/resources/person.ts index bcef4d61..5f3f3872 100644 --- a/front/packages/models/src/resources/person.ts +++ b/front/packages/models/src/resources/person.ts @@ -19,26 +19,29 @@ */ import { z } from "zod"; -import { ImagesP } from "../traits"; +import { withImages } from "../traits"; import { ResourceP } from "../traits/resource"; -export const PersonP = ResourceP.merge(ImagesP).extend({ - /** - * The name of this person. - */ - name: z.string(), - /** - * The type of work the person has done for the show. That can be something like "Actor", - * "Writer", "Music", "Voice Actor"... - */ - type: z.string().optional(), +export const PersonP = withImages( + ResourceP.extend({ + /** + * The name of this person. + */ + name: z.string(), + /** + * The type of work the person has done for the show. That can be something like "Actor", + * "Writer", "Music", "Voice Actor"... + */ + type: z.string().optional(), - /** - * The role the People played. This is mostly used to inform witch character was played for actor - * and voice actors. - */ - role: z.string().optional(), -}); + /** + * The role the People played. This is mostly used to inform witch character was played for actor + * and voice actors. + */ + role: z.string().optional(), + }), + "people", +); /** * A studio that make shows. diff --git a/front/packages/models/src/resources/season.ts b/front/packages/models/src/resources/season.ts index c4584428..99edb3b1 100644 --- a/front/packages/models/src/resources/season.ts +++ b/front/packages/models/src/resources/season.ts @@ -20,35 +20,38 @@ import { z } from "zod"; import { zdate } from "../utils"; -import { ImagesP } from "../traits"; +import { withImages } from "../traits"; import { ResourceP } from "../traits/resource"; -export const SeasonP = ResourceP.merge(ImagesP).extend({ - /** - * The name of this season. - */ - name: z.string(), - /** - * The number of this season. This can be set to 0 to indicate specials. - */ - seasonNumber: z.number(), - /** - * A quick overview of this season. - */ - overview: z.string().nullable(), - /** - * The starting air date of this season. - */ - startDate: zdate().nullable(), - /** - * The ending date of this season. - */ - endDate: zdate().nullable(), - /** - * The number of episodes available on kyoo of this season. - */ - episodesCount: z.number(), -}); +export const SeasonP = withImages( + ResourceP.extend({ + /** + * The name of this season. + */ + name: z.string(), + /** + * The number of this season. This can be set to 0 to indicate specials. + */ + seasonNumber: z.number(), + /** + * A quick overview of this season. + */ + overview: z.string().nullable(), + /** + * The starting air date of this season. + */ + startDate: zdate().nullable(), + /** + * The ending date of this season. + */ + endDate: zdate().nullable(), + /** + * The number of episodes available on kyoo of this season. + */ + episodesCount: z.number(), + }), + "seasons", +); /** * A season of a Show. diff --git a/front/packages/models/src/resources/show.ts b/front/packages/models/src/resources/show.ts index d1dc4bcd..4c0cbe97 100644 --- a/front/packages/models/src/resources/show.ts +++ b/front/packages/models/src/resources/show.ts @@ -20,7 +20,7 @@ import { z } from "zod"; import { zdate } from "../utils"; -import { ImagesP, ResourceP } from "../traits"; +import { withImages, ResourceP } from "../traits"; import { Genre } from "./genre"; import { SeasonP } from "./season"; import { StudioP } from "./studio"; @@ -35,8 +35,7 @@ export enum Status { Planned = "Planned", } -export const ShowP = ResourceP.merge(ImagesP) - .extend({ +export const ShowP = withImages(ResourceP.extend({ /** * The title of this show. */ @@ -85,7 +84,7 @@ export const ShowP = ResourceP.merge(ImagesP) * The list of seasons of this show. */ seasons: z.array(SeasonP).optional(), - }) + }), "shows") .transform((x) => { if (!x.thumbnail && x.poster) { x.thumbnail = { ...x.poster }; diff --git a/front/packages/models/src/traits/images.ts b/front/packages/models/src/traits/images.ts index eed6b026..ef77b67c 100644 --- a/front/packages/models/src/traits/images.ts +++ b/front/packages/models/src/traits/images.ts @@ -19,7 +19,7 @@ */ import { Platform } from "react-native"; -import { z } from "zod"; +import { ZodObject, ZodRawShape, z } from "zod"; import { kyooApiUrl } from ".."; export const imageFn = (url: string) => (Platform.OS === "web" ? `/api${url}` : kyooApiUrl + url); @@ -27,12 +27,9 @@ export const imageFn = (url: string) => (Platform.OS === "web" ? `/api${url}` : export const Img = z.object({ source: z.string(), blurhash: z.string(), - low: z.string().transform(imageFn), - medium: z.string().transform(imageFn), - high: z.string().transform(imageFn), }); -export const ImagesP = z.object({ +const ImagesP = z.object({ /** * An url to the poster of this resource. If this resource does not have an image, the link will * be null. If the kyoo's instance is not capable of handling this kind of image for the specific @@ -55,7 +52,28 @@ export const ImagesP = z.object({ logo: Img.nullable(), }); +const addQualities = (x: object | null | undefined, href: string) => { + if (x === null) return null; + return { + ...x, + low: imageFn(`${href}?quality=low`), + medium: imageFn(`${href}?quality=medium`), + high: imageFn(`${href}?quality=high`), + }; +}; + +export const withImages = (parser: ZodObject, type: string) => { + return parser.merge(ImagesP).transform((x) => { + return { + ...x, + poster: addQualities(x.poster, `/${type}/${x.slug}/poster`), + thumbnail: addQualities(x.thumbnail, `/${type}/${x.slug}/thumbnail`), + logo: addQualities(x.logo, `/${type}/${x.slug}/logo`), + }; + }); +}; + /** * Base traits for items that has image resources. */ -export type KyooImage = z.infer; +export type KyooImage = z.infer & { low: string; medium: string; high: string }; diff --git a/front/packages/ui/src/details/season.tsx b/front/packages/ui/src/details/season.tsx index 5e27b482..7e0d276d 100644 --- a/front/packages/ui/src/details/season.tsx +++ b/front/packages/ui/src/details/season.tsx @@ -98,7 +98,7 @@ SeasonHeader.query = (slug: string): QueryIdentifier => map: (seasons) => seasons.reduce((acc, x) => { if (x.episodesCount == 0) return acc; - return [...acc, { ...x, range: null, href: `/show/${slug}?season=${x.seasonNumber}` }]; + return [...acc, { ...x, href: `/show/${slug}?season=${x.seasonNumber}` }]; }, [] as SeasonProcessed[]), }, });