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 />
public override async Task<ICollection<Collection>> Search(string query)
{
return (await Sort(
return await Sort(
_database.Collections
.Where(_database.Like<Collection>(x => x.Name + " " + x.Slug, $"%{query}%"))
.Take(20)
).ToListAsync())
.Select(SetBackingImageSelf).ToList();
.Where(_database.Like<Collection>(x => x.Name + " " + x.Slug, $"%{query}%"))
.Take(20)
).ToListAsync();
}
/// <inheritdoc />

View File

@ -77,7 +77,7 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc />
public override async Task<ICollection<Episode>> Search(string query)
{
List<Episode> 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;
}
/// <inheritdoc />

View File

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

View File

@ -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;
}
/// <summary>
/// Get a resource from it's ID and make the <see cref="Database"/> instance track it.
/// </summary>
@ -309,7 +287,6 @@ namespace Kyoo.Core.Controllers
protected virtual async Task<T> GetWithTracking(int id)
{
T? ret = await Database.Set<T>().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<T?> GetOrDefault(int id, Include<T>? include = default)
{
return AddIncludes(Database.Set<T>(), include)
.FirstOrDefaultAsync(x => x.Id == id)
.Then(SetBackingImage);
.FirstOrDefaultAsync(x => x.Id == id);
}
/// <inheritdoc />
@ -357,12 +333,10 @@ namespace Kyoo.Core.Controllers
{
return AddIncludes(Database.Set<T>(), include)
.OrderBy(x => EF.Functions.Random())
.FirstOrDefaultAsync()
.Then(SetBackingImage);
.FirstOrDefaultAsync();
}
return AddIncludes(Database.Set<T>(), include)
.FirstOrDefaultAsync(x => x.Slug == slug)
.Then(SetBackingImage);
.FirstOrDefaultAsync(x => x.Slug == slug);
}
/// <inheritdoc />
@ -374,21 +348,19 @@ namespace Kyoo.Core.Controllers
AddIncludes(Database.Set<T>(), include),
sortBy
)
.FirstOrDefaultAsync(where)
.Then(SetBackingImage);
.FirstOrDefaultAsync(where);
}
/// <inheritdoc/>
public abstract Task<ICollection<T>> Search(string query);
/// <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,
Pagination? limit = default,
Include<T>? include = default)
{
return (await ApplyFilters(Database.Set<T>(), where, sort, limit, include))
.Select(SetBackingImageSelf).ToList();
return ApplyFilters(Database.Set<T>(), where, sort, limit, include);
}
/// <summary>
@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

@ -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.

View File

@ -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 };

View File

@ -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.

View File

@ -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.

View File

@ -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 };

View File

@ -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 = <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.
*/
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) =>
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[]),
},
});