Update player to use new api

This commit is contained in:
Zoe Roux 2023-09-01 17:55:18 +02:00
parent 25418071fe
commit 5ddfe1ddb2
No known key found for this signature in database
33 changed files with 382 additions and 286 deletions

View File

@ -130,6 +130,16 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <summary>
/// Links to watch this movie.
/// </summary>
public VideoLinks? Links => Kind == ItemKind.Movie ? new()
{
Direct = $"/video/movie/{Slug}/direct",
Hls = $"/video/movie/{Slug}/master.m3u8",
}
: null;
public LibraryItem() { }
[JsonConstructor]

View File

@ -163,7 +163,7 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// Links to watch this episode.
/// </summary>
public object Links => new
public VideoLinks Links => new()
{
Direct = $"/video/episode/{Slug}/direct",
Hls = $"/video/episode/{Slug}/master.m3u8",

View File

@ -124,7 +124,7 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// Links to watch this movie.
/// </summary>
public object Links => new
public VideoLinks Links => new()
{
Direct = $"/video/movie/{Slug}/direct",
Hls = $"/video/movie/{Slug}/master.m3u8",

View File

@ -0,0 +1,36 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
namespace Kyoo.Abstractions.Models
{
/// <summary>
/// The links to see a movie or an episode.
/// </summary>
public class VideoLinks
{
/// <summary>
/// The direct link to the unprocessed video (pristine quality).
/// </summary>
public string Direct { get; set; }
/// <summary>
/// The link to an HLS master playlist containing all qualities available for this video.
/// </summary>
public string Hls { get; set; }
}
}

View File

@ -320,6 +320,11 @@ namespace Kyoo.Postgresql
modelBuilder.Entity<User>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Movie>()
.Ignore(x => x.Links);
modelBuilder.Entity<LibraryItem>()
.Ignore(x => x.Links);
}
/// <summary>

View File

@ -67,7 +67,7 @@ namespace Kyoo.Swagger
Kind? kind = controller.Type == null
? controller.Kind
: cur.Kind;
ICollection<string> permissions = _GetPermissionsList(agg, group!.Value);
ICollection<string> permissions = _GetPermissionsList(agg, group ?? Group.Overall);
permissions.Add($"{type}.{kind!.Value.ToString().ToLower()}");
agg[nameof(Kyoo)] = permissions;
return agg;

View File

@ -0,0 +1,34 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Player } from "@kyoo/ui";
import { withRoute } from "../../../utils";
export default withRoute(
Player,
{
options: {
headerShown: false,
},
statusBar: { hidden: true },
fullscreen: true,
},
{ type: "movie" },
);

View File

@ -21,10 +21,14 @@
import { Player } from "@kyoo/ui";
import { withRoute } from "../../utils";
export default withRoute(Player, {
options: {
headerShown: false,
export default withRoute(
Player,
{
options: {
headerShown: false,
},
statusBar: { hidden: true },
fullscreen: true,
},
statusBar: { hidden: true },
fullscreen: true,
});
{ type: "episode" },
);

View File

@ -42,6 +42,7 @@ export const withRoute = <Props,>(
statusBar?: StatusBarProps;
fullscreen?: boolean;
},
defaultProps?: Partial<Props>,
) => {
const { statusBar, fullscreen, ...routeOptions } = options ?? {};
const WithUseRoute = (props: any) => {
@ -51,7 +52,7 @@ export const withRoute = <Props,>(
{routeOptions && <Stack.Screen {...routeOptions} />}
{statusBar && <StatusBar {...statusBar} />}
{fullscreen && <FullscreenProvider />}
<Component {...routeParams} {...props} />
<Component {...defaultProps} {...routeParams} {...props} />
</>
);
};

View File

@ -0,0 +1,24 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Player } from "@kyoo/ui";
import { withRoute } from "~/router";
export default withRoute(Player, { type: "movie" });

View File

@ -21,4 +21,4 @@
import { Player } from "@kyoo/ui";
import { withRoute } from "~/router";
export default withRoute(Player);
export default withRoute(Player, { type: "episode" });

View File

