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 />
|
/// <inheritdoc />
|
||||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
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() { }
|
public LibraryItem() { }
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
|
@ -163,7 +163,7 @@ namespace Kyoo.Abstractions.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Links to watch this episode.
|
/// Links to watch this episode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object Links => new
|
public VideoLinks Links => new()
|
||||||
{
|
{
|
||||||
Direct = $"/video/episode/{Slug}/direct",
|
Direct = $"/video/episode/{Slug}/direct",
|
||||||
Hls = $"/video/episode/{Slug}/master.m3u8",
|
Hls = $"/video/episode/{Slug}/master.m3u8",
|
||||||
|
@ -124,7 +124,7 @@ namespace Kyoo.Abstractions.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Links to watch this movie.
|
/// Links to watch this movie.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object Links => new
|
public VideoLinks Links => new()
|
||||||
{
|
{
|
||||||
Direct = $"/video/movie/{Slug}/direct",
|
Direct = $"/video/movie/{Slug}/direct",
|
||||||
Hls = $"/video/movie/{Slug}/master.m3u8",
|
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>()
|
modelBuilder.Entity<User>()
|
||||||
.HasIndex(x => x.Slug)
|
.HasIndex(x => x.Slug)
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
modelBuilder.Entity<Movie>()
|
||||||
|
.Ignore(x => x.Links);
|
||||||
|
modelBuilder.Entity<LibraryItem>()
|
||||||
|
.Ignore(x => x.Links);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -67,7 +67,7 @@ namespace Kyoo.Swagger
|
|||||||
Kind? kind = controller.Type == null
|
Kind? kind = controller.Type == null
|
||||||
? controller.Kind
|
? controller.Kind
|
||||||
: cur.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()}");
|
permissions.Add($"{type}.{kind!.Value.ToString().ToLower()}");
|
||||||
agg[nameof(Kyoo)] = permissions;
|
agg[nameof(Kyoo)] = permissions;
|
||||||
return agg;
|
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 { Player } from "@kyoo/ui";
|
||||||
import { withRoute } from "../../utils";
|
import { withRoute } from "../../utils";
|
||||||
|
|
||||||
export default withRoute(Player, {
|
export default withRoute(
|
||||||
|
Player,
|
||||||
|
{
|
||||||
options: {
|
options: {
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
},
|
},
|
||||||
statusBar: { hidden: true },
|
statusBar: { hidden: true },
|
||||||
fullscreen: true,
|
fullscreen: true,
|
||||||
});
|
},
|
||||||
|
{ type: "episode" },
|
||||||
|
);
|
||||||
|
@ -42,6 +42,7 @@ export const withRoute = <Props,>(
|
|||||||
statusBar?: StatusBarProps;
|
statusBar?: StatusBarProps;
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean;
|
||||||
},
|
},
|
||||||
|
defaultProps?: Partial<Props>,
|
||||||
) => {
|
) => {
|
||||||
const { statusBar, fullscreen, ...routeOptions } = options ?? {};
|
const { statusBar, fullscreen, ...routeOptions } = options ?? {};
|
||||||
const WithUseRoute = (props: any) => {
|
const WithUseRoute = (props: any) => {
|
||||||
@ -51,7 +52,7 @@ export const withRoute = <Props,>(
|
|||||||
{routeOptions && <Stack.Screen {...routeOptions} />}
|
{routeOptions && <Stack.Screen {...routeOptions} />}
|
||||||
{statusBar && <StatusBar {...statusBar} />}
|
{statusBar && <StatusBar {...statusBar} />}
|
||||||
{fullscreen && <FullscreenProvider />}
|
{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 { Player } from "@kyoo/ui";
|
||||||
import { withRoute } from "~/router";
|
import { withRoute } from "~/router";
|
||||||
|
|
||||||
export default withRoute(Player);
|
export default withRoute(Player, { type: "episode" });
|
||||||
|
@ -21,12 +21,12 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ComponentType } from "react";
|
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 WithUseRoute = (props: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return <Component {...router.query} {...props} />;
|
return <Component {...defaultProps} {...router.query} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { ...all } = Component;
|
const { ...all } = Component;
|
||||||
|
@ -20,8 +20,9 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zdate } from "../utils";
|
import { zdate } from "../utils";
|
||||||
import { ImagesP } from "../traits";
|
import { ImagesP, imageFn } from "../traits";
|
||||||
import { ResourceP } from "../traits/resource";
|
import { ResourceP } from "../traits/resource";
|
||||||
|
import { ShowP } from "./show";
|
||||||
|
|
||||||
const BaseEpisodeP = ResourceP.merge(ImagesP).extend({
|
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.
|
* The release date of this episode. It can be null if unknown.
|
||||||
*/
|
*/
|
||||||
releaseDate: zdate().nullable(),
|
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({
|
export const EpisodeP = BaseEpisodeP.extend({
|
||||||
|
@ -27,5 +27,5 @@ export * from "./person";
|
|||||||
export * from "./studio";
|
export * from "./studio";
|
||||||
export * from "./episode";
|
export * from "./episode";
|
||||||
export * from "./season";
|
export * from "./season";
|
||||||
export * from "./watch-item";
|
export * from "./watch-info";
|
||||||
export * from "./user";
|
export * from "./user";
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zdate } from "../utils";
|
import { zdate } from "../utils";
|
||||||
import { ImagesP, ResourceP } from "../traits";
|
import { ImagesP, ResourceP, imageFn } from "../traits";
|
||||||
import { Genre } from "./genre";
|
import { Genre } from "./genre";
|
||||||
import { StudioP } from "./studio";
|
import { StudioP } from "./studio";
|
||||||
import { Status } from "./show";
|
import { Status } from "./show";
|
||||||
@ -67,6 +67,21 @@ export const MovieP = ResourceP.merge(ImagesP).extend({
|
|||||||
* The studio that made this movie.
|
* The studio that made this movie.
|
||||||
*/
|
*/
|
||||||
studio: StudioP.optional().nullable(),
|
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,
|
bg: (theme) => theme.accent,
|
||||||
fover: {
|
fover: {
|
||||||
self: {
|
self: {
|
||||||
transform: [{ scale: 1.3 }],
|
transform: "scale(1.3)" as any,
|
||||||
bg: (theme: Theme) => theme.accent,
|
bg: (theme: Theme) => theme.accent,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -76,6 +76,7 @@ const TitleLine = ({
|
|||||||
poster,
|
poster,
|
||||||
studio,
|
studio,
|
||||||
trailerUrl,
|
trailerUrl,
|
||||||
|
type,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@ -86,6 +87,7 @@ const TitleLine = ({
|
|||||||
poster?: KyooImage | null;
|
poster?: KyooImage | null;
|
||||||
studio?: Studio | null;
|
studio?: Studio | null;
|
||||||
trailerUrl?: string | null;
|
trailerUrl?: string | null;
|
||||||
|
type: "movie" | "show";
|
||||||
} & Stylable) => {
|
} & Stylable) => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -193,7 +195,7 @@ const TitleLine = ({
|
|||||||
<IconFab
|
<IconFab
|
||||||
icon={PlayArrow}
|
icon={PlayArrow}
|
||||||
as={Link}
|
as={Link}
|
||||||
href={`/watch/${slug}`}
|
href={type === "show" ? `/watch/${slug}` : `/movie/${slug}/watch`}
|
||||||
color={{ xs: theme.user.colors.black, md: theme.colors.black }}
|
color={{ xs: theme.user.colors.black, md: theme.colors.black }}
|
||||||
{...css({
|
{...css({
|
||||||
bg: theme.user.accent,
|
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();
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -365,6 +367,7 @@ export const Header = ({ query, slug }: { query: QueryIdentifier<Show | Movie>;
|
|||||||
>
|
>
|
||||||
<TitleLine
|
<TitleLine
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
type={type}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
name={data?.name}
|
name={data?.name}
|
||||||
tagline={data?.tagline}
|
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} /> */}
|
{/* <Staff slug={slug} /> */}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
@ -25,8 +25,8 @@ import { DefaultLayout } from "../layout";
|
|||||||
import { EpisodeList } from "./season";
|
import { EpisodeList } from "./season";
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
import Svg, { Path, SvgProps } from "react-native-svg";
|
import Svg, { Path, SvgProps } from "react-native-svg";
|
||||||
import { Container, SwitchVariant } from "@kyoo/primitives";
|
import { Container } from "@kyoo/primitives";
|
||||||
import { forwardRef, useCallback } from "react";
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
const SvgWave = (props: SvgProps) => {
|
const SvgWave = (props: SvgProps) => {
|
||||||
const { css } = useYoshiki();
|
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 },
|
{ children, slug, ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
@ -69,7 +69,7 @@ const ShowHeader = forwardRef<View, ViewProps & { slug: string }>(function _Show
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* TODO: Remove the slug quickfix for the play button */}
|
{/* 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} /> */}
|
{/* <Staff slug={slug} /> */}
|
||||||
<SvgWave
|
<SvgWave
|
||||||
fill={theme.variant.background}
|
fill={theme.variant.background}
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
tooltip,
|
tooltip,
|
||||||
ts,
|
ts,
|
||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
import { Chapter, Subtitle, WatchItem } from "@kyoo/models";
|
import { Chapter, KyooImage, Subtitle } from "@kyoo/models";
|
||||||
import { useAtomValue, useSetAtom, useAtom } from "jotai";
|
import { useAtomValue, useSetAtom, useAtom } from "jotai";
|
||||||
import { Platform, Pressable, View, ViewProps } from "react-native";
|
import { Platform, Pressable, View, ViewProps } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -51,7 +51,6 @@ export const Hover = ({
|
|||||||
href,
|
href,
|
||||||
poster,
|
poster,
|
||||||
chapters,
|
chapters,
|
||||||
qualities,
|
|
||||||
subtitles,
|
subtitles,
|
||||||
fonts,
|
fonts,
|
||||||
previousSlug,
|
previousSlug,
|
||||||
@ -66,9 +65,8 @@ export const Hover = ({
|
|||||||
name?: string | null;
|
name?: string | null;
|
||||||
showName?: string;
|
showName?: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
poster?: string | null;
|
poster?: KyooImage | null;
|
||||||
chapters?: Chapter[];
|
chapters?: Chapter[];
|
||||||
qualities?: WatchItem["link"]
|
|
||||||
subtitles?: Subtitle[];
|
subtitles?: Subtitle[];
|
||||||
fonts?: string[];
|
fonts?: string[];
|
||||||
previousSlug?: string | null;
|
previousSlug?: string | null;
|
||||||
@ -77,7 +75,7 @@ export const Hover = ({
|
|||||||
onMenuClose: () => void;
|
onMenuClose: () => void;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
} & ViewProps) => {
|
} & ViewProps) => {
|
||||||
// TODO animate show
|
// TODO: animate show
|
||||||
const opacity = !show && (Platform.OS === "web" ? { opacity: 0 } : { display: "none" as const});
|
const opacity = !show && (Platform.OS === "web" ? { opacity: 0 } : { display: "none" as const});
|
||||||
return (
|
return (
|
||||||
<ContrastArea mode="dark">
|
<ContrastArea mode="dark">
|
||||||
@ -126,7 +124,6 @@ export const Hover = ({
|
|||||||
<RightButtons
|
<RightButtons
|
||||||
subtitles={subtitles}
|
subtitles={subtitles}
|
||||||
fonts={fonts}
|
fonts={fonts}
|
||||||
qualities={qualities}
|
|
||||||
onMenuOpen={onMenuOpen}
|
onMenuOpen={onMenuOpen}
|
||||||
onMenuClose={onMenuClose}
|
onMenuClose={onMenuClose}
|
||||||
/>
|
/>
|
||||||
@ -210,7 +207,7 @@ export const Back = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const VideoPoster = ({ poster }: { poster?: string | null }) => {
|
const VideoPoster = ({ poster }: { poster?: KyooImage | null }) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* 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 { IconButton, tooltip, Menu, ts } from "@kyoo/primitives";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
@ -46,14 +46,12 @@ export const getDisplayName = (sub: Subtitle) => {
|
|||||||
export const RightButtons = ({
|
export const RightButtons = ({
|
||||||
subtitles,
|
subtitles,
|
||||||
fonts,
|
fonts,
|
||||||
qualities,
|
|
||||||
onMenuOpen,
|
onMenuOpen,
|
||||||
onMenuClose,
|
onMenuClose,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
subtitles?: Subtitle[];
|
subtitles?: Subtitle[];
|
||||||
fonts?: string[];
|
fonts?: string[];
|
||||||
qualities?: WatchItem["link"];
|
|
||||||
onMenuOpen: () => void;
|
onMenuOpen: () => void;
|
||||||
onMenuClose: () => void;
|
onMenuClose: () => void;
|
||||||
} & Stylable) => {
|
} & Stylable) => {
|
||||||
|
@ -18,10 +18,20 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* 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 { Head } from "@kyoo/primitives";
|
||||||
import { useState, useEffect, ComponentProps } from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { useRouter } from "solito/router";
|
import { useRouter } from "solito/router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
@ -33,43 +43,47 @@ import { useVideoKeyboard } from "./keyboard";
|
|||||||
import { MediaSessionManager } from "./media-session";
|
import { MediaSessionManager } from "./media-session";
|
||||||
import { ErrorView } from "../fetch";
|
import { ErrorView } from "../fetch";
|
||||||
|
|
||||||
const query = (slug: string): QueryIdentifier<WatchItem> => ({
|
type Item = (Movie & { type: "movie" }) | (Episode & { type: "episode" });
|
||||||
path: ["watch", slug],
|
|
||||||
parser: WatchItemP,
|
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 = (
|
const mapData = (
|
||||||
data: WatchItem | undefined,
|
data: Item | undefined,
|
||||||
|
info: WatchInfo | undefined,
|
||||||
previousSlug?: string,
|
previousSlug?: string,
|
||||||
nextSlug?: string,
|
nextSlug?: string,
|
||||||
): Partial<ComponentProps<typeof Hover>> & { isLoading: boolean } => {
|
): Partial<ComponentProps<typeof Hover>> & { isLoading: boolean } => {
|
||||||
if (!data) return { isLoading: true };
|
if (!data || !info) return { isLoading: true };
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
name: data.isMovie ? data.name : `${episodeDisplayNumber(data, "")} ${data.name}`,
|
name: data.type === "movie" ? data.name : `${episodeDisplayNumber(data, "")} ${data.name}`,
|
||||||
showName: data.isMovie ? data.name! : data.showTitle,
|
showName: data.type === "movie" ? data.name! : data.show!.name,
|
||||||
href: data ? (data.isMovie ? `/movie/${data.slug}` : `/show/${data.showSlug}`) : "#",
|
href: data ? (data.type === "movie" ? `/movie/${data.slug}` : `/show/${data.show!.slug}`) : "#",
|
||||||
poster: data.poster,
|
poster: data.poster,
|
||||||
qualities: data.link,
|
subtitles: info.subtitles,
|
||||||
subtitles: data.info.subtitles,
|
chapters: info.chapters,
|
||||||
chapters: data.info.chapters,
|
fonts: info.fonts,
|
||||||
fonts: data.info.fonts,
|
|
||||||
previousSlug,
|
previousSlug,
|
||||||
nextSlug,
|
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
|
// 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)
|
// if the mouse moves again (if this is stored as a state, the whole page is redrawn on mouse move)
|
||||||
let mouseCallback: NodeJS.Timeout;
|
let mouseCallback: NodeJS.Timeout;
|
||||||
@ -78,21 +92,24 @@ let mouseCallback: NodeJS.Timeout;
|
|||||||
let touchCount = 0;
|
let touchCount = 0;
|
||||||
let touchTimeout: NodeJS.Timeout;
|
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 { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [playbackError, setPlaybackError] = useState<string | undefined>(undefined);
|
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 =
|
const previous =
|
||||||
data && !data.isMovie && data.previousEpisode
|
data && data.type === "episode" && data.previousEpisode
|
||||||
? `/watch/${data.previousEpisode.slug}`
|
? `/watch/${data.previousEpisode.slug}`
|
||||||
: undefined;
|
: undefined;
|
||||||
const next =
|
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 [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
||||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||||
@ -145,11 +162,11 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
setPlay(!isPlaying);
|
setPlay(!isPlaying);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (error || playbackError)
|
if (error || infoError || playbackError)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Back isLoading={false} {...css({ position: "relative", bg: (theme) => theme.accent })} />
|
<Back isLoading={false} {...css({ position: "relative", bg: (theme) => theme.accent })} />
|
||||||
<ErrorView error={error ?? { errors: [playbackError!] }} />
|
<ErrorView error={error ?? infoError ?? { errors: [playbackError!] }} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -158,9 +175,9 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
{data && (
|
{data && (
|
||||||
<Head
|
<Head
|
||||||
title={
|
title={
|
||||||
data.isMovie
|
data.type === "movie"
|
||||||
? data.name
|
? data.name
|
||||||
: data.showTitle +
|
: data.show!.name +
|
||||||
" " +
|
" " +
|
||||||
episodeDisplayNumber({
|
episodeDisplayNumber({
|
||||||
seasonNumber: data.seasonNumber,
|
seasonNumber: data.seasonNumber,
|
||||||
@ -173,7 +190,7 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
)}
|
)}
|
||||||
<MediaSessionManager
|
<MediaSessionManager
|
||||||
title={data?.name ?? t("show.episodeNoMetadata")}
|
title={data?.name ?? t("show.episodeNoMetadata")}
|
||||||
image={data?.thumbnail}
|
image={data?.thumbnail?.high}
|
||||||
next={next}
|
next={next}
|
||||||
previous={previous}
|
previous={previous}
|
||||||
/>
|
/>
|
||||||
@ -190,20 +207,20 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Video
|
<Video
|
||||||
links={data?.link}
|
links={data?.links}
|
||||||
subtitles={data?.info.subtitles}
|
subtitles={info?.subtitles}
|
||||||
setError={setPlaybackError}
|
setError={setPlaybackError}
|
||||||
fonts={data?.info.fonts}
|
fonts={info?.fonts}
|
||||||
onPointerDown={(e) => onPointerDown(e)}
|
onPointerDown={(e) => onPointerDown(e)}
|
||||||
onEnd={() => {
|
onEnd={() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
if (data.isMovie)
|
if (data.type === "movie")
|
||||||
router.replace(`/movie/${data.slug}`, undefined, {
|
router.replace(`/movie/${data.slug}`, undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
router.replace(
|
router.replace(
|
||||||
data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : `/show/${data.showSlug}`,
|
data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : `/show/${data.show!.slug}`,
|
||||||
undefined,
|
undefined,
|
||||||
{ experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false } },
|
{ experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false } },
|
||||||
);
|
);
|
||||||
@ -212,7 +229,7 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
/>
|
/>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
<Hover
|
<Hover
|
||||||
{...mapData(data, previous, next)}
|
{...mapData(data, info, previous, next)}
|
||||||
onPointerEnter={(e) => {
|
onPointerEnter={(e) => {
|
||||||
if (Platform.OS !== "web" || e.nativeEvent.pointerType === "mouse") setHover(true);
|
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/>.
|
* 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 { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { ElementRef, memo, useEffect, useLayoutEffect, useRef, useState } from "react";
|
import { ElementRef, memo, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import NativeVideo, { VideoProperties as VideoProps } from "./video";
|
import NativeVideo, { VideoProperties as VideoProps } from "./video";
|
||||||
@ -74,14 +74,14 @@ const privateFullscreen = atom(false);
|
|||||||
|
|
||||||
export const subtitleAtom = atom<Subtitle | null>(null);
|
export const subtitleAtom = atom<Subtitle | null>(null);
|
||||||
|
|
||||||
export const Video = memo(function _Video({
|
export const Video = memo(function Video({
|
||||||
links,
|
links,
|
||||||
subtitles,
|
subtitles,
|
||||||
setError,
|
setError,
|
||||||
fonts,
|
fonts,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
links?: WatchItem["link"];
|
links?: Episode["links"];
|
||||||
subtitles?: Subtitle[];
|
subtitles?: Subtitle[];
|
||||||
setError: (error: string | undefined) => void;
|
setError: (error: string | undefined) => void;
|
||||||
fonts?: string[];
|
fonts?: string[];
|
||||||
|
@ -55,7 +55,7 @@ const audioAtom = atom(0);
|
|||||||
|
|
||||||
const clientId = uuid.v4() as string;
|
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 },
|
{ onLoad, source, onPointerDown, subtitles, ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
|
@ -67,7 +67,7 @@ const initHls = async (): Promise<Hls> => {
|
|||||||
return hls;
|
return hls;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function _Video(
|
const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function Video(
|
||||||
{
|
{
|
||||||
source,
|
source,
|
||||||
paused,
|
paused,
|
||||||
@ -140,7 +140,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
if (!d.fatal || !hls?.media) return;
|
if (!d.fatal || !hls?.media) return;
|
||||||
console.warn("Hls error", d);
|
console.warn("Hls error", d);
|
||||||
onError?.call(null, {
|
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,
|
pub length: f32,
|
||||||
/// The container of the video file of this episode.
|
/// The container of the video file of this episode.
|
||||||
pub container: String,
|
pub container: String,
|
||||||
|
/// The video codec and infromations.
|
||||||
pub video: Video,
|
pub video: Video,
|
||||||
|
/// The list of audio tracks.
|
||||||
pub audios: Vec<Audio>,
|
pub audios: Vec<Audio>,
|
||||||
|
/// The list of subtitles tracks.
|
||||||
pub subtitles: Vec<Subtitle>,
|
pub subtitles: Vec<Subtitle>,
|
||||||
|
/// The list of fonts that can be used to display subtitles.
|
||||||
pub fonts: Vec<String>,
|
pub fonts: Vec<String>,
|
||||||
|
/// The list of chapters. See Chapter for more information.
|
||||||
pub chapters: Vec<Chapter>,
|
pub chapters: Vec<Chapter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ use crate::{
|
|||||||
identify::{identify, Audio, Chapter, MediaInfo, Subtitle, Video},
|
identify::{identify, Audio, Chapter, MediaInfo, Subtitle, Video},
|
||||||
state::Transcoder,
|
state::Transcoder,
|
||||||
video::*,
|
video::*,
|
||||||
|
transcode::Quality
|
||||||
};
|
};
|
||||||
mod audio;
|
mod audio;
|
||||||
mod error;
|
mod error;
|
||||||
@ -179,7 +180,7 @@ async fn get_swagger() -> String {
|
|||||||
get_attachment,
|
get_attachment,
|
||||||
get_subtitle,
|
get_subtitle,
|
||||||
),
|
),
|
||||||
components(schemas(MediaInfo, Video, Audio, Subtitle, Chapter))
|
components(schemas(MediaInfo, Video, Audio, Subtitle, Chapter, Quality))
|
||||||
)]
|
)]
|
||||||
struct ApiDoc;
|
struct ApiDoc;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ struct Item {
|
|||||||
path: String,
|
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_url = std::env::var("API_URL").unwrap_or("http://back:5000".to_string());
|
||||||
let api_key = std::env::var("KYOO_APIKEYS")
|
let api_key = std::env::var("KYOO_APIKEYS")
|
||||||
.expect("Missing api keys.")
|
.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
|
// TODO: Store the client somewhere gobal
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
// TODO: The api create dummy episodes for movies right now so we hard code the /episode/
|
|
||||||
client
|
client
|
||||||
.get(format!("{api_url}/episode/{slug}"))
|
.get(format!("{api_url}/{resource}/{slug}"))
|
||||||
.header("X-API-KEY", api_key)
|
.header("X-API-KEY", api_key)
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
|
@ -2,6 +2,7 @@ use derive_more::Display;
|
|||||||
use rand::distributions::Alphanumeric;
|
use rand::distributions::Alphanumeric;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@ -20,7 +21,7 @@ pub enum TranscodeError {
|
|||||||
ArgumentError(String),
|
ArgumentError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Serialize, Display, Clone, Copy)]
|
#[derive(PartialEq, Eq, Serialize, Display, Clone, Copy, ToSchema)]
|
||||||
pub enum Quality {
|
pub enum Quality {
|
||||||
#[display(fmt = "240p")]
|
#[display(fmt = "240p")]
|
||||||
P240,
|
P240,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user