mirror of
https://github.com/movie-web/movie-web.git
synced 2025-01-14 17:19:12 +01:00
subtitle type checks
This commit is contained in:
parent
1585805d86
commit
9c13be37e8
@ -1,15 +1,28 @@
|
|||||||
import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
|
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 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";
|
import { ContentCaption } from "subsrt-ts/dist/types/handler";
|
||||||
|
|
||||||
export const subtitleTypeList = list().map((type) => `.${type}`);
|
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 const sanitize = DOMPurify.sanitize;
|
||||||
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
||||||
if (caption.url.startsWith("blob:")) return caption.url;
|
|
||||||
let captionBlob: Blob;
|
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, {
|
captionBlob = await proxiedFetch<Blob>(caption.url, {
|
||||||
responseType: "blob" as any,
|
responseType: "blob" as any,
|
||||||
});
|
});
|
||||||
@ -18,7 +31,10 @@ export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
|||||||
responseType: "blob" as any,
|
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) {
|
export function revokeCaptionBlob(url: string | undefined) {
|
||||||
@ -28,10 +44,14 @@ export function revokeCaptionBlob(url: string | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseSubtitles(text: string): ContentCaption[] {
|
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");
|
throw new Error("Invalid subtitle format");
|
||||||
}
|
}
|
||||||
return parse(text).filter(
|
return parse(textTrimmed).filter(
|
||||||
(cue) => cue.type === "caption"
|
(cue) => cue.type === "caption"
|
||||||
) as ContentCaption[];
|
) as ContentCaption[];
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,16 @@ export enum MWStreamType {
|
|||||||
HLS = "hls",
|
HLS = "hls",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subsrt-ts supported types
|
||||||
export enum MWCaptionType {
|
export enum MWCaptionType {
|
||||||
VTT = "vtt",
|
VTT = "vtt",
|
||||||
SRT = "srt",
|
SRT = "srt",
|
||||||
|
LRC = "lrc",
|
||||||
|
SBV = "sbv",
|
||||||
|
SUB = "sub",
|
||||||
|
SSA = "ssa",
|
||||||
|
ASS = "ass",
|
||||||
|
JSON = "json",
|
||||||
UNKNOWN = "unknown",
|
UNKNOWN = "unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { compareTitle } from "@/utils/titleMatch";
|
import { compareTitle } from "@/utils/titleMatch";
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import {
|
import { MWCaption, MWStreamQuality, MWStreamType } from "../helpers/streams";
|
||||||
MWCaptionType,
|
|
||||||
MWStreamQuality,
|
|
||||||
MWStreamType,
|
|
||||||
} from "../helpers/streams";
|
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
import {
|
||||||
|
getMWCaptionTypeFromUrl,
|
||||||
|
isSupportedSubtitle,
|
||||||
|
} from "../helpers/captions";
|
||||||
|
|
||||||
const flixHqBase = "https://api.consumet.org/meta/tmdb";
|
const flixHqBase = "https://api.consumet.org/meta/tmdb";
|
||||||
|
|
||||||
@ -19,15 +19,19 @@ interface FLIXMediaBase {
|
|||||||
type: FlixHQMediaType;
|
type: FlixHQMediaType;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
}
|
}
|
||||||
|
interface FLIXSubType {
|
||||||
function castSubtitles({ url, lang }: { url: string; lang: string }) {
|
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 {
|
return {
|
||||||
url,
|
url,
|
||||||
langIso: lang,
|
langIso: lang,
|
||||||
type:
|
type,
|
||||||
url.substring(url.length - 3) === "vtt"
|
|
||||||
? MWCaptionType.VTT
|
|
||||||
: MWCaptionType.SRT,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,11 +120,7 @@ registerProvider({
|
|||||||
streamUrl: source.url,
|
streamUrl: source.url,
|
||||||
quality: qualityMap[source.quality],
|
quality: qualityMap[source.quality],
|
||||||
type: source.isM3U8 ? MWStreamType.HLS : MWStreamType.MP4,
|
type: source.isM3U8 ? MWStreamType.HLS : MWStreamType.MP4,
|
||||||
captions: watchInfo.subtitles
|
captions: watchInfo.subtitles.map(convertSubtitles).filter(Boolean),
|
||||||
.filter(
|
|
||||||
(x: { url: string; lang: string }) => !x.lang.includes("(maybe)")
|
|
||||||
)
|
|
||||||
.map(castSubtitles),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,10 @@ import {
|
|||||||
MWStreamType,
|
MWStreamType,
|
||||||
} from "@/backend/helpers/streams";
|
} from "@/backend/helpers/streams";
|
||||||
import { compareTitle } from "@/utils/titleMatch";
|
import { compareTitle } from "@/utils/titleMatch";
|
||||||
|
import {
|
||||||
|
getMWCaptionTypeFromUrl,
|
||||||
|
isSupportedSubtitle,
|
||||||
|
} from "@/backend/helpers/captions";
|
||||||
|
|
||||||
const nanoid = customAlphabet("0123456789abcdef", 32);
|
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({
|
registerProvider({
|
||||||
id: "superstream",
|
id: "superstream",
|
||||||
displayName: "Superstream",
|
displayName: "Superstream",
|
||||||
@ -164,16 +192,9 @@ registerProvider({
|
|||||||
|
|
||||||
const subtitleRes = (await get(subtitleApiQuery)).data;
|
const subtitleRes = (await get(subtitleApiQuery)).data;
|
||||||
|
|
||||||
const mappedCaptions = subtitleRes.list.map(
|
const mappedCaptions = subtitleRes.list
|
||||||
(subtitle: any): MWCaption => {
|
.map(convertSubtitles)
|
||||||
return {
|
.filter(Boolean);
|
||||||
needsProxy: true,
|
|
||||||
langIso: subtitle.language,
|
|
||||||
url: subtitle.subtitles[0].file_path,
|
|
||||||
type: MWCaptionType.SRT,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
embeds: [],
|
embeds: [],
|
||||||
@ -224,24 +245,9 @@ registerProvider({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const subtitleRes = (await get(subtitleApiQuery)).data;
|
const subtitleRes = (await get(subtitleApiQuery)).data;
|
||||||
|
const mappedCaptions = subtitleRes.list
|
||||||
const mappedCaptions = subtitleRes.list.map(
|
.map(convertSubtitles)
|
||||||
(subtitle: any): MWCaption | null => {
|
.filter(Boolean);
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
embeds: [],
|
embeds: [],
|
||||||
stream: {
|
stream: {
|
||||||
|
@ -2,7 +2,7 @@ import { Transition } from "@/components/Transition";
|
|||||||
import { useSettings } from "@/state/settings";
|
import { useSettings } from "@/state/settings";
|
||||||
import { sanitize, parseSubtitles } from "@/backend/helpers/captions";
|
import { sanitize, parseSubtitles } from "@/backend/helpers/captions";
|
||||||
import { ContentCaption } from "subsrt-ts/dist/types/handler";
|
import { ContentCaption } from "subsrt-ts/dist/types/handler";
|
||||||
import { useRef } from "react";
|
import { useRef, useEffect, useCallback } from "react";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
import { useVideoPlayerDescriptor } from "../../state/hooks";
|
import { useVideoPlayerDescriptor } from "../../state/hooks";
|
||||||
import { useProgress } from "../../state/logic/progress";
|
import { useProgress } from "../../state/logic/progress";
|
||||||
@ -47,7 +47,7 @@ export function CaptionRendererAction({
|
|||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const source = useSource(descriptor).source;
|
const source = useSource(descriptor).source;
|
||||||
const videoTime = useProgress(descriptor).time;
|
const videoTime = useProgress(descriptor).time;
|
||||||
const { captionSettings } = useSettings();
|
const { captionSettings, setCaptionDelay } = useSettings();
|
||||||
const captions = useRef<ContentCaption[]>([]);
|
const captions = useRef<ContentCaption[]>([]);
|
||||||
|
|
||||||
useAsync(async () => {
|
useAsync(async () => {
|
||||||
@ -60,20 +60,37 @@ export function CaptionRendererAction({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
captions.current = [];
|
captions.current = [];
|
||||||
}
|
}
|
||||||
|
// reset delay on every subtitle change
|
||||||
|
setCaptionDelay(0);
|
||||||
} else {
|
} else {
|
||||||
captions.current = [];
|
captions.current = [];
|
||||||
}
|
}
|
||||||
}, [source?.caption?.url]);
|
}, [source?.caption?.url]);
|
||||||
|
useEffect(() => {
|
||||||
if (!captions.current.length) return null;
|
// reset delay after video ends
|
||||||
const isVisible = (start: number, end: number): boolean => {
|
return () => setCaptionDelay(0);
|
||||||
const delayedStart = start / 1000 + captionSettings.delay;
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const delayedEnd = end / 1000 + captionSettings.delay;
|
}, []);
|
||||||
|
const isVisible = useCallback(
|
||||||
|
(
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
delay: number,
|
||||||
|
currentTime: number
|
||||||
|
): boolean => {
|
||||||
|
const delayedStart = start / 1000 + delay;
|
||||||
|
const delayedEnd = end / 1000 + delay;
|
||||||
return (
|
return (
|
||||||
Math.max(0, delayedStart) <= videoTime &&
|
Math.max(0, delayedStart) <= currentTime &&
|
||||||
Math.max(0, delayedEnd) >= videoTime
|
Math.max(0, delayedEnd) >= currentTime
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
if (!captions.current.length) return null;
|
||||||
|
const visibileCaptions = captions.current.filter(({ start, end }) =>
|
||||||
|
isVisible(start, end, captionSettings.delay, videoTime)
|
||||||
);
|
);
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
className={[
|
className={[
|
||||||
@ -83,12 +100,9 @@ export function CaptionRendererAction({
|
|||||||
animation="slide-up"
|
animation="slide-up"
|
||||||
show
|
show
|
||||||
>
|
>
|
||||||
{captions.current.map(
|
{visibileCaptions.map(({ start, end, content }) => (
|
||||||
({ start, end, content }) =>
|
|
||||||
isVisible(start, end) && (
|
|
||||||
<CaptionCue key={`${start}-${end}`} text={content} />
|
<CaptionCue key={`${start}-${end}`} text={content} />
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
</Transition>
|
</Transition>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user