@ -21,12 +21,12 @@
import { useRouter } from "next/router";
import { ComponentType } from "react";
export const withRoute = <Props,>(Component: ComponentType<Props>) => {
export const withRoute = <Props,>(Component: ComponentType<Props>, defaultProps?: Partial<Props>) => {
const WithUseRoute = (props: Props) => {
const router = useRouter();
// @ts-ignore
return <Component {...router.query} {...props} />;
return <Component {...defaultProps} {...router.query} {...props} />;
};
const { ...all } = Component;

View File

@ -20,8 +20,9 @@
import { z } from "zod";
import { zdate } from "../utils";
import { ImagesP } from "../traits";
import { ImagesP, imageFn } from "../traits";
import { ResourceP } from "../traits/resource";
import { ShowP } from "./show";
const BaseEpisodeP = ResourceP.merge(ImagesP).extend({
/**
@ -54,6 +55,23 @@ const BaseEpisodeP = ResourceP.merge(ImagesP).extend({
* 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),
}),
show: ShowP.optional()
});
export const EpisodeP = BaseEpisodeP.extend({

View File

@ -27,5 +27,5 @@ export * from "./person";
export * from "./studio";
export * from "./episode";
export * from "./season";
export * from "./watch-item";
export * from "./watch-info";
export * from "./user";

View File

@ -20,7 +20,7 @@
import { z } from "zod";
import { zdate } from "../utils";
import { ImagesP, ResourceP } from "../traits";
import { ImagesP, ResourceP, imageFn } from "../traits";
import { Genre } from "./genre";
import { StudioP } from "./studio";
import { Status } from "./show";
@ -67,6 +67,21 @@ export const MovieP = ResourceP.merge(ImagesP).extend({
* The studio that made this movie.
*/
studio: StudioP.optional().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),
}),
});
/**

View File

@ -0,0 +1,118 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { z } from "zod";
import { imageFn } from "../traits";
/**
* A audio or subtitle track.
*/
export const TrackP = z.object({
/**
* The index of this track on the episode.
*/
index: z.number(),
/**
* The title of the stream.
*/
title: z.string().nullable(),
/**
* The language of this stream (as a ISO-639-2 language code)
*/
language: z.string().nullable(),
/**
* The codec of this stream.
*/
codec: z.string(),
/**
* Is this stream the default one of it's type?
*/
isDefault: z.boolean(),
/**
* Is this stream tagged as forced?
*/
isForced: z.boolean(),
});
export type Audio = z.infer<typeof TrackP>;
export const SubtitleP = TrackP.extend({
/*
* The url of this track (only if this is a subtitle)..
*/
link: z.string().transform(imageFn).nullable(),
});
export type Subtitle = z.infer<typeof SubtitleP>;
export const ChapterP = z.object({
/**
* The start time of the chapter (in second from the start of the episode).
*/
startTime: z.number(),
/**
* The end time of the chapter (in second from the start of the episode).
*/
endTime: z.number(),
/**
* The name of this chapter. This should be a human-readable name that could be presented to the
* user. There should be well-known chapters name for commonly used chapters. For example, use
* "Opening" for the introduction-song and "Credits" for the end chapter with credits.
*/
name: z.string(),
});
export type Chapter = z.infer<typeof ChapterP>;
/**
* The transcoder's info for this item. This include subtitles, fonts, chapters...
*/
export const WatchInfoP = z.object({
/**
* The sha1 of the video file.
*/
sha: z.string(),
/**
* The internal path of the video file.
*/
path: z.string(),
/**
* The container of the video file of this episode. Common containers are mp4, mkv, avi and so on.
*/
container: z.string(),
/**
* The list of audio tracks.
*/
audios: z.array(TrackP),
/**
* The list of subtitles tracks.
*/
subtitles: z.array(SubtitleP),
/**
* The list of fonts that can be used to display subtitles.
*/
fonts: z.array(z.string().transform(imageFn)),
/**
* The list of chapters. See Chapter for more information.
*/
chapters: z.array(ChapterP),
});
/**
* A watch info for a video
*/
export type WatchInfo = z.infer<typeof WatchInfoP>;

