mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-04-07 01:31:56 -04:00
Hide unaired nextups (#1421)
This commit is contained in:
commit
fb03290b49
4
api/drizzle/0031_entry-content.sql
Normal file
4
api/drizzle/0031_entry-content.sql
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TYPE "kyoo"."entry_content" AS ENUM('story', 'recap', 'filler', 'ova');--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entries" ADD COLUMN "content" "kyoo"."entry_content";--> statement-breakpoint
|
||||
UPDATE "kyoo"."entries" SET content = 'story';-->statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entries" ALTER COLUMN "content" SET NOT NULl;--> statement-breakpoint
|
||||
2028
api/drizzle/meta/0031_snapshot.json
Normal file
2028
api/drizzle/meta/0031_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -218,6 +218,13 @@
|
||||
"when": 1774974162419,
|
||||
"tag": "0030_external_hist",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 31,
|
||||
"version": "7",
|
||||
"when": 1775238108619,
|
||||
"tag": "0031_entry-content",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
MovieEntry,
|
||||
Special,
|
||||
} from "~/models/entry";
|
||||
import { EntryContent } from "~/models/entry/base-entry";
|
||||
import { KError } from "~/models/error";
|
||||
import { madeInAbyss } from "~/models/examples";
|
||||
import { Season } from "~/models/season";
|
||||
@ -83,6 +84,7 @@ export const entryFilters: FilterDef = {
|
||||
order: { column: entries.order, type: "float" },
|
||||
runtime: { column: entries.runtime, type: "float" },
|
||||
airDate: { column: entries.airDate, type: "date" },
|
||||
content: { column: entries.content, type: "enum", values: EntryContent.enum },
|
||||
playedDate: { column: entryProgressQ.playedDate, type: "date" },
|
||||
isAvailable: { column: isNotNull(entries.availableSince), type: "bool" },
|
||||
};
|
||||
|
||||
@ -20,6 +20,7 @@ import { coalesce, sqlarr } from "~/db/utils";
|
||||
import { Entry } from "~/models/entry";
|
||||
import { KError } from "~/models/error";
|
||||
import { SeedHistory } from "~/models/history";
|
||||
import { Show } from "~/models/show";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
createPage,
|
||||
@ -198,6 +199,7 @@ async function updateWatchlist(
|
||||
and(
|
||||
eq(nextEntry.showPk, entries.showPk),
|
||||
ne(nextEntry.kind, "extra"),
|
||||
eq(nextEntry.content, "story"),
|
||||
gt(nextEntry.order, entries.order),
|
||||
),
|
||||
)
|
||||
@ -366,15 +368,16 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
query,
|
||||
sort,
|
||||
filter: and(
|
||||
isNotNull(entryProgressQ.playedDate),
|
||||
eq(entryProgressQ.external, false),
|
||||
isNotNull(historyProgressQ.playedDate),
|
||||
eq(historyProgressQ.external, false),
|
||||
ne(entries.kind, "extra"),
|
||||
filter,
|
||||
),
|
||||
languages: langs,
|
||||
userId: sub,
|
||||
progressQ: historyProgressQ,
|
||||
})) as Entry[];
|
||||
relations: ["show"],
|
||||
})) as (Entry & { show: Show })[];
|
||||
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
@ -386,7 +389,7 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||
}),
|
||||
response: {
|
||||
200: Page(Entry),
|
||||
200: Page(t.Intersect([Entry, t.Object({ show: Show })])),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { and, eq, isNotNull, lt, or, sql } from "drizzle-orm";
|
||||
import Elysia, { t } from "elysia";
|
||||
import { auth } from "~/auth";
|
||||
import { db } from "~/db";
|
||||
@ -118,6 +118,10 @@ export const nextup = new Elysia({ tags: ["profiles"] })
|
||||
.leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk))
|
||||
.where(
|
||||
and(
|
||||
or(
|
||||
lt(entries.airDate, sql`now()`),
|
||||
isNotNull(entries.availableSince),
|
||||
),
|
||||
filter,
|
||||
query ? sql`${transQ.name} %> ${query}::text` : undefined,
|
||||
keysetPaginate({ after, sort }),
|
||||
|
||||
@ -59,6 +59,7 @@ export const insertEntries = record(
|
||||
const { translations, videos, video, ...entry } = seed;
|
||||
return {
|
||||
...entry,
|
||||
content: entry.kind !== "extra" ? entry.content : "story",
|
||||
showPk: show.pk,
|
||||
slug: generateSlug(show.slug, seed),
|
||||
thumbnail: enqueueOptImage(imgQueue, {
|
||||
|
||||
@ -96,6 +96,7 @@ export const seedMovie = async (
|
||||
{
|
||||
...movie,
|
||||
kind: "movie",
|
||||
content: "story",
|
||||
order: 1,
|
||||
thumbnail: (movie.originalLanguage
|
||||
? translations[movie.originalLanguage]
|
||||
|
||||
@ -247,7 +247,7 @@ function getNextVideoEntry({
|
||||
.leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk))
|
||||
.crossJoinLateral(entryVideosQ)
|
||||
.leftJoin(entryVideoJoin, eq(entries.pk, entryVideoJoin.entryPk))
|
||||
.innerJoin(vids, eq(vids.pk, entryVideoJoin.videoPk))
|
||||
.leftJoin(vids, eq(vids.pk, entryVideoJoin.videoPk))
|
||||
.where(
|
||||
and(
|
||||
// either way it needs to be of the same show
|
||||
@ -277,6 +277,7 @@ function getNextVideoEntry({
|
||||
eq(vids.part, sql`${videos.part} ${sql.raw(prev ? "-" : "+")} 1`),
|
||||
),
|
||||
),
|
||||
eq(entries.content, "story"),
|
||||
),
|
||||
)
|
||||
.orderBy(
|
||||
|
||||
@ -24,6 +24,13 @@ export const entryType = schema.enum("entry_type", [
|
||||
"extra",
|
||||
]);
|
||||
|
||||
export const entryContent = schema.enum("entry_content", [
|
||||
"story",
|
||||
"recap",
|
||||
"filler",
|
||||
"ova",
|
||||
]);
|
||||
|
||||
export const entry_extid = () =>
|
||||
jsonb()
|
||||
.$type<
|
||||
@ -68,6 +75,7 @@ export const entries = schema.table(
|
||||
airDate: date(),
|
||||
runtime: integer(),
|
||||
thumbnail: image(),
|
||||
content: entryContent().notNull(),
|
||||
|
||||
externalId: entry_extid(),
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { t } from "elysia";
|
||||
import { Image } from "../utils/image";
|
||||
|
||||
export const EntryContent = t.UnionEnum(["story", "recap", "filler", "ova"]);
|
||||
|
||||
export const BaseEntry = () =>
|
||||
t.Object({
|
||||
airDate: t.Nullable(t.String({ format: "date" })),
|
||||
@ -11,6 +13,7 @@ export const BaseEntry = () =>
|
||||
}),
|
||||
),
|
||||
thumbnail: t.Nullable(Image),
|
||||
content: EntryContent,
|
||||
});
|
||||
|
||||
export const EntryTranslation = () =>
|
||||
|
||||
@ -22,7 +22,7 @@ export const BaseExtra = t.Composite(
|
||||
kind: ExtraType,
|
||||
name: t.String(),
|
||||
}),
|
||||
t.Omit(BaseEntry(), ["nextRefresh", "airDate"]),
|
||||
t.Omit(BaseEntry(), ["nextRefresh", "airDate", "content"]),
|
||||
],
|
||||
{
|
||||
description: comment`
|
||||
|
||||
@ -175,6 +175,7 @@ export const madeInAbyss = {
|
||||
entries: [
|
||||
{
|
||||
kind: "episode",
|
||||
content: "story",
|
||||
order: 13,
|
||||
seasonNumber: 1,
|
||||
episodeNumber: 13,
|
||||
@ -203,6 +204,7 @@ export const madeInAbyss = {
|
||||
},
|
||||
{
|
||||
kind: "special",
|
||||
content: "ova",
|
||||
// between s1e13 & movie (which has 13.5 for the `order field`)
|
||||
order: 13.25,
|
||||
number: 3,
|
||||
@ -230,6 +232,7 @@ export const madeInAbyss = {
|
||||
},
|
||||
{
|
||||
kind: "movie",
|
||||
content: "story",
|
||||
slug: "made-in-abyss-dawn-of-the-deep-soul",
|
||||
order: 13.5,
|
||||
translations: {
|
||||
@ -257,6 +260,7 @@ export const madeInAbyss = {
|
||||
},
|
||||
{
|
||||
kind: "episode",
|
||||
content: "story",
|
||||
order: 14,
|
||||
seasonNumber: 2,
|
||||
episodeNumber: 1,
|
||||
@ -284,6 +288,7 @@ export const madeInAbyss = {
|
||||
},
|
||||
{
|
||||
kind: "episode",
|
||||
content: "story",
|
||||
order: 15,
|
||||
seasonNumber: 2,
|
||||
episodeNumber: 2,
|
||||
@ -311,6 +316,7 @@ export const madeInAbyss = {
|
||||
},
|
||||
{
|
||||
kind: "episode",
|
||||
content: "story",
|
||||
order: 16,
|
||||
seasonNumber: 2,
|
||||
episodeNumber: 3,
|
||||
@ -338,6 +344,7 @@ export const madeInAbyss = {
|
||||
},
|
||||
{
|
||||
kind: "episode",
|
||||
content: "story",
|
||||
order: 17,
|
||||
seasonNumber: 2,
|
||||
episodeNumber: 4,
|
||||
|
||||
@ -16,10 +16,12 @@ export const FullVideo = t.Composite([
|
||||
previous: t.Optional(
|
||||
t.Nullable(
|
||||
t.Object({
|
||||
video: t.String({
|
||||
format: "slug",
|
||||
examples: ["made-in-abyss-s1e12"],
|
||||
}),
|
||||
video: t.Nullable(
|
||||
t.String({
|
||||
format: "slug",
|
||||
examples: ["made-in-abyss-s1e12"],
|
||||
}),
|
||||
),
|
||||
entry: Entry,
|
||||
}),
|
||||
),
|
||||
@ -27,10 +29,12 @@ export const FullVideo = t.Composite([
|
||||
next: t.Optional(
|
||||
t.Nullable(
|
||||
t.Object({
|
||||
video: t.String({
|
||||
format: "slug",
|
||||
examples: ["made-in-abyss-dawn-of-the-deep-soul"],
|
||||
}),
|
||||
video: t.Nullable(
|
||||
t.String({
|
||||
format: "slug",
|
||||
examples: ["made-in-abyss-dawn-of-the-deep-soul"],
|
||||
}),
|
||||
),
|
||||
entry: Entry,
|
||||
}),
|
||||
),
|
||||
|
||||
@ -28,7 +28,7 @@ beforeAll(async () => {
|
||||
});
|
||||
|
||||
const miaEntrySlug = `${madeInAbyss.slug}-s1e13`;
|
||||
const miaNextEntrySlug = `${madeInAbyss.slug}-sp3`;
|
||||
const miaNextEntrySlug = "made-in-abyss-dawn-of-the-deep-soul";
|
||||
|
||||
describe("nextup", () => {
|
||||
it("Watchlist populates nextup", async () => {
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
"part": "Part {{number}}",
|
||||
"videos-map": "Edit video mappings",
|
||||
"remap": "Remap",
|
||||
"staff-as":"as {{character}}",
|
||||
"staff-as": "as {{character}}",
|
||||
"staff-kind": {
|
||||
"actor": "Actor",
|
||||
"director": "Director",
|
||||
@ -253,7 +253,9 @@
|
||||
"transmux": "Original",
|
||||
"auto": "Auto",
|
||||
"notInPristine": "Unavailable in pristine",
|
||||
"unsupportedError": "Video codec not supported, transcoding in progress..."
|
||||
"unsupportedError": "Video codec not supported, transcoding in progress...",
|
||||
"not-available": "{{entry}} is not available on kyoo yet, ask your server admins about it",
|
||||
"fatal": "Fatal playback error"
|
||||
},
|
||||
"search": {
|
||||
"empty": "No result found. Try a different query."
|
||||
|
||||
@ -36,7 +36,7 @@ export const EntryBox = ({
|
||||
serieSlug: string | null;
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
href: string;
|
||||
href: string | null;
|
||||
thumbnail: KImage | null;
|
||||
watchedPercent: number;
|
||||
videos: Entry["videos"];
|
||||
@ -51,7 +51,11 @@ export const EntryBox = ({
|
||||
href={moreOpened || videos.length > 1 ? undefined : href}
|
||||
onPress={videos.length > 1 ? onSelectVideos : undefined}
|
||||
onLongPress={() => setMoreOpened(true)}
|
||||
className={cn("group w-[350px] items-center p-1 outline-0", className)}
|
||||
className={cn(
|
||||
"group w-[350px] items-center p-1 outline-0",
|
||||
href === null && "opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ThumbnailBackground
|
||||
|
||||
@ -56,13 +56,6 @@ export const EntryContext = ({
|
||||
{...tooltip(t("misc.more"))}
|
||||
{...(props as any)}
|
||||
>
|
||||
{account && (
|
||||
<Menu.Item
|
||||
label={t("show.watchlistMark.completed")}
|
||||
icon={watchListIcon("completed")}
|
||||
onSelect={() => markAsSeenMutation.mutate()}
|
||||
/>
|
||||
)}
|
||||
{serieSlug && (
|
||||
<Menu.Item
|
||||
label={t("home.episodeMore.goToShow")}
|
||||
@ -70,6 +63,13 @@ export const EntryContext = ({
|
||||
href={`/${kind === "movie" ? "movies" : "series"}/${serieSlug}`}
|
||||
/>
|
||||
)}
|
||||
{account && (
|
||||
<Menu.Item
|
||||
label={t("show.watchlistMark.completed")}
|
||||
icon={watchListIcon("completed")}
|
||||
onSelect={() => markAsSeenMutation.mutate()}
|
||||
/>
|
||||
)}
|
||||
{/* <Menu.Item */}
|
||||
{/* label={t("home.episodeMore.download")} */}
|
||||
{/* icon={Download} */}
|
||||
|
||||
@ -14,6 +14,8 @@ const Base = z.object({
|
||||
runtime: z.number().nullable(),
|
||||
thumbnail: KImage.nullable(),
|
||||
|
||||
content: z.enum(["story", "recap", "filler", "ova"]),
|
||||
|
||||
createdAt: zdate(),
|
||||
updatedAt: zdate(),
|
||||
|
||||
|
||||
@ -49,8 +49,14 @@ export const FullVideo = Video.extend({
|
||||
playedDate: zdate().nullable(),
|
||||
videoId: z.string().nullable(),
|
||||
}),
|
||||
previous: z.object({ video: z.string(), entry: Entry }).nullable().optional(),
|
||||
next: z.object({ video: z.string(), entry: Entry }).nullable().optional(),
|
||||
previous: z
|
||||
.object({ video: z.string().nullable(), entry: Entry })
|
||||
.nullable()
|
||||
.optional(),
|
||||
next: z
|
||||
.object({ video: z.string().nullable(), entry: Entry })
|
||||
.nullable()
|
||||
.optional(),
|
||||
show: Show.optional().nullable(),
|
||||
});
|
||||
export type FullVideo = z.infer<typeof FullVideo>;
|
||||
|
||||
@ -24,6 +24,7 @@ export const InfiniteFetch = <Data, Type extends string = string>({
|
||||
query,
|
||||
placeholderCount = 4,
|
||||
incremental = false,
|
||||
getKey,
|
||||
getItemType,
|
||||
getItemSizeMult,
|
||||
getStickyIndices,
|
||||
@ -43,6 +44,7 @@ export const InfiniteFetch = <Data, Type extends string = string>({
|
||||
placeholderCount?: number;
|
||||
layout: Layout;
|
||||
horizontal?: boolean;
|
||||
getKey?: (item: Data, index: number) => string;
|
||||
getItemType?: (item: Data, index: number) => Type;
|
||||
getItemSizeMult?: (item: Data, index: number, type: Type) => number;
|
||||
getStickyIndices?: (items: Data[]) => number[];
|
||||
@ -94,7 +96,10 @@ export const InfiniteFetch = <Data, Type extends string = string>({
|
||||
renderItem={({ item, index }) =>
|
||||
item ? <Render index={index} item={item} /> : <Loader index={index} />
|
||||
}
|
||||
keyExtractor={(item: any, index) => (item ? item.id : index + 1)}
|
||||
keyExtractor={(item: any, index) => {
|
||||
if (!item) return index + 1;
|
||||
return getKey ? getKey(item, index) : item.id;
|
||||
}}
|
||||
horizontal={layout.layout === "horizontal"}
|
||||
numColumns={layout.layout === "horizontal" ? 1 : numColumns}
|
||||
onEndReached={
|
||||
|
||||
@ -37,7 +37,6 @@ import {
|
||||
HR,
|
||||
IconButton,
|
||||
IconFab,
|
||||
Image,
|
||||
ImageBackground,
|
||||
LI,
|
||||
Link,
|
||||
|
||||
@ -246,8 +246,13 @@ EntryList.query = (
|
||||
path: ["api", "series", slug, "entries"],
|
||||
params: {
|
||||
query,
|
||||
// TODO: use a better filter, it removes specials and movies
|
||||
filter: season ? `seasonNumber ge ${season}` : undefined,
|
||||
filter: [
|
||||
// TODO: use a better filter, it removes specials and movies
|
||||
season && `seasonNumber ge ${season}`,
|
||||
"(kind eq episode or isAvailable eq true or content eq story)",
|
||||
]
|
||||
.filter((x) => x)
|
||||
.join(" and "),
|
||||
includeSeasons: true,
|
||||
},
|
||||
infinite: true,
|
||||
|
||||
@ -3,6 +3,7 @@ import { View } from "react-native";
|
||||
import { type KImage, Role } from "~/models";
|
||||
import { Container, H2, Link, P, Poster, Skeleton, SubP } from "~/primitives";
|
||||
import { InfiniteGrid, type QueryIdentifier } from "~/query";
|
||||
import { cn } from "~/utils";
|
||||
import { EmptyView } from "../empty-view";
|
||||
|
||||
export const CharacterCard = ({
|
||||
@ -21,11 +22,17 @@ export const CharacterCard = ({
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="flex-row items-center overflow-hidden rounded-xl bg-card"
|
||||
className={cn(
|
||||
"flex-row items-center overflow-hidden rounded-xl bg-card",
|
||||
"group ring-accent hover:ring-3 focus-visible:ring-3",
|
||||
)}
|
||||
>
|
||||
<Poster src={image} quality="low" className="w-28" />
|
||||
<View className="flex-1 items-center justify-center py-5">
|
||||
<P className="text-center font-semibold" numberOfLines={2}>
|
||||
<P
|
||||
className="text-center font-semibold group-hover:underline group-focus-visible:underline"
|
||||
numberOfLines={2}
|
||||
>
|
||||
{name}
|
||||
</P>
|
||||
<SubP className="mt-1 text-center" numberOfLines={2}>
|
||||
|
||||
@ -45,7 +45,7 @@ export const NewsList = () => {
|
||||
name={`${item.show!.name} ${entryDisplayNumber(item)}`}
|
||||
description={item.name}
|
||||
thumbnail={item.thumbnail ?? item.show!.thumbnail}
|
||||
href={item.href ?? "#"}
|
||||
href={item.href}
|
||||
watchedPercent={item.progress.percent}
|
||||
videos={item.videos}
|
||||
onSelectVideos={() =>
|
||||
|
||||
@ -66,7 +66,7 @@ export const NextupList = () => {
|
||||
name={`${item.show!.name} ${entryDisplayNumber(item)}`}
|
||||
description={item.name}
|
||||
thumbnail={item.thumbnail ?? item.show!.thumbnail}
|
||||
href={item.href ?? "#"}
|
||||
href={item.href}
|
||||
watchedPercent={item.progress.percent}
|
||||
videos={item.videos}
|
||||
onSelectVideos={() =>
|
||||
|
||||
29
front/src/ui/player/controls/error-popup.tsx
Normal file
29
front/src/ui/player/controls/error-popup.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { Heading, IconButton, P } from "~/primitives";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
export const ErrorPopup = ({
|
||||
message,
|
||||
dismiss,
|
||||
}: {
|
||||
message: string;
|
||||
dismiss: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View
|
||||
className={cn(
|
||||
"absolute inset-x-6 top-1/2 flex-1 -translate-y-1/2 flex-row justify-between",
|
||||
"rounded-xl border border-slate-700 bg-background p-5",
|
||||
)}
|
||||
>
|
||||
<View className="flex-1 flex-wrap">
|
||||
<Heading className="my-2">{t("player.fatal")}</Heading>
|
||||
<P className="mt-2 flex-1">{message}</P>
|
||||
</View>
|
||||
<IconButton icon={Close} onPress={dismiss} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@ -20,6 +20,7 @@ export const Controls = ({
|
||||
chapters,
|
||||
playPrev,
|
||||
playNext,
|
||||
forceShow,
|
||||
}: {
|
||||
player: VideoPlayer;
|
||||
showHref?: string;
|
||||
@ -31,6 +32,7 @@ export const Controls = ({
|
||||
chapters: Chapter[];
|
||||
playPrev: (() => boolean) | null;
|
||||
playNext: (() => boolean) | null;
|
||||
forceShow?: boolean;
|
||||
}) => {
|
||||
const isTouch = useIsTouch();
|
||||
|
||||
@ -56,7 +58,7 @@ export const Controls = ({
|
||||
<View className="absolute inset-0">
|
||||
<TouchControls
|
||||
player={player}
|
||||
forceShow={hover || menuOpened}
|
||||
forceShow={hover || menuOpened || forceShow}
|
||||
className="absolute inset-0"
|
||||
>
|
||||
<Back
|
||||
|
||||
@ -2,6 +2,7 @@ import "react-native-get-random-values";
|
||||
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { useEvent, useVideoPlayer, VideoView } from "react-native-video";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@ -14,6 +15,7 @@ import { type QueryIdentifier, useFetch } from "~/query";
|
||||
import { Info } from "~/ui/info";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { Controls, LoadingIndicator } from "./controls";
|
||||
import { ErrorPopup } from "./controls/error-popup";
|
||||
import { toggleFullscreen } from "./controls/misc";
|
||||
import { PlayModeContext } from "./controls/tracks-menu";
|
||||
import { useKeyboard } from "./keyboard";
|
||||
@ -45,6 +47,7 @@ export const Player = () => {
|
||||
);
|
||||
const playModeState = useState(defaultPlayMode);
|
||||
const [playMode, setPlayMode] = playModeState;
|
||||
const [playbackError, setPlaybackError] = useState<KyooError | undefined>();
|
||||
const player = useVideoPlayer(
|
||||
{
|
||||
uri: `${apiUrl}/api/videos/${slug}/${playMode === "direct" ? "direct" : "master.m3u8"}?clientId=${clientId}`,
|
||||
@ -101,18 +104,39 @@ export const Player = () => {
|
||||
}, [player, info?.fonts]);
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const playPrev = useCallback(() => {
|
||||
if (!data?.previous) return false;
|
||||
if (!data.previous.video) {
|
||||
setPlaybackError({
|
||||
status: "not-available",
|
||||
message: t("player.not-available", {
|
||||
entry: `${entryDisplayNumber(data.previous.entry)} ${data.previous.entry.name}`,
|
||||
}),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
setPlaybackError(undefined);
|
||||
setStart("0");
|
||||
setSlug(data.previous.video);
|
||||
return true;
|
||||
}, [data?.previous, setSlug, setStart]);
|
||||
}, [data?.previous, setSlug, setStart, t]);
|
||||
const playNext = useCallback(() => {
|
||||
if (!data?.next) return false;
|
||||
if (!data.next.video) {
|
||||
setPlaybackError({
|
||||
status: "not-available",
|
||||
message: t("player.not-available", {
|
||||
entry: `${entryDisplayNumber(data.next.entry)} ${data.next.entry.name}`,
|
||||
}),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
setPlaybackError(undefined);
|
||||
setStart("0");
|
||||
setSlug(data.next.video);
|
||||
return true;
|
||||
}, [data?.next, setSlug, setStart]);
|
||||
}, [data?.next, setSlug, setStart, t]);
|
||||
|
||||
useProgressObserver(
|
||||
player,
|
||||
@ -148,7 +172,6 @@ export const Player = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [playbackError, setPlaybackError] = useState<KyooError | undefined>();
|
||||
useEvent(player, "onError", (error) => {
|
||||
if (
|
||||
error.code === "source/unsupported-content-type" &&
|
||||
@ -157,9 +180,6 @@ export const Player = () => {
|
||||
setPlayMode("hls");
|
||||
else setPlaybackError({ status: error.code, message: error.message });
|
||||
});
|
||||
if (playbackError) {
|
||||
throw playbackError;
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-black">
|
||||
@ -201,10 +221,17 @@ export const Player = () => {
|
||||
: data?.path
|
||||
}
|
||||
chapters={info?.chapters ?? []}
|
||||
playPrev={data?.previous?.video ? playPrev : null}
|
||||
playNext={data?.next?.video ? playNext : null}
|
||||
playPrev={data?.previous ? playPrev : null}
|
||||
playNext={data?.next ? playNext : null}
|
||||
forceShow={!!playbackError}
|
||||
/>
|
||||
</PlayModeContext.Provider>
|
||||
{playbackError && (
|
||||
<ErrorPopup
|
||||
message={playbackError.message}
|
||||
dismiss={() => setPlaybackError(undefined)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,12 +3,14 @@ import Cancel from "@material-symbols/svg-400/rounded/cancel-fill.svg";
|
||||
import CheckCircle from "@material-symbols/svg-400/rounded/check_circle-fill.svg";
|
||||
import Replay from "@material-symbols/svg-400/rounded/replay.svg";
|
||||
import Clock from "@material-symbols/svg-400/rounded/schedule-fill.svg";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { EntryBox, entryDisplayNumber } from "~/components/entries";
|
||||
import { EntrySelect } from "~/components/entries/select";
|
||||
import { ItemGrid, itemMap } from "~/components/items";
|
||||
import { Entry, Show, type User, User as UserModel } from "~/models";
|
||||
import { Avatar, H1, H3, P, Tabs } from "~/primitives";
|
||||
import { Avatar, H1, H3, P, Tabs, usePopup } from "~/primitives";
|
||||
import { Fetch, InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
import { EmptyView } from "~/ui/empty-view";
|
||||
import { useQueryState } from "~/utils";
|
||||
@ -58,6 +60,25 @@ const ProfileHeader = ({
|
||||
setStatus: (value: WatchlistFilter) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [setPopup, closePopup] = usePopup();
|
||||
|
||||
const openEntrySelect = useCallback(
|
||||
(entry: {
|
||||
displayNumber: string;
|
||||
name: string | null;
|
||||
videos: Entry["videos"];
|
||||
}) => {
|
||||
setPopup(
|
||||
<EntrySelect
|
||||
displayNumber={entry.displayNumber}
|
||||
name={entry.name ?? ""}
|
||||
videos={entry.videos}
|
||||
close={closePopup}
|
||||
/>,
|
||||
);
|
||||
},
|
||||
[setPopup, closePopup],
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="mx-2 my-4 gap-4">
|
||||
@ -90,6 +111,7 @@ const ProfileHeader = ({
|
||||
<InfiniteFetch
|
||||
query={ProfilePage.historyQuery(slug)}
|
||||
layout={{ ...EntryBox.layout, layout: "horizontal" }}
|
||||
getKey={(x) => `${x.id}-${x.progress.playedDate?.toISOString()}`}
|
||||
Empty={<EmptyView message={t("home.none")} />}
|
||||
Render={({ item }) => (
|
||||
<EntryBox
|
||||
@ -103,10 +125,16 @@ const ProfileHeader = ({
|
||||
}
|
||||
description={item.name}
|
||||
thumbnail={item.thumbnail ?? item.show?.thumbnail ?? null}
|
||||
href={item.href ?? "#"}
|
||||
href={item.href}
|
||||
watchedPercent={item.progress.percent}
|
||||
videos={item.videos}
|
||||
onSelectVideos={() => {}}
|
||||
onSelectVideos={() =>
|
||||
openEntrySelect({
|
||||
displayNumber: entryDisplayNumber(item),
|
||||
name: item.name,
|
||||
videos: item.videos,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
Loader={EntryBox.Loader}
|
||||
@ -170,9 +198,6 @@ ProfilePage.historyQuery = (slug: string): QueryIdentifier<Entry> => ({
|
||||
parser: Entry,
|
||||
infinite: true,
|
||||
path: ["api", "profiles", slug, "history"],
|
||||
params: {
|
||||
with: ["show"],
|
||||
},
|
||||
});
|
||||
|
||||
ProfilePage.userQuery = (slug: string): QueryIdentifier<User> => ({
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from enum import Enum
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import Field
|
||||
@ -9,12 +10,20 @@ from ..utils import Language, Model
|
||||
from .metadataid import EpisodeId, MetadataId
|
||||
|
||||
|
||||
class EntryContent(str, Enum):
|
||||
STORY = "story"
|
||||
RECAP = "recap"
|
||||
FILLER = "filler"
|
||||
OVA = "ova"
|
||||
|
||||
|
||||
class Entry(Model):
|
||||
kind: Literal["episode", "movie", "special"]
|
||||
order: float
|
||||
runtime: int | None
|
||||
air_date: date | None
|
||||
thumbnail: str | None
|
||||
content: EntryContent
|
||||
|
||||
# Movie-specific fields
|
||||
slug: str | None
|
||||
|
||||
@ -11,7 +11,7 @@ from aiohttp import ClientResponseError, ClientSession
|
||||
from langcodes import Language
|
||||
|
||||
from ..models.collection import Collection, CollectionTranslation
|
||||
from ..models.entry import Entry, EntryTranslation
|
||||
from ..models.entry import Entry, EntryContent, EntryTranslation
|
||||
from ..models.genre import Genre
|
||||
from ..models.metadataid import EpisodeId, MetadataId, SeasonId
|
||||
from ..models.movie import Movie, MovieStatus, MovieTranslation, SearchMovie
|
||||
@ -556,6 +556,7 @@ class TheMovieDatabase(Provider):
|
||||
if episode["air_date"]
|
||||
else None,
|
||||
thumbnail=self._map_image(episode["still_path"]),
|
||||
content=EntryContent.STORY,
|
||||
slug=None,
|
||||
season_number=episode["season_number"],
|
||||
episode_number=episode["episode_number"],
|
||||
|
||||
@ -11,7 +11,7 @@ from langcodes.data_dicts import LANGUAGE_REPLACEMENTS
|
||||
|
||||
from ..cache import cache
|
||||
from ..models.collection import Collection, CollectionTranslation
|
||||
from ..models.entry import Entry, EntryTranslation
|
||||
from ..models.entry import Entry, EntryContent, EntryTranslation
|
||||
from ..models.genre import Genre
|
||||
from ..models.metadataid import EpisodeId, MetadataId, SeasonId
|
||||
from ..models.movie import Movie, MovieStatus, MovieTranslation, SearchMovie
|
||||
@ -541,6 +541,12 @@ class TVDB(Provider):
|
||||
thumbnail=f"https://artworks.thetvdb.com{entry['image']}"
|
||||
if entry["image"]
|
||||
else None,
|
||||
# Mark specials as ova, waiting for https://github.com/thetvdb/v4-api/issues/350
|
||||
content=(
|
||||
EntryContent.STORY
|
||||
if entry["seasonNumber"] != 0 or entry["isMovie"]
|
||||
else EntryContent.OVA
|
||||
),
|
||||
slug=None,
|
||||
season_number=entry["seasonNumber"],
|
||||
episode_number=entry["number"],
|
||||
@ -620,7 +626,7 @@ class TVDB(Provider):
|
||||
)
|
||||
|
||||
# handle specials and such that are between seasons
|
||||
for entry in ret:
|
||||
for entry in reversed(ret):
|
||||
if entry.order != 0:
|
||||
continue
|
||||
|
||||
@ -635,7 +641,11 @@ class TVDB(Provider):
|
||||
)
|
||||
after = min((x.order for x in ret if x.order > before), default=before)
|
||||
entry.order = (before + after) / 2
|
||||
elif entry.extra["airs_before_season"] is not None:
|
||||
|
||||
for entry in ret:
|
||||
if entry.order != 0:
|
||||
continue
|
||||
if entry.extra["airs_before_season"] is not None:
|
||||
before = (
|
||||
next(
|
||||
(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user