diff --git a/front/src/models/index.ts b/front/src/models/index.ts index 329bba61..08400360 100644 --- a/front/src/models/index.ts +++ b/front/src/models/index.ts @@ -12,3 +12,4 @@ export * from "./utils/genre"; export * from "./utils/images"; export * from "./utils/page"; export * from "./video"; +export * from "./video-info"; diff --git a/front/src/models/resources/quality.ts b/front/src/models/resources/quality.ts deleted file mode 100644 index 305688a8..00000000 --- a/front/src/models/resources/quality.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from "zod"; - -export const QualityP = z - .union([ - z.literal("original"), - z.literal("8k"), - z.literal("4k"), - z.literal("1440p"), - z.literal("1080p"), - z.literal("720p"), - z.literal("480p"), - z.literal("360p"), - z.literal("240p"), - ]) - .default("original"); - -/** - * A Video Quality Enum. - */ -export type Quality = z.infer; diff --git a/front/src/models/resources/watch-info.ts b/front/src/models/resources/watch-info.ts deleted file mode 100644 index 8c93f813..00000000 --- a/front/src/models/resources/watch-info.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { z } from "zod"; -import { QualityP } from "./quality"; - -/** - * A audio or subtitle track. - */ -export const TrackP = z.object({ - /** - * The index of this track on the episode. - * NOTE: external subtitles can have a null index - */ - index: z.number().nullable(), - /** - * 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? - * NOTE: not available for videos - */ - isForced: z.boolean().optional(), -}); -export type Track = z.infer; - -/** - * A Video track - */ -export const VideoP = TrackP.extend({ - /** - * The Quality of the Video - * E.g. "1080p" - */ - quality: QualityP, - /** - * The Width of the Video Frame - * E.g. 1424 - */ - width: z.number(), - /** - * The Height of the Video Frame - * E.g. 1072 - */ - height: z.number(), - /** - * The Bitrate (in bits/seconds) of the video track - * E.g. 2693245 - */ - bitrate: z.number(), -}); - -export type Video = z.infer; - -export const AudioP = TrackP; -export type Audio = z.infer; - -export const SubtitleP = TrackP.extend({ - /* - * The url of this track (only if this is a subtitle).. - */ - link: z.string().nullable(), - /* - * Is this an external subtitle (as in stored in a different file) - */ - isExternal: z.boolean(), - /** - * Is this a hearing impaired subtitle? - */ - isHearingImpaired: z.boolean(), -}); -export type Subtitle = z.infer; - -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; - -/** - * 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 extension used to store this video file. - */ - extension: z.string(), - /** - * The whole mimetype (defined as the RFC 6381). - * ex: `video/mp4; codecs="avc1.640028, mp4a.40.2"` - */ - mimeCodec: z.string(), - /** - * The file size of the video file. - */ - size: z.number(), - /** - * The duration of the video (in seconds). - */ - duration: z.number(), - /** - * The container of the video file of this episode. Common containers are mp4, mkv, avi and so on. - */ - container: z.string().nullable(), - /** - * The video track. - */ - videos: z.array(VideoP), - /** - * The list of audio tracks. - */ - audios: z.array(AudioP), - /** - * 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()), - /** - * The list of chapters. See Chapter for more information. - */ - chapters: z.array(ChapterP), - }) - .transform((x) => { - const hour = Math.floor(x.duration / 3600); - const minutes = Math.ceil((x.duration % 3600) / 60); - - return { - ...x, - duration: `${hour ? `${hour}h` : ""}${minutes}m`, - durationSeconds: x.duration, - size: humanFileSize(x.size), - }; - }); - -// from https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string -const humanFileSize = (size: number): string => { - const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); - return ( - // @ts-ignore I'm not gonna fix stackoverflow's working code. - // biome-ignore lint/style/useTemplate: same as above - (size / 1024 ** i).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i] - ); -}; - -/** - * A watch info for a video - */ -export type WatchInfo = z.infer; diff --git a/front/src/models/video-info.ts b/front/src/models/video-info.ts new file mode 100644 index 00000000..41e338e6 --- /dev/null +++ b/front/src/models/video-info.ts @@ -0,0 +1,108 @@ +import { z } from "zod/v4"; + +export const Quality = z + .enum([ + "original", + "8k", + "4k", + "1440p", + "1080p", + "720p", + "480p", + "360p", + "240p", + ]) + .default("original"); +export type Quality = z.infer; + +export const Video = z.object({ + index: z.number(), + title: z.string().nullable(), + language: z.string().nullable(), + codec: z.string(), + mimeCodec: z.string(), + width: z.number(), + height: z.number(), + bitrate: z.number(), + isDefault: z.boolean(), +}); + +export type Video = z.infer; + +export const Audio = z.object({ + index: z.number(), + title: z.string().nullable(), + language: z.string().nullable(), + codec: z.string(), + mimeCodec: z.string(), + bitrate: z.number(), + isDefault: z.boolean(), +}); +export type Audio = z.infer; + +export const Subtitle = z.object({ + // external subtitles don't have indexes + index: z.number().nullable(), + title: z.string().nullable(), + language: z.string().nullable(), + codec: z.string(), + mimeCodec: z.string(), + extension: z.string(), + isDefault: z.boolean(), + isForced: z.boolean(), + isHearingImpaired: z.boolean(), + isExternal: z.boolean(), + // only non-null when `isExternal` is true + path: z.string().nullable(), + link: z.string().nullable(), +}); +export type Subtitle = z.infer; + +export const Chapter = z.object({ + // in seconds + startTime: z.number(), + // in seconds + endTime: z.number(), + name: z.string(), + type: z.enum(["content", "recap", "intro", "credits", "preview"]), +}); +export type Chapter = z.infer; + +export const VideoInfo = z + .object({ + sha: z.string(), + path: z.string(), + extension: z.string(), + mimeCodec: z.string(), + size: z.number(), + // in seconds + duration: z.number(), + container: z.string().nullable(), + videos: z.array(Video), + audios: z.array(Audio), + subtitles: z.array(Subtitle), + fonts: z.array(z.string()), + chapters: z.array(Chapter), + }) + .transform((x) => { + const hour = Math.floor(x.duration / 3600); + const minutes = Math.ceil((x.duration % 3600) / 60); + + return { + ...x, + duration: `${hour ? `${hour}h` : ""}${minutes}m`, + durationSeconds: x.duration, + size: humanFileSize(x.size), + }; + }); +export type VideoInfo = z.infer; + +// from https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string +const humanFileSize = (size: number): string => { + const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); + return ( + // @ts-ignore I'm not gonna fix stackoverflow's working code. + // biome-ignore lint/style/useTemplate: same as above + (size / 1024 ** i).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i] + ); +};