Add images links on the front instead of the back

This commit is contained in:
Zoe Roux 2023-10-29 21:29:11 +01:00
parent c9c1ac5126
commit 1373d0ce26
19 changed files with 182 additions and 202 deletions

View File

@ -54,12 +54,11 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<Collection>> Search(string query) public override async Task<ICollection<Collection>> Search(string query)
{ {
return (await Sort( return await Sort(
_database.Collections _database.Collections
.Where(_database.Like<Collection>(x => x.Name + " " + x.Slug, $"%{query}%")) .Where(_database.Like<Collection>(x => x.Name + " " + x.Slug, $"%{query}%"))
.Take(20) .Take(20)
).ToListAsync()) ).ToListAsync();
.Select(SetBackingImageSelf).ToList();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -77,7 +77,7 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<Episode>> Search(string query) public override async Task<ICollection<Episode>> Search(string query)
{ {
List<Episode> ret = await Sort( return await Sort(
_database.Episodes _database.Episodes
.Include(x => x.Show) .Include(x => x.Show)
.Where(x => x.EpisodeNumber != null || x.AbsoluteNumber != null) .Where(x => x.EpisodeNumber != null || x.AbsoluteNumber != null)
@ -85,12 +85,6 @@ namespace Kyoo.Core.Controllers
) )
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
foreach (Episode ep in ret)
{
ep.Show!.Episodes = null;
SetBackingImage(ep);
}
return ret;
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -54,14 +54,12 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<LibraryItem>> Search(string query) public override async Task<ICollection<LibraryItem>> Search(string query)
{ {
return (await Sort( return await Sort(
_database.LibraryItems _database.LibraryItems
.Where(_database.Like<LibraryItem>(x => x.Name, $"%{query}%")) .Where(_database.Like<LibraryItem>(x => x.Name, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync()) .ToListAsync();
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -278,28 +278,6 @@ namespace Kyoo.Core.Controllers
return query; 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;
}
/// <summary> /// <summary>
/// Get a resource from it's ID and make the <see cref="Database"/> instance track it. /// Get a resource from it's ID and make the <see cref="Database"/> instance track it.
/// </summary> /// </summary>
@ -309,7 +287,6 @@ namespace Kyoo.Core.Controllers
protected virtual async Task<T> GetWithTracking(int id) protected virtual async Task<T> GetWithTracking(int id)
{ {
T? ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.Id == id); T? ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.Id == id);
SetBackingImage(ret);
if (ret == null) if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}"); throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}");
return ret; return ret;
@ -346,8 +323,7 @@ namespace Kyoo.Core.Controllers
public virtual Task<T?> GetOrDefault(int id, Include<T>? include = default) public virtual Task<T?> GetOrDefault(int id, Include<T>? include = default)
{ {
return AddIncludes(Database.Set<T>(), include) return AddIncludes(Database.Set<T>(), include)
.FirstOrDefaultAsync(x => x.Id == id) .FirstOrDefaultAsync(x => x.Id == id);
.Then(SetBackingImage);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -357,12 +333,10 @@ namespace Kyoo.Core.Controllers
{ {
return AddIncludes(Database.Set<T>(), include) return AddIncludes(Database.Set<T>(), include)
.OrderBy(x => EF.Functions.Random()) .OrderBy(x => EF.Functions.Random())
.FirstOrDefaultAsync() .FirstOrDefaultAsync();
.Then(SetBackingImage);
} }
return AddIncludes(Database.Set<T>(), include) return AddIncludes(Database.Set<T>(), include)
.FirstOrDefaultAsync(x => x.Slug == slug) .FirstOrDefaultAsync(x => x.Slug == slug);
.Then(SetBackingImage);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -374,21 +348,19 @@ namespace Kyoo.Core.Controllers
AddIncludes(Database.Set<T>(), include), AddIncludes(Database.Set<T>(), include),
sortBy sortBy
) )
.FirstOrDefaultAsync(where) .FirstOrDefaultAsync(where);
.Then(SetBackingImage);
} }
/// <inheritdoc/> /// <inheritdoc/>
public abstract Task<ICollection<T>> Search(string query); public abstract Task<ICollection<T>> Search(string query);
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<ICollection<T>> GetAll(Expression<Func<T, bool>>? where = null, public virtual Task<ICollection<T>> GetAll(Expression<Func<T, bool>>? where = null,
Sort<T>? sort = default, Sort<T>? sort = default,
Pagination? limit = default, Pagination? limit = default,
Include<T>? include = default) Include<T>? include = default)
{ {
return (await ApplyFilters(Database.Set<T>(), where, sort, limit, include)) return ApplyFilters(Database.Set<T>(), where, sort, limit, include);
.Select(SetBackingImageSelf).ToList();
} }
/// <summary> /// <summary>
@ -447,7 +419,6 @@ namespace Kyoo.Core.Controllers
if (thumbs.Logo != null) if (thumbs.Logo != null)
Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry!.State = EntityState.Added; Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry!.State = EntityState.Added;
} }
SetBackingImage(obj);
return obj; return obj;
} }
@ -499,7 +470,6 @@ namespace Kyoo.Core.Controllers
await EditRelations(old, edited); await EditRelations(old, edited);
await Database.SaveChangesAsync(); await Database.SaveChangesAsync();
OnEdited?.Invoke(old); OnEdited?.Invoke(old);
SetBackingImage(old);
return old; return old;
} }
finally finally
@ -523,7 +493,6 @@ namespace Kyoo.Core.Controllers
await Database.SaveChangesAsync(); await Database.SaveChangesAsync();
OnEdited?.Invoke(resource); OnEdited?.Invoke(resource);
SetBackingImage(resource);
return resource; return resource;
} }
finally finally

