mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Update player to use new api
This commit is contained in:
parent
25418071fe
commit
5ddfe1ddb2
@ -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]
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
36
back/src/Kyoo.Abstractions/Models/VideoLinks.cs
Normal file
36
back/src/Kyoo.Abstractions/Models/VideoLinks.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
|
34
front/apps/mobile/app/movie/[slug]/watch.tsx
Normal file
34
front/apps/mobile/app/movie/[slug]/watch.tsx
Normal 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" },
|
||||
);
|
@ -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" },
|
||||
);
|
||||
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
24
front/apps/web/src/pages/movie/[slug]/watch.tsx
Normal file
24
front/apps/web/src/pages/movie/[slug]/watch.tsx
Normal 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" });
|
@ -21,4 +21,4 @@
|
||||
import { Player } from "@kyoo/ui";
|
||||
import { withRoute } from "~/router";
|
||||
|
||||
export default withRoute(Player);
|
||||
export default withRoute(Player, { type: "episode" });
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -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";
|
||||
|
@ -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),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
|
118
front/packages/models/src/resources/watch-info.ts
Normal file
118
front/packages/models/src/resources/watch-info.ts
Normal 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>;
|
@ -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>;
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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 (
|
||||
|
@ -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) => {
|
||||
|
@ -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)];
|
||||
|
@ -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[];
|
||||
|
@ -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,
|
||||
) {
|
||||
|
@ -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" },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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>,
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user