mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Adapt models (serie
& co) for new api
This commit is contained in:
parent
43cf343841
commit
6c243b8961
7
front/src/models/entry.ts
Normal file
7
front/src/models/entry.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import z from "zod";
|
||||
|
||||
export const Entry = z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
});
|
||||
export type Entry = z.infer<typeof Entry>;
|
@ -1,4 +1,4 @@
|
||||
export * from "./page";
|
||||
export * from "./utils/page";
|
||||
export * from "./kyoo-error";
|
||||
export * from "./resources";
|
||||
export * from "./traits";
|
||||
|
@ -1,26 +0,0 @@
|
||||
export enum Genre {
|
||||
Action = "Action",
|
||||
Adventure = "Adventure",
|
||||
Animation = "Animation",
|
||||
Comedy = "Comedy",
|
||||
Crime = "Crime",
|
||||
Documentary = "Documentary",
|
||||
Drama = "Drama",
|
||||
Family = "Family",
|
||||
Fantasy = "Fantasy",
|
||||
History = "History",
|
||||
Horror = "Horror",
|
||||
Music = "Music",
|
||||
Mystery = "Mystery",
|
||||
Romance = "Romance",
|
||||
ScienceFiction = "ScienceFiction",
|
||||
Thriller = "Thriller",
|
||||
War = "War",
|
||||
Western = "Western",
|
||||
Kids = "Kids",
|
||||
News = "News",
|
||||
Reality = "Reality",
|
||||
Soap = "Soap",
|
||||
Talk = "Talk",
|
||||
Politics = "Politics",
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
export * from "./account";
|
||||
export * from "./library-item";
|
||||
export * from "./news";
|
||||
export * from "./show";
|
||||
export * from "./movie";
|
||||
export * from "./collection";
|
||||
export * from "./genre";
|
||||
export * from "./person";
|
||||
export * from "./studio";
|
||||
export * from "./episode";
|
||||
export * from "./season";
|
||||
export * from "./watch-info";
|
||||
export * from "./watch-status";
|
||||
export * from "./watchlist";
|
||||
export * from "./user";
|
||||
export * from "./server-info";
|
@ -1,21 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const MetadataP = z.preprocess(
|
||||
(x) =>
|
||||
typeof x === "object" && x ? Object.fromEntries(Object.entries(x).filter(([_, v]) => v)) : x,
|
||||
z.record(
|
||||
z.object({
|
||||
/*
|
||||
* The ID of the resource on the external provider.
|
||||
*/
|
||||
dataId: z.string(),
|
||||
|
||||
/*
|
||||
* The URL of the resource on the external provider.
|
||||
*/
|
||||
link: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export type Metadata = z.infer<typeof MetadataP>;
|
@ -1,107 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { ImagesP, ResourceP } from "../traits";
|
||||
import { zdate } from "../utils";
|
||||
import { BaseEpisodeP } from "./episode.base";
|
||||
import { Genre } from "./genre";
|
||||
import { MetadataP } from "./metadata";
|
||||
import { StudioP } from "./studio";
|
||||
import { ShowWatchStatusP } from "./watch-status";
|
||||
|
||||
/**
|
||||
* The enum containing show's status.
|
||||
*/
|
||||
export enum Status {
|
||||
Unknown = "Unknown",
|
||||
Finished = "Finished",
|
||||
Airing = "Airing",
|
||||
Planned = "Planned",
|
||||
}
|
||||
|
||||
export const ShowP = ResourceP("show")
|
||||
.merge(ImagesP)
|
||||
.extend({
|
||||
/**
|
||||
* The title of this show.
|
||||
*/
|
||||
name: z.string(),
|
||||
/**
|
||||
* A catchphrase for this show.
|
||||
*/
|
||||
tagline: z.string().nullable(),
|
||||
/**
|
||||
* The list of alternative titles of this show.
|
||||
*/
|
||||
aliases: z.array(z.string()),
|
||||
/**
|
||||
* The summary of this show.
|
||||
*/
|
||||
overview: z.string().nullable(),
|
||||
/**
|
||||
* A list of tags that match this movie.
|
||||
*/
|
||||
tags: z.array(z.string()),
|
||||
/**
|
||||
* Is this show airing, not aired yet or finished?
|
||||
*/
|
||||
status: z.nativeEnum(Status),
|
||||
/**
|
||||
* How well this item is rated? (from 0 to 100).
|
||||
*/
|
||||
rating: z.number().int().gte(0).lte(100),
|
||||
/**
|
||||
* The date this show started airing. It can be null if this is unknown.
|
||||
*/
|
||||
startAir: zdate().nullable(),
|
||||
/**
|
||||
* The date this show finished airing. It can also be null if this is unknown.
|
||||
*/
|
||||
endAir: zdate().nullable(),
|
||||
/**
|
||||
* The list of genres (themes) this show has.
|
||||
*/
|
||||
genres: z.array(z.nativeEnum(Genre)),
|
||||
/**
|
||||
* A youtube url for the trailer.
|
||||
*/
|
||||
trailer: z.string().optional().nullable(),
|
||||
/**
|
||||
* The studio that made this show.
|
||||
*/
|
||||
studio: StudioP.optional().nullable(),
|
||||
/**
|
||||
* The first episode of this show
|
||||
*/
|
||||
firstEpisode: BaseEpisodeP.optional().nullable(),
|
||||
/**
|
||||
* The link to metadata providers that this show has.
|
||||
*/
|
||||
externalId: MetadataP,
|
||||
/**
|
||||
* Metadata of what an user as started/planned to watch.
|
||||
*/
|
||||
watchStatus: ShowWatchStatusP.nullable().optional(),
|
||||
/**
|
||||
* The number of episodes in this show.
|
||||
*/
|
||||
episodesCount: z.number().int().gte(0).optional(),
|
||||
})
|
||||
.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) => ({
|
||||
href: `/show/${x.slug}`,
|
||||
playHref: x.firstEpisode ? `/watch/${x.firstEpisode.slug}` : null,
|
||||
...x,
|
||||
}));
|
||||
|
||||
/**
|
||||
* A tv serie or an anime.
|
||||
*/
|
||||
export type Show = z.infer<typeof ShowP>;
|
@ -1,14 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { ResourceP } from "../traits";
|
||||
|
||||
export const StudioP = ResourceP("studio").extend({
|
||||
/**
|
||||
* The name of this studio.
|
||||
*/
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* A studio that make shows.
|
||||
*/
|
||||
export type Studio = z.infer<typeof StudioP>;
|
@ -1,50 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { zdate } from "../utils";
|
||||
import { BaseEpisodeP } from "./episode.base";
|
||||
|
||||
export enum WatchStatusV {
|
||||
Completed = "Completed",
|
||||
Watching = "Watching",
|
||||
Droped = "Droped",
|
||||
Planned = "Planned",
|
||||
}
|
||||
|
||||
export const WatchStatusP = z.object({
|
||||
/**
|
||||
* The date this item was added to the watchlist (watched or plan to watch by the user).
|
||||
*/
|
||||
addedDate: zdate(),
|
||||
/**
|
||||
* The date at which this item was played.
|
||||
*/
|
||||
playedDate: zdate().nullable(),
|
||||
/**
|
||||
* Has the user started watching, is it planned?
|
||||
*/
|
||||
status: z.nativeEnum(WatchStatusV),
|
||||
/**
|
||||
* Where the player has stopped watching the episode (in seconds).
|
||||
* Null if the status is not Watching or if the next episode is not started.
|
||||
*/
|
||||
watchedTime: z.number().int().gte(0).nullable(),
|
||||
/**
|
||||
* Where the player has stopped watching the episode (in percentage between 0 and 100).
|
||||
* Null if the status is not Watching or if the next episode is not started.
|
||||
*/
|
||||
watchedPercent: z.number().int().gte(0).lte(100).nullable(),
|
||||
});
|
||||
export type WatchStatus = z.infer<typeof WatchStatusP>;
|
||||
|
||||
export const ShowWatchStatusP = WatchStatusP.and(
|
||||
z.object({
|
||||
/**
|
||||
* The number of episodes the user has not seen.
|
||||
*/
|
||||
unseenEpisodesCount: z.number().int().gte(0),
|
||||
/**
|
||||
* The next episode to watch
|
||||
*/
|
||||
nextEpisode: BaseEpisodeP.nullable(),
|
||||
}),
|
||||
);
|
||||
export type ShowWatchStatus = z.infer<typeof ShowWatchStatusP>;
|
62
front/src/models/serie.ts
Normal file
62
front/src/models/serie.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { z } from "zod";
|
||||
import { Entry } from "./entry";
|
||||
import { Studio } from "./studio";
|
||||
import { Genre } from "./utils/genre";
|
||||
import { Image } from "./utils/images";
|
||||
import { Metadata } from "./utils/metadata";
|
||||
import { zdate } from "./utils/utils";
|
||||
|
||||
export const Serie = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
original: z.object({
|
||||
name: z.string(),
|
||||
latinName: z.string().nullable(),
|
||||
language: z.string(),
|
||||
}),
|
||||
tagline: z.string().nullable(),
|
||||
aliases: z.array(z.string()),
|
||||
tags: z.array(z.string()),
|
||||
description: z.string().nullable(),
|
||||
status: z.enum(["unknown", "finished", "airing", "planned"]),
|
||||
rating: z.number().int().gte(0).lte(100).nullable(),
|
||||
startAir: zdate().nullable(),
|
||||
endAir: zdate().nullable(),
|
||||
genres: z.array(Genre),
|
||||
runtime: z.number().nullable(),
|
||||
externalId: Metadata,
|
||||
|
||||
entriesCount: z.number().int(),
|
||||
availableCount: z.number().int(),
|
||||
|
||||
poster: Image.nullable(),
|
||||
thumbnail: Image.nullable(),
|
||||
banner: Image.nullable(),
|
||||
logo: Image.nullable(),
|
||||
trailerUrl: z.string().optional().nullable(),
|
||||
|
||||
createdAt: zdate(),
|
||||
updatedAt: zdate(),
|
||||
|
||||
studios: z.array(Studio).optional(),
|
||||
firstEntry: Entry.optional().nullable(),
|
||||
nextEntry: Entry.optional().nullable(),
|
||||
watchStatus: z
|
||||
.object({
|
||||
status: z.enum(["completed", "watching", "rewatching", "dropped", "planned"]),
|
||||
score: z.number().int().gte(0).lte(100).nullable(),
|
||||
startedAt: zdate().nullable(),
|
||||
completedAt: zdate().nullable(),
|
||||
seenCount: z.number().int().gte(0),
|
||||
})
|
||||
.nullable(),
|
||||
})
|
||||
.transform((x) => ({
|
||||
...x,
|
||||
href: `/serie/${x.slug}`,
|
||||
playHref: x.firstEntry ? `/watch/${x.firstEntry.slug}` : null,
|
||||
}));
|
||||
|
||||
export type Serie = z.infer<typeof Serie>;
|
15
front/src/models/studio.ts
Normal file
15
front/src/models/studio.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
import { Image } from "./utils/images";
|
||||
import { Metadata } from "./utils/metadata";
|
||||
import { zdate } from "./utils/utils";
|
||||
|
||||
export const Studio = z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
logo: Image.nullable(),
|
||||
externalId: Metadata,
|
||||
createdAt: zdate(),
|
||||
updatedAt: zdate(),
|
||||
});
|
||||
export type Studio = z.infer<typeof Studio>;
|
@ -1,37 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const Img = z.object({
|
||||
source: z.string(),
|
||||
blurhash: z.string(),
|
||||
low: z.string(),
|
||||
medium: z.string(),
|
||||
high: z.string(),
|
||||
});
|
||||
|
||||
export 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
|
||||
* resource, this field won't be present.
|
||||
*/
|
||||
poster: Img.nullable(),
|
||||
|
||||
/**
|
||||
* An url to the thumbnail 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 resource, this field won't be present.
|
||||
*/
|
||||
thumbnail: Img.nullable(),
|
||||
|
||||
/**
|
||||
* An url to the logo 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
|
||||
* resource, this field won't be present.
|
||||
*/
|
||||
logo: Img.nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Base traits for items that has image resources.
|
||||
*/
|
||||
export type KyooImage = z.infer<typeof Img>;
|
@ -1,2 +0,0 @@
|
||||
export * from "./resource";
|
||||
export * from "./images";
|
@ -1,25 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ResourceP = <T extends string>(kind: T) =>
|
||||
z.object({
|
||||
/**
|
||||
* A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
|
||||
*/
|
||||
id: z.string(),
|
||||
|
||||
/**
|
||||
* A human-readable identifier that can be used instead of an ID. A slug must be unique for a type
|
||||
* of resource but it can be changed.
|
||||
*/
|
||||
slug: z.string(),
|
||||
|
||||
/**
|
||||
* The type of resource
|
||||
*/
|
||||
kind: z.literal(kind),
|
||||
});
|
||||
|
||||
/**
|
||||
* The base trait used to represent identifiable resources.
|
||||
*/
|
||||
export type Resource = z.infer<ReturnType<typeof ResourceP>>;
|
27
front/src/models/utils/genre.ts
Normal file
27
front/src/models/utils/genre.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import z from "zod";
|
||||
|
||||
export const Genre = z.enum([
|
||||
"action",
|
||||
"adventure",
|
||||
"animation",
|
||||
"comedy",
|
||||
"crime",
|
||||
"documentary",
|
||||
"drama",
|
||||
"family",
|
||||
"fantasy",
|
||||
"history",
|
||||
"horror",
|
||||
"music",
|
||||
"mystery",
|
||||
"romance",
|
||||
"science-fiction",
|
||||
"thriller",
|
||||
"war",
|
||||
"western",
|
||||
"kids",
|
||||
"reality",
|
||||
"politics",
|
||||
"soap",
|
||||
"talk",
|
||||
]);
|
16
front/src/models/utils/images.ts
Normal file
16
front/src/models/utils/images.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const Image = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
source: z.string(),
|
||||
blurhash: z.string(),
|
||||
})
|
||||
.transform((x) => ({
|
||||
...x,
|
||||
low: `/images/${x.id}?quality=low`,
|
||||
medium: `/images/${x.id}?quality=medium`,
|
||||
high: `/images/${x.id}?quality=high`,
|
||||
}));
|
||||
|
||||
export type Image = z.infer<typeof Image>;
|
9
front/src/models/utils/metadata.ts
Normal file
9
front/src/models/utils/metadata.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const Metadata = z.record(
|
||||
z.object({
|
||||
dataId: z.string(),
|
||||
link: z.string().nullable(),
|
||||
}),
|
||||
);
|
||||
export type Metadata = z.infer<typeof Metadata>;
|
@ -1,38 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* A page of resource that contains information about the pagination of resources.
|
||||
*/
|
||||
export interface Page<T> {
|
||||
/**
|
||||
* The link of the current page.
|
||||
*
|
||||
* @format uri
|
||||
*/
|
||||
this: string;
|
||||
|
||||
/**
|
||||
* The link of the first page.
|
||||
*
|
||||
* @format uri
|
||||
*/
|
||||
first: string;
|
||||
|
||||
/**
|
||||
* The link of the next page.
|
||||
*
|
||||
* @format uri
|
||||
*/
|
||||
next: string | null;
|
||||
|
||||
/**
|
||||
* The number of items in the current page.
|
||||
*/
|
||||
count: number;
|
||||
|
||||
/**
|
||||
* The list of items in the page.
|
||||
*/
|
||||
items: T[];
|
||||
}
|
||||
|
@ -1,176 +0,0 @@
|
||||
import type { KyooImage, WatchStatusV } from "@kyoo/models";
|
||||
import {
|
||||
GradientImageBackground,
|
||||
Heading,
|
||||
Link,
|
||||
P,
|
||||
Poster,
|
||||
PosterBackground,
|
||||
Skeleton,
|
||||
imageBorderRadius,
|
||||
important,
|
||||
ts,
|
||||
} from "@kyoo/primitives";
|
||||
import { useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||
import { ItemContext } from "../../packages/ui/src/components/context-menus";
|
||||
import type { Layout } from "../fetch";
|
||||
import { ItemWatchStatus } from "../ui/browse/grid";
|
||||
|
||||
export const ItemList = ({
|
||||
href,
|
||||
slug,
|
||||
type,
|
||||
name,
|
||||
subtitle,
|
||||
thumbnail,
|
||||
poster,
|
||||
watchStatus,
|
||||
unseenEpisodesCount,
|
||||
...props
|
||||
}: {
|
||||
href: string;
|
||||
slug: string;
|
||||
type: "movie" | "show" | "collection";
|
||||
name: string;
|
||||
subtitle: string | null;
|
||||
poster: KyooImage | null;
|
||||
thumbnail: KyooImage | null;
|
||||
watchStatus: WatchStatusV | null;
|
||||
unseenEpisodesCount: number | null;
|
||||
}) => {
|
||||
const { css } = useYoshiki("line");
|
||||
const [moreOpened, setMoreOpened] = useState(false);
|
||||
|
||||
return (
|
||||
<GradientImageBackground
|
||||
src={thumbnail}
|
||||
alt={name}
|
||||
quality="medium"
|
||||
as={Link}
|
||||
href={moreOpened ? undefined : href}
|
||||
onLongPress={() => setMoreOpened(true)}
|
||||
{...css(
|
||||
{
|
||||
alignItems: "center",
|
||||
justifyContent: "space-evenly",
|
||||
flexDirection: "row",
|
||||
height: ItemList.layout.size,
|
||||
borderRadius: px(imageBorderRadius),
|
||||
overflow: "hidden",
|
||||
marginX: ItemList.layout.gap,
|
||||
child: {
|
||||
more: {
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
fover: {
|
||||
title: {
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
more: {
|
||||
opacity: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
width: { xs: "50%", lg: "30%" },
|
||||
})}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
})}
|
||||
>
|
||||
<Heading
|
||||
{...css([
|
||||
"title",
|
||||
{
|
||||
textAlign: "center",
|
||||
fontSize: rem(2),
|
||||
letterSpacing: rem(0.002),
|
||||
fontWeight: "900",
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
])}
|
||||
>
|
||||
{name}
|
||||
</Heading>
|
||||
{type !== "collection" && (
|
||||
<ItemContext
|
||||
type={type}
|
||||
slug={slug}
|
||||
status={watchStatus}
|
||||
isOpen={moreOpened}
|
||||
setOpen={(v) => setMoreOpened(v)}
|
||||
{...css([
|
||||
{
|
||||
// I dont know why marginLeft gets overwritten by the margin: px(2) so we important
|
||||
marginLeft: important(ts(2)),
|
||||
bg: (theme) => theme.darkOverlay,
|
||||
},
|
||||
"more",
|
||||
Platform.OS === "web" && moreOpened && { opacity: important(100) },
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{subtitle && (
|
||||
<P
|
||||
{...css({
|
||||
textAlign: "center",
|
||||
marginRight: ts(4),
|
||||
})}
|
||||
>
|
||||
{subtitle}
|
||||
</P>
|
||||
)}
|
||||
</View>
|
||||
<PosterBackground src={poster} alt="" quality="low" layout={{ height: percent(80) }}>
|
||||
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
|
||||
</PosterBackground>
|
||||
</GradientImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
ItemList.Loader = (props: object) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
alignItems: "center",
|
||||
justifyContent: "space-evenly",
|
||||
flexDirection: "row",
|
||||
height: ItemList.layout.size,
|
||||
borderRadius: px(imageBorderRadius),
|
||||
overflow: "hidden",
|
||||
bg: (theme) => theme.dark.background,
|
||||
marginX: ItemList.layout.gap,
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
width: { xs: "50%", lg: "30%" },
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
})}
|
||||
>
|
||||
<Skeleton {...css({ height: rem(2), alignSelf: "center" })} />
|
||||
<Skeleton {...css({ width: rem(5), alignSelf: "center" })} />
|
||||
</View>
|
||||
<Poster.Loader layout={{ height: percent(80) }} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
ItemList.layout = { numColumns: 1, size: 300, layout: "vertical", gap: ts(2) } satisfies Layout;
|
Loading…
x
Reference in New Issue
Block a user