View File

@ -1,190 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { z } from "zod";
import { zdate } from "../utils";
import { ImagesP, imageFn } from "../traits";
import { EpisodeP } from "./episode";
/**
* A audio or subtitle track.
*/
export const TrackP = z.object({
/**
* The index of this track on the episode.
*/
index: z.number(),
/**
* The title of the stream.
*/
title: z.string().nullable(),
/**
* The language of this stream (as a ISO-639-2 language code)
*/
language: z.string().nullable(),
/**
* The codec of this stream.
*/
codec: z.string(),
/**
* Is this stream the default one of it's type?
*/
isDefault: z.boolean(),
/**
* Is this stream tagged as forced?
*/
isForced: z.boolean(),
});
export type Audio = z.infer<typeof TrackP>;
export const SubtitleP = TrackP.extend({
/*
* The url of this track (only if this is a subtitle)..
*/
link: z.string().transform(imageFn).nullable(),
});
export type Subtitle = z.infer<typeof SubtitleP>;
export const ChapterP = z.object({
/**
* The start time of the chapter (in second from the start of the episode).
*/
startTime: z.number(),
/**
* The end time of the chapter (in second from the start of the episode).
*/
endTime: z.number(),
/**
* The name of this chapter. This should be a human-readable name that could be presented to the
* user. There should be well-known chapters name for commonly used chapters. For example, use
* "Opening" for the introduction-song and "Credits" for the end chapter with credits.
*/
name: z.string(),
});
export type Chapter = z.infer<typeof ChapterP>;
const WatchMovieP = z.preprocess(
(x: any) => {
if (!x) return x;
x.name = x.title;
return x;
},
ImagesP.extend({
/**
* The slug of this episode.
*/
slug: z.string(),
/**
* The title of this episode.
*/
name: z.string().nullable(),
/**
* The sumarry of this episode.
*/
overview: z.string().nullable(),
/**
* The release date of this episode. It can be null if unknown.
*/
releaseDate: zdate().nullable(),
/**
* The transcoder's info for this item. This include subtitles, fonts, chapters...
*/
info: z.object({
/**
* The sha1 of the video file.
*/
sha: z.string(),
/**
* The internal path of the video file.
*/
path: z.string(),
/**
* The container of the video file of this episode. Common containers are mp4, mkv, avi and so
* on.
*/
container: z.string(),
/**
* The list of audio tracks.
*/
audios: z.array(TrackP),
/**
* The list of subtitles tracks.
*/
subtitles: z.array(SubtitleP),
/**
* The list of fonts that can be used to display subtitles.
*/
fonts: z.array(z.string().transform(imageFn)),
/**
* The list of chapters. See Chapter for more information.
*/
chapters: z.array(ChapterP),
}),
/**
* The links to the videos of this watch item.
*/
link: z.object({
direct: z.string().transform(imageFn),
hls: z.string().transform(imageFn),
}),
}),
);
const WatchEpisodeP = WatchMovieP.and(
z.object({
/**
* The ID of the episode associated with this item.
*/
episodeID: z.number(),
/**
* The title of the show containing this episode.
*/
showTitle: z.string(),
/**
* The slug of the show containing this episode
*/
showSlug: z.string(),
/**
* The season in witch this episode is in.
*/
seasonNumber: z.number().nullable(),
/**
* The number of this episode is 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(),
}),
);
export const WatchItemP = z.union([
WatchMovieP.and(z.object({ isMovie: z.literal(true) })),
WatchEpisodeP.and(z.object({ isMovie: z.literal(false) })),
]);
/**
* A watch item for a movie or an episode
*/
export type WatchItem = z.infer<typeof WatchItemP>;

View File

@ -113,7 +113,7 @@ export const IconFab = <AsProps = PressableProps,>(
bg: (theme) => theme.accent,
fover: {
self: {
transform: [{ scale: 1.3 }],
transform: "scale(1.3)" as any,
bg: (theme: Theme) => theme.accent,
},
},

View File

@ -76,6 +76,7 @@ const TitleLine = ({
poster,
studio,
trailerUrl,
type,
...props
}: {
isLoading: boolean;
@ -86,6 +87,7 @@ const TitleLine = ({
poster?: KyooImage | null;
studio?: Studio | null;
trailerUrl?: string | null;
type: "movie" | "show";
} & Stylable) => {
const { css, theme } = useYoshiki();
const { t } = useTranslation();
@ -193,7 +195,7 @@ const TitleLine = ({
<IconFab
icon={PlayArrow}
as={Link}
href={`/watch/${slug}`}
href={type === "show" ? `/watch/${slug}` : `/movie/${slug}/watch`}
color={{ xs: theme.user.colors.black, md: theme.colors.black }}
{...css({
bg: theme.user.accent,
@ -341,7 +343,7 @@ const Description = ({
);
};
export const Header = ({ query, slug }: { query: QueryIdentifier<Show | Movie>; slug: string }) => {
export const Header = ({ query, type, slug }: { query: QueryIdentifier<Show | Movie>; type: "movie" | "show", slug: string }) => {
const { css } = useYoshiki();
return (
@ -365,6 +367,7 @@ export const Header = ({ query, slug }: { query: QueryIdentifier<Show | Movie>;
>
<TitleLine
isLoading={isLoading}
type={type}
slug={slug}
name={data?.name}
tagline={data?.tagline}

View File

@ -48,7 +48,7 @@ export const MovieDetails: QueryPage<{ slug: string }> = ({ slug }) => {
},
)}
>
<Header slug={slug} query={query(slug)} />
<Header slug={slug} type="movie" query={query(slug)} />
{/* <Staff slug={slug} /> */}
</ScrollView>
);

View File

@ -25,8 +25,8 @@ import { DefaultLayout } from "../layout";
import { EpisodeList } from "./season";
import { Header } from "./header";
import Svg, { Path, SvgProps } from "react-native-svg";
import { Container, SwitchVariant } from "@kyoo/primitives";
import { forwardRef, useCallback } from "react";
import { Container } from "@kyoo/primitives";
import { forwardRef } from "react";
const SvgWave = (props: SvgProps) => {
const { css } = useYoshiki();
@ -42,7 +42,7 @@ const SvgWave = (props: SvgProps) => {
);
};
const ShowHeader = forwardRef<View, ViewProps & { slug: string }>(function _ShowHeader(
const ShowHeader = forwardRef<View, ViewProps & { slug: string }>(function ShowHeader(
{ children, slug, ...props },
ref,
) {
@ -69,7 +69,7 @@ const ShowHeader = forwardRef<View, ViewProps & { slug: string }>(function _Show
)}
>
{/* TODO: Remove the slug quickfix for the play button */}
<Header slug={`${slug}-s1e1`} query={query(slug)} />
<Header slug={`${slug}-s1e1`} type="show" query={query(slug)} />
{/* <Staff slug={slug} /> */}
<SvgWave
fill={theme.variant.background}

View File

@ -33,7 +33,7 @@ import {
tooltip,
ts,
} from "@kyoo/primitives";
import { Chapter, Subtitle, WatchItem } from "@kyoo/models";
import { Chapter, KyooImage, Subtitle } from "@kyoo/models";
import { useAtomValue, useSetAtom, useAtom } from "jotai";
import { Platform, Pressable, View, ViewProps } from "react-native";
import { useTranslation } from "react-i18next";
@ -51,7 +51,6 @@ export const Hover = ({
href,
poster,
chapters,
qualities,
subtitles,
fonts,
previousSlug,
@ -66,9 +65,8 @@ export const Hover = ({
name?: string | null;
showName?: string;
href?: string;
poster?: string | null;
poster?: KyooImage | null;
chapters?: Chapter[];
qualities?: WatchItem["link"]
subtitles?: Subtitle[];
fonts?: string[];
previousSlug?: string | null;
@ -77,7 +75,7 @@ export const Hover = ({
onMenuClose: () => void;
show: boolean;
} & ViewProps) => {
// TODO animate show
// TODO: animate show
const opacity = !show && (Platform.OS === "web" ? { opacity: 0 } : { display: "none" as const});
return (
<ContrastArea mode="dark">
@ -126,7 +124,6 @@ export const Hover = ({
<RightButtons
subtitles={subtitles}
fonts={fonts}
qualities={qualities}
onMenuOpen={onMenuOpen}
onMenuClose={onMenuClose}
/>
@ -210,7 +207,7 @@ export const Back = ({
);
};
const VideoPoster = ({ poster }: { poster?: string | null }) => {
const VideoPoster = ({ poster }: { poster?: KyooImage | null }) => {
const { css } = useYoshiki();
return (

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Subtitle, WatchItem } from "@kyoo/models";
import { Subtitle } from "@kyoo/models";
import { IconButton, tooltip, Menu, ts } from "@kyoo/primitives";
import { useAtom } from "jotai";
import { Platform, View } from "react-native";
@ -46,14 +46,12 @@ export const getDisplayName = (sub: Subtitle) => {
export const RightButtons = ({
subtitles,
fonts,
qualities,
onMenuOpen,
onMenuClose,
...props
}: {
subtitles?: Subtitle[];
fonts?: string[];
qualities?: WatchItem["link"];
onMenuOpen: () => void;
onMenuClose: () => void;
} & Stylable) => {

View File

@ -18,10 +18,20 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryIdentifier, QueryPage, WatchItem, WatchItemP, useFetch } from "@kyoo/models";
import {
Episode,
EpisodeP,
Movie,
MovieP,
QueryIdentifier,
QueryPage,
WatchInfo,
WatchInfoP,
useFetch,
} from "@kyoo/models";
import { Head } from "@kyoo/primitives";
import { useState, useEffect, ComponentProps } from "react";
import { Platform, Pressable, PressableProps, StyleSheet, View, PointerEvent as NativePointerEvent } from "react-native";
import { Platform, StyleSheet, View, PointerEvent as NativePointerEvent } from "react-native";
import { useTranslation } from "react-i18next";
import { useRouter } from "solito/router";
import { useAtom } from "jotai";
@ -33,43 +43,47 @@ import { useVideoKeyboard } from "./keyboard";
import { MediaSessionManager } from "./media-session";
import { ErrorView } from "../fetch";
const query = (slug: string): QueryIdentifier<WatchItem> => ({
path: ["watch", slug],
parser: WatchItemP,
type Item = (Movie & { type: "movie" }) | (Episode & { type: "episode" });
const query = (type: string, slug: string): QueryIdentifier<Item> =>
type === "episode"
? {
path: ["episode", slug],
params: {
fields: ["nextEpisode", "previousEpisode", "show"],
},
parser: EpisodeP.transform((x) => ({ ...x, type: "episode" })),
}
: {
path: ["movie", slug],
parser: MovieP.transform((x) => ({ ...x, type: "movie" })),
};
const infoQuery = (type: string, slug: string): QueryIdentifier<WatchInfo> => ({
path: ["video", type, slug, "info"],
parser: WatchInfoP,
});
const mapData = (
data: WatchItem | undefined,
data: Item | undefined,
info: WatchInfo | undefined,
previousSlug?: string,
nextSlug?: string,
): Partial<ComponentProps<typeof Hover>> & { isLoading: boolean } => {
if (!data) return { isLoading: true };
if (!data || !info) return { isLoading: true };
return {
isLoading: false,
name: data.isMovie ? data.name : `${episodeDisplayNumber(data, "")} ${data.name}`,
showName: data.isMovie ? data.name! : data.showTitle,
href: data ? (data.isMovie ? `/movie/${data.slug}` : `/show/${data.showSlug}`) : "#",
name: data.type === "movie" ? data.name : `${episodeDisplayNumber(data, "")} ${data.name}`,
showName: data.type === "movie" ? data.name! : data.show!.name,
href: data ? (data.type === "movie" ? `/movie/${data.slug}` : `/show/${data.show!.slug}`) : "#",
poster: data.poster,
qualities: data.link,
subtitles: data.info.subtitles,
chapters: data.info.chapters,
fonts: data.info.fonts,
subtitles: info.subtitles,
chapters: info.chapters,
fonts: info.fonts,
previousSlug,
nextSlug,
};
};
const PressView =
Platform.OS === "web"
? View
: ({
onPointerDown,
onMobilePress,
...props
}: PressableProps & { onMobilePress: PressableProps["onPress"] }) => (
<Pressable focusable={false} onPress={(e) => onMobilePress?.(e)} {...props} />
);
// Callback used to hide the controls when the mouse goes iddle. This is stored globally to clear the old timeout
// if the mouse moves again (if this is stored as a state, the whole page is redrawn on mouse move)
let mouseCallback: NodeJS.Timeout;
@ -78,21 +92,24 @@ let mouseCallback: NodeJS.Timeout;
let touchCount = 0;
let touchTimeout: NodeJS.Timeout;
export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({ slug, type }) => {
const { css } = useYoshiki();
const { t } = useTranslation();
const router = useRouter();
const [playbackError, setPlaybackError] = useState<string | undefined>(undefined);
const { data, error } = useFetch(query(slug));
const { data, error } = useFetch(query(type, slug));
const { data: info, error: infoError } = useFetch(infoQuery(type, slug));
const previous =
data && !data.isMovie && data.previousEpisode
data && data.type === "episode" && data.previousEpisode
? `/watch/${data.previousEpisode.slug}`
: undefined;
const next =
data && !data.isMovie && data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : undefined;
data && data.type === "episode" && data.nextEpisode
? `/watch/${data.nextEpisode.slug}`
: undefined;
useVideoKeyboard(data?.info.subtitles, data?.info.fonts, previous, next);
useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
const [isPlaying, setPlay] = useAtom(playAtom);
@ -139,17 +156,17 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
setFullscreen(!isFullscreen);
clearTimeout(touchTimeout);
} else
touchTimeout = setTimeout(() => {
touchCount = 0;
}, 400);
touchTimeout = setTimeout(() => {
touchCount = 0;
}, 400);
setPlay(!isPlaying);
};
if (error || playbackError)
if (error || infoError || playbackError)
return (
<>
<Back isLoading={false} {...css({ position: "relative", bg: (theme) => theme.accent })} />
<ErrorView error={error ?? { errors: [playbackError!] }} />
<ErrorView error={error ?? infoError ?? { errors: [playbackError!] }} />
</>
);
@ -158,22 +175,22 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
{data && (
<Head
title={
data.isMovie
data.type === "movie"
? data.name
: data.showTitle +
" " +
episodeDisplayNumber({
seasonNumber: data.seasonNumber,
episodeNumber: data.episodeNumber,
absoluteNumber: data.absoluteNumber,
})
: data.show!.name +
" " +
episodeDisplayNumber({
seasonNumber: data.seasonNumber,
episodeNumber: data.episodeNumber,
absoluteNumber: data.absoluteNumber,
})
}
description={data.overview}
/>
)}
<MediaSessionManager
title={data?.name ?? t("show.episodeNoMetadata")}
image={data?.thumbnail}
image={data?.thumbnail?.high}
next={next}
previous={previous}
/>
@ -190,20 +207,20 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
})}
>
<Video
links={data?.link}
subtitles={data?.info.subtitles}
links={data?.links}
subtitles={info?.subtitles}
setError={setPlaybackError}
fonts={data?.info.fonts}
fonts={info?.fonts}
onPointerDown={(e) => onPointerDown(e)}
onEnd={() => {
if (!data) return;
if (data.isMovie)
if (data.type === "movie")
router.replace(`/movie/${data.slug}`, undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
});
else
router.replace(
data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : `/show/${data.showSlug}`,
data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : `/show/${data.show!.slug}`,
undefined,
{ experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false } },
);
@ -212,7 +229,7 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
/>
<LoadingIndicator />
<Hover
{...mapData(data, previous, next)}
{...mapData(data, info, previous, next)}
onPointerEnter={(e) => {
if (Platform.OS !== "web" || e.nativeEvent.pointerType === "mouse") setHover(true);
}}
@ -238,4 +255,4 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
);
};
Player.getFetchUrls = ({ slug }) => [query(slug)];
Player.getFetchUrls = ({ slug, type }) => [query(type, slug), infoQuery(type, slug)];

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Subtitle, WatchItem } from "@kyoo/models";
import { Episode, Subtitle } from "@kyoo/models";
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { ElementRef, memo, useEffect, useLayoutEffect, useRef, useState } from "react";
import NativeVideo, { VideoProperties as VideoProps } from "./video";
@ -74,14 +74,14 @@ const privateFullscreen = atom(false);
export const subtitleAtom = atom<Subtitle | null>(null);
export const Video = memo(function _Video({
export const Video = memo(function Video({
links,
subtitles,
setError,
fonts,
...props
}: {
links?: WatchItem["link"];
links?: Episode["links"];
subtitles?: Subtitle[];
setError: (error: string | undefined) => void;
fonts?: string[];

View File

@ -55,7 +55,7 @@ const audioAtom = atom(0);
const clientId = uuid.v4() as string;
const Video = forwardRef<NativeVideo, VideoProps>(function _NativeVideo(
const Video = forwardRef<NativeVideo, VideoProps>(function Video(
{ onLoad, source, onPointerDown, subtitles, ...props },
ref,
) {

View File

@ -67,7 +67,7 @@ const initHls = async (): Promise<Hls> => {
return hls;
};
const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function _Video(
const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function Video(
{
source,
paused,
@ -140,7 +140,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
if (!d.fatal || !hls?.media) return;
console.warn("Hls error", d);
onError?.call(null, {
error: { "": "", errorString: d.reason ?? d.err?.message ?? "Unknown hls error" },
error: { "": "", errorString: d.reason ?? d.error?.message ?? "Unknown hls error" },
});
});
}

View File

@ -24,10 +24,15 @@ pub struct MediaInfo {
pub length: f32,
/// The container of the video file of this episode.
pub container: String,
/// The video codec and infromations.
pub video: Video,
/// The list of audio tracks.
pub audios: Vec<Audio>,
/// The list of subtitles tracks.
pub subtitles: Vec<Subtitle>,
/// The list of fonts that can be used to display subtitles.
pub fonts: Vec<String>,
/// The list of chapters. See Chapter for more information.
pub chapters: Vec<Chapter>,
}

View File

@ -14,6 +14,7 @@ use crate::{
identify::{identify, Audio, Chapter, MediaInfo, Subtitle, Video},
state::Transcoder,
video::*,
transcode::Quality
};
mod audio;
mod error;
@ -179,7 +180,7 @@ async fn get_swagger() -> String {
get_attachment,
get_subtitle,
),
components(schemas(MediaInfo, Video, Audio, Subtitle, Chapter))
components(schemas(MediaInfo, Video, Audio, Subtitle, Chapter, Quality))
)]
struct ApiDoc;

View File

@ -5,7 +5,7 @@ struct Item {
path: String,
}
pub async fn get_path(_resource: String, slug: String) -> Result<String, reqwest::Error> {
pub async fn get_path(resource: String, slug: String) -> Result<String, reqwest::Error> {
let api_url = std::env::var("API_URL").unwrap_or("http://back:5000".to_string());
let api_key = std::env::var("KYOO_APIKEYS")
.expect("Missing api keys.")
@ -16,9 +16,8 @@ pub async fn get_path(_resource: String, slug: String) -> Result<String, reqwest
// TODO: Store the client somewhere gobal
let client = reqwest::Client::new();
// TODO: The api create dummy episodes for movies right now so we hard code the /episode/
client
.get(format!("{api_url}/episode/{slug}"))
.get(format!("{api_url}/{resource}/{slug}"))
.header("X-API-KEY", api_key)
.send()
.await?

View File

@ -2,6 +2,7 @@ use derive_more::Display;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use serde::Serialize;
use utoipa::ToSchema;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
@ -20,7 +21,7 @@ pub enum TranscodeError {
ArgumentError(String),
}
#[derive(PartialEq, Eq, Serialize, Display, Clone, Copy)]
#[derive(PartialEq, Eq, Serialize, Display, Clone, Copy, ToSchema)]
pub enum Quality {
#[display(fmt = "240p")]
P240,