subtitle type checks

This commit is contained in:
frost768 2023-04-03 23:18:10 +03:00
parent 1585805d86
commit 9c13be37e8
5 changed files with 115 additions and 68 deletions

View File

@ -1,15 +1,28 @@
import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
import { MWCaption } from "@/backend/helpers/streams";
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
import DOMPurify from "dompurify";
import { parse, detect, list } from "subsrt-ts";
import { parse, detect, list, convert } from "subsrt-ts";
import { ContentCaption } from "subsrt-ts/dist/types/handler";
export const subtitleTypeList = list().map((type) => `.${type}`);
export function isSupportedSubtitle(url: string): boolean {
return subtitleTypeList.some((type) => url.endsWith(type));
}
export function getMWCaptionTypeFromUrl(url: string): MWCaptionType {
if (!isSupportedSubtitle(url)) return MWCaptionType.UNKNOWN;
const type = subtitleTypeList.find((t) => url.endsWith(t));
if (!type) return MWCaptionType.UNKNOWN;
return type.slice(1) as MWCaptionType;
}
export const sanitize = DOMPurify.sanitize;
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
if (caption.url.startsWith("blob:")) return caption.url;
let captionBlob: Blob;
if (caption.needsProxy) {
if (caption.url.startsWith("blob:")) {
// custom subtitle
captionBlob = await (await fetch(caption.url)).blob();
} else if (caption.needsProxy) {
captionBlob = await proxiedFetch<Blob>(caption.url, {
responseType: "blob" as any,
});
@ -18,7 +31,10 @@ export async function getCaptionUrl(caption: MWCaption): Promise<string> {
responseType: "blob" as any,
});
}
return URL.createObjectURL(captionBlob);
// convert to vtt for track element source which will be used in PiP mode
const text = await captionBlob.text();
const vtt = convert(text, "vtt");
return URL.createObjectURL(new Blob([vtt], { type: "text/vtt" }));
}
export function revokeCaptionBlob(url: string | undefined) {
@ -28,10 +44,14 @@ export function revokeCaptionBlob(url: string | undefined) {
}
export function parseSubtitles(text: string): ContentCaption[] {
if (detect(text) === "") {
const textTrimmed = text.trim();
if (textTrimmed === "") {
throw new Error("Given text is empty");
}
if (detect(textTrimmed) === "") {
throw new Error("Invalid subtitle format");
}
return parse(text).filter(
return parse(textTrimmed).filter(
(cue) => cue.type === "caption"
) as ContentCaption[];
}

View File

@ -3,9 +3,16 @@ export enum MWStreamType {
HLS = "hls",
}
// subsrt-ts supported types
export enum MWCaptionType {
VTT = "vtt",
SRT = "srt",
LRC = "lrc",
SBV = "sbv",
SUB = "sub",
SSA = "ssa",
ASS = "ass",
JSON = "json",
UNKNOWN = "unknown",
}

View File

@ -1,12 +1,12 @@
import { compareTitle } from "@/utils/titleMatch";
import { proxiedFetch } from "../helpers/fetch";
import { registerProvider } from "../helpers/register";
import {
MWCaptionType,
MWStreamQuality,
MWStreamType,
} from "../helpers/streams";
import { MWCaption, MWStreamQuality, MWStreamType } from "../helpers/streams";
import { MWMediaType } from "../metadata/types";
import {
getMWCaptionTypeFromUrl,
isSupportedSubtitle,
} from "../helpers/captions";
const flixHqBase = "https://api.consumet.org/meta/tmdb";
@ -19,15 +19,19 @@ interface FLIXMediaBase {
type: FlixHQMediaType;
releaseDate: string;
}
function castSubtitles({ url, lang }: { url: string; lang: string }) {
interface FLIXSubType {
url: string;
lang: string;
}
function convertSubtitles({ url, lang }: FLIXSubType): MWCaption | null {
if (lang.includes("(maybe)")) return null;
const supported = isSupportedSubtitle(url);
if (!supported) return null;
const type = getMWCaptionTypeFromUrl(url);
return {
url,
langIso: lang,
type:
url.substring(url.length - 3) === "vtt"
? MWCaptionType.VTT
: MWCaptionType.SRT,
type,
};
}
@ -116,11 +120,7 @@ registerProvider({
streamUrl: source.url,
quality: qualityMap[source.quality],
type: source.isM3U8 ? MWStreamType.HLS : MWStreamType.MP4,
captions: watchInfo.subtitles
.filter(
(x: { url: string; lang: string }) => !x.lang.includes("(maybe)")
)
.map(castSubtitles),
captions: watchInfo.subtitles.map(convertSubtitles).filter(Boolean),
},
};
},

View File

@ -11,6 +11,10 @@ import {
MWStreamType,
} from "@/backend/helpers/streams";
import { compareTitle } from "@/utils/titleMatch";
import {
getMWCaptionTypeFromUrl,
isSupportedSubtitle,
} from "@/backend/helpers/captions";
const nanoid = customAlphabet("0123456789abcdef", 32);
@ -111,6 +115,30 @@ const getBestQuality = (list: any[]) => {
);
};
const convertSubtitles = (subtitleGroup: any): MWCaption | null => {
let subtitles = subtitleGroup.subtitles;
subtitles = subtitles
.map((subFile: any) => {
const supported = isSupportedSubtitle(subFile.file_path);
if (!supported) return null;
const type = getMWCaptionTypeFromUrl(subFile.file_path);
return {
...subFile,
type: type as MWCaptionType,
};
})
.filter(Boolean);
if (!subtitles.length) return null;
const subFile = subtitles[0];
return {
needsProxy: true,
langIso: subtitleGroup.language,
url: subFile.file_path,
type: subFile.type,
};
};
registerProvider({
id: "superstream",
displayName: "Superstream",
@ -164,16 +192,9 @@ registerProvider({
const subtitleRes = (await get(subtitleApiQuery)).data;
const mappedCaptions = subtitleRes.list.map(
(subtitle: any): MWCaption => {
return {
needsProxy: true,
langIso: subtitle.language,
url: subtitle.subtitles[0].file_path,
type: MWCaptionType.SRT,
};
}
);
const mappedCaptions = subtitleRes.list
.map(convertSubtitles)
.filter(Boolean);
return {
embeds: [],
@ -224,24 +245,9 @@ registerProvider({
};
const subtitleRes = (await get(subtitleApiQuery)).data;
const mappedCaptions = subtitleRes.list.map(
(subtitle: any): MWCaption | null => {
const sub = subtitle;
sub.subtitles = subtitle.subtitles.filter((subFile: any) => {
const extension = subFile.file_path.substring(
sub.file_path.length - 3
);
return [MWCaptionType.SRT, MWCaptionType.VTT].includes(extension);
});
return {
needsProxy: true,
langIso: subtitle.language,
url: sub.subtitles[0].file_path,
type: MWCaptionType.SRT,
};
}
);
const mappedCaptions = subtitleRes.list
.map(convertSubtitles)
.filter(Boolean);
return {
embeds: [],
stream: {

View File

@ -2,7 +2,7 @@ import { Transition } from "@/components/Transition";
import { useSettings } from "@/state/settings";
import { sanitize, parseSubtitles } from "@/backend/helpers/captions";
import { ContentCaption } from "subsrt-ts/dist/types/handler";
import { useRef } from "react";
import { useRef, useEffect, useCallback } from "react";
import { useAsync } from "react-use";
import { useVideoPlayerDescriptor } from "../../state/hooks";
import { useProgress } from "../../state/logic/progress";
@ -47,7 +47,7 @@ export function CaptionRendererAction({
const descriptor = useVideoPlayerDescriptor();
const source = useSource(descriptor).source;
const videoTime = useProgress(descriptor).time;
const { captionSettings } = useSettings();
const { captionSettings, setCaptionDelay } = useSettings();
const captions = useRef<ContentCaption[]>([]);
useAsync(async () => {
@ -60,20 +60,37 @@ export function CaptionRendererAction({
} catch (error) {
captions.current = [];
}
// reset delay on every subtitle change
setCaptionDelay(0);
} else {
captions.current = [];
}
}, [source?.caption?.url]);
useEffect(() => {
// reset delay after video ends
return () => setCaptionDelay(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isVisible = useCallback(
(
start: number,
end: number,
delay: number,
currentTime: number
): boolean => {
const delayedStart = start / 1000 + delay;
const delayedEnd = end / 1000 + delay;
return (
Math.max(0, delayedStart) <= currentTime &&
Math.max(0, delayedEnd) >= currentTime
);
},
[]
);
if (!captions.current.length) return null;
const isVisible = (start: number, end: number): boolean => {
const delayedStart = start / 1000 + captionSettings.delay;
const delayedEnd = end / 1000 + captionSettings.delay;
return (
Math.max(0, delayedStart) <= videoTime &&
Math.max(0, delayedEnd) >= videoTime
);
};
const visibileCaptions = captions.current.filter(({ start, end }) =>
isVisible(start, end, captionSettings.delay, videoTime)
);
return (
<Transition
className={[
@ -83,12 +100,9 @@ export function CaptionRendererAction({
animation="slide-up"
show
>
{captions.current.map(
({ start, end, content }) =>
isVisible(start, end) && (
<CaptionCue key={`${start}-${end}`} text={content} />
)
)}
{visibileCaptions.map(({ start, end, content }) => (
<CaptionCue key={`${start}-${end}`} text={content} />
))}
</Transition>
);
}