View File

@ -71,14 +71,12 @@ namespace Kyoo.Core.Controllers
public override async Task<ICollection<Movie>> Search(string query) public override async Task<ICollection<Movie>> Search(string query)
{ {
query = $"%{query}%"; query = $"%{query}%";
return (await Sort( return await Sort(
_database.Movies _database.Movies
.Where(_database.Like<Movie>(x => x.Name + " " + x.Slug, query)) .Where(_database.Like<Movie>(x => x.Name + " " + x.Slug, query))
) )
.Take(20) .Take(20)
.ToListAsync()) .ToListAsync();
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -65,14 +65,12 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<People>> Search(string query) public override async Task<ICollection<People>> Search(string query)
{ {
return (await Sort( return await Sort(
_database.People _database.People
.Where(_database.Like<People>(x => x.Name, $"%{query}%")) .Where(_database.Like<People>(x => x.Name, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync()) .ToListAsync();
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -71,14 +71,12 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<ICollection<Season>> Search(string query) public override async Task<ICollection<Season>> Search(string query)
{ {
return (await Sort( return await Sort(
_database.Seasons _database.Seasons
.Where(_database.Like<Season>(x => x.Name!, $"%{query}%")) .Where(_database.Like<Season>(x => x.Name!, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync()) .ToListAsync();
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@ -71,15 +71,12 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<Show>> Search(string query) public override async Task<ICollection<Show>> Search(string query)
{ {
query = $"%{query}%"; return await Sort(
return (await Sort(
_database.Shows _database.Shows
.Where(_database.Like<Show>(x => x.Name + " " + x.Slug, query)) .Where(_database.Like<Show>(x => x.Name + " " + x.Slug, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync()) .ToListAsync();
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -54,14 +54,12 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<Studio>> Search(string query) public override async Task<ICollection<Studio>> Search(string query)
{ {
return (await Sort( return await Sort(
_database.Studios _database.Studios
.Where(_database.Like<Studio>(x => x.Name, $"%{query}%")) .Where(_database.Like<Studio>(x => x.Name, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync()) .ToListAsync();
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -53,14 +53,12 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<User>> Search(string query) public override async Task<ICollection<User>> Search(string query)
{ {
return (await Sort( return await Sort(
_database.Users _database.Users
.Where(_database.Like<User>(x => x.Username, $"%{query}%")) .Where(_database.Like<User>(x => x.Username, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync()) .ToListAsync();
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -59,6 +59,8 @@ namespace Kyoo.Core.Api
property.ShouldSerialize = _ => property.ShouldSerialize = _ =>
{ {
ICollection<string> fields = (ICollection<string>)_httpContextAccessor.HttpContext!.Items["fields"]!; ICollection<string> fields = (ICollection<string>)_httpContextAccessor.HttpContext!.Items["fields"]!;
if (fields == null)
return false;
return fields.Contains(member.Name); return fields.Contains(member.Name);
}; };
} }

View File

@ -19,10 +19,10 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { ImagesP, ResourceP } from "../traits"; import { withImages, ResourceP } from "../traits";
export const CollectionP = ResourceP.merge(ImagesP) export const CollectionP = withImages(
.extend({ ResourceP.extend({
/** /**
* The title of this collection. * The title of this collection.
*/ */
@ -31,11 +31,12 @@ export const CollectionP = ResourceP.merge(ImagesP)
* The summary of this show. * The summary of this show.
*/ */
overview: z.string().nullable(), overview: z.string().nullable(),
}) }),
.transform((x) => ({ "collections",
...x, ).transform((x) => ({
href: `/collection/${x.slug}`, ...x,
})); href: `/collection/${x.slug}`,
}));
/** /**
* A class representing collections of show or movies. * A class representing collections of show or movies.

View File

@ -20,72 +20,77 @@
import { z } from "zod"; import { z } from "zod";
import { zdate } from "../utils"; import { zdate } from "../utils";
import { ImagesP, imageFn } from "../traits"; import { withImages, imageFn } from "../traits";
import { ResourceP } from "../traits/resource"; import { ResourceP } from "../traits/resource";
import { ShowP } from "./show"; import { ShowP } from "./show";
const BaseEpisodeP = ResourceP.merge(ImagesP).extend({ const BaseEpisodeP = withImages(
/** ResourceP.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({
/** /**
* 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({ 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 or this is a movie, it will be null. * 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(), */
/** 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. * 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(), */
nextEpisode: BaseEpisodeP.nullable().optional(),
show: ShowP.optional(), show: ShowP.optional(),
}); }),
);
/** /**
* A class to represent a single show's episode. * A class to represent a single show's episode.

View File

@ -20,13 +20,13 @@
import { z } from "zod"; import { z } from "zod";
import { zdate } from "../utils"; import { zdate } from "../utils";
import { ImagesP, ResourceP, imageFn } from "../traits"; import { withImages, ResourceP, imageFn } from "../traits";
import { Genre } from "./genre"; import { Genre } from "./genre";
import { StudioP } from "./studio"; import { StudioP } from "./studio";
import { Status } from "./show"; import { Status } from "./show";
export const MovieP = ResourceP.merge(ImagesP) export const MovieP = withImages(
.extend({ ResourceP.extend({
/** /**
* The title of this movie. * The title of this movie.
*/ */
@ -82,7 +82,9 @@ export const MovieP = ResourceP.merge(ImagesP)
*/ */
hls: z.string().transform(imageFn), hls: z.string().transform(imageFn),
}), }),
}) }),
"movies",
)
.transform((x) => { .transform((x) => {
if (!x.thumbnail && x.poster) { if (!x.thumbnail && x.poster) {
x.thumbnail = { ...x.poster }; x.thumbnail = { ...x.poster };

View File

@ -19,26 +19,29 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { ImagesP } from "../traits"; import { withImages } from "../traits";
import { ResourceP } from "../traits/resource"; import { ResourceP } from "../traits/resource";
export const PersonP = ResourceP.merge(ImagesP).extend({ export const PersonP = withImages(
/** ResourceP.extend({
* The name of this person. /**
*/ * The name of this person.
name: z.string(), */
/** name: z.string(),
* The type of work the person has done for the show. That can be something like "Actor", /**
* "Writer", "Music", "Voice Actor"... * 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(), */
type: z.string().optional(),
/** /**
* The role the People played. This is mostly used to inform witch character was played for actor * The role the People played. This is mostly used to inform witch character was played for actor
* and voice actors. * and voice actors.
*/ */
role: z.string().optional(), role: z.string().optional(),
}); }),
"people",
);
/** /**
* A studio that make shows. * A studio that make shows.

View File

@ -20,35 +20,38 @@
import { z } from "zod"; import { z } from "zod";
import { zdate } from "../utils"; import { zdate } from "../utils";
import { ImagesP } from "../traits"; import { withImages } from "../traits";
import { ResourceP } from "../traits/resource"; import { ResourceP } from "../traits/resource";
export const SeasonP = ResourceP.merge(ImagesP).extend({ export const SeasonP = withImages(
/** ResourceP.extend({
* The name of this season. /**
*/ * The name of this season.
name: z.string(), */
/** name: z.string(),
* The number of this season. This can be set to 0 to indicate specials. /**
*/ * The number of this season. This can be set to 0 to indicate specials.
seasonNumber: z.number(), */
/** seasonNumber: z.number(),
* A quick overview of this season. /**
*/ * A quick overview of this season.
overview: z.string().nullable(), */
/** overview: z.string().nullable(),
* The starting air date of this season. /**
*/ * The starting air date of this season.
startDate: zdate().nullable(), */
/** startDate: zdate().nullable(),
* The ending date of this season. /**
*/ * The ending date of this season.
endDate: zdate().nullable(), */
/** endDate: zdate().nullable(),
* The number of episodes available on kyoo of this season. /**
*/ * The number of episodes available on kyoo of this season.
episodesCount: z.number(), */
}); episodesCount: z.number(),
}),
"seasons",
);
/** /**
* A season of a Show. * A season of a Show.

View File

@ -20,7 +20,7 @@
import { z } from "zod"; import { z } from "zod";
import { zdate } from "../utils"; import { zdate } from "../utils";
import { ImagesP, ResourceP } from "../traits"; import { withImages, ResourceP } from "../traits";
import { Genre } from "./genre"; import { Genre } from "./genre";
import { SeasonP } from "./season"; import { SeasonP } from "./season";
import { StudioP } from "./studio"; import { StudioP } from "./studio";
@ -35,8 +35,7 @@ export enum Status {
Planned = "Planned", Planned = "Planned",
} }
export const ShowP = ResourceP.merge(ImagesP) export const ShowP = withImages(ResourceP.extend({
.extend({
/** /**
* The title of this show. * The title of this show.
*/ */
@ -85,7 +84,7 @@ export const ShowP = ResourceP.merge(ImagesP)
* The list of seasons of this show. * The list of seasons of this show.
*/ */
seasons: z.array(SeasonP).optional(), seasons: z.array(SeasonP).optional(),
}) }), "shows")
.transform((x) => { .transform((x) => {
if (!x.thumbnail && x.poster) { if (!x.thumbnail && x.poster) {
x.thumbnail = { ...x.poster }; x.thumbnail = { ...x.poster };

View File

@ -19,7 +19,7 @@
*/ */
import { Platform } from "react-native"; import { Platform } from "react-native";
import { z } from "zod"; import { ZodObject, ZodRawShape, z } from "zod";
import { kyooApiUrl } from ".."; import { kyooApiUrl } from "..";
export const imageFn = (url: string) => (Platform.OS === "web" ? `/api${url}` : kyooApiUrl + url); 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({ export const Img = z.object({
source: z.string(), source: z.string(),
blurhash: 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 * 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 * 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(), 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 = <T extends ZodRawShape>(parser: ZodObject<T>, 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. * Base traits for items that has image resources.
*/ */
export type KyooImage = z.infer<typeof Img>; export type KyooImage = z.infer<typeof Img> & { low: string; medium: string; high: string };

View File

@ -98,7 +98,7 @@ SeasonHeader.query = (slug: string): QueryIdentifier<Season, SeasonProcessed> =>
map: (seasons) => map: (seasons) =>
seasons.reduce((acc, x) => { seasons.reduce((acc, x) => {
if (x.episodesCount == 0) return acc; 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[]), }, [] as SeasonProcessed[]),
}, },
